diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 37b7bc2ca8c3a..397a106a4574b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -23,7 +23,7 @@ For more detailed information on contribution please read our [beginners guide]( * Unit/integration test coverage * Proposed [documentation](https://devdocs.magento.com) updates. Documentation contributions can be submitted via the [devdocs GitHub](https://github.com/magento/devdocs). 4. For larger features or changes, please [open an issue](https://github.com/magento/magento2/issues) to discuss the proposed changes prior to development. This may prevent duplicate or unnecessary effort and allow other contributors to provide input. -5. All automated tests must pass (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). +5. All automated tests must pass. ## Contribution process diff --git a/.github/ISSUE_TEMPLATE/story.md b/.github/ISSUE_TEMPLATE/story.md new file mode 100644 index 0000000000000..f4ba43d4c4389 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/story.md @@ -0,0 +1,14 @@ +--- +name: GraphQL Story +about: User story for GraphQL project +labels: 'Project: GraphQL' + +--- + +*As a ___ I want to ___ so that ___.* + +### AC +* a +* b +### Approved Schema +* a diff --git a/.htaccess b/.htaccess index e07a564bc0ab6..c5f3bf034d2fb 100644 --- a/.htaccess +++ b/.htaccess @@ -238,15 +238,6 @@ Require all denied - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - order allow,deny diff --git a/.htaccess.sample b/.htaccess.sample index c9e83a53cc8bd..776f9046cf11d 100644 --- a/.htaccess.sample +++ b/.htaccess.sample @@ -238,15 +238,6 @@ Require all denied - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - order allow,deny diff --git a/CHANGELOG.md b/CHANGELOG.md index 4661c4875737d..919f3f020088b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,817 @@ +2.4.0 +============= +* GitHub issues: + * [#24229](https://github.com/magento/magento2/issues/24229) -- Unable to enable maintenance mode when env.php is read only (fixed in [magento/magento2#25790](https://github.com/magento/magento2/pull/25790)) + * [#22416](https://github.com/magento/magento2/issues/22416) -- Coupling beetwen Magento_Checkout::js/view/shipping.js:validateShippingInformation() and layout definition or view. (fixed in [magento/magento2#25541](https://github.com/magento/magento2/pull/25541)) + * [#25739](https://github.com/magento/magento2/issues/25739) -- grunt clean does not clean generated folder (fixed in [magento/magento2#25765](https://github.com/magento/magento2/pull/25765)) + * [#25654](https://github.com/magento/magento2/issues/25654) -- Magento OpenGraph meta description / title content bleeding (fixed in [magento/magento2#25655](https://github.com/magento/magento2/pull/25655)) + * [#25731](https://github.com/magento/magento2/issues/25731) -- queue_consumer.xml doesn't allow numbers in handler class (fixed in [magento/magento2#25952](https://github.com/magento/magento2/pull/25952)) + * [#25935](https://github.com/magento/magento2/issues/25935) -- Email address mismatch with text in iPad(768) view (fixed in [magento/magento2#25942](https://github.com/magento/magento2/pull/25942)) + * [#25931](https://github.com/magento/magento2/issues/25931) -- Refresh Statistics: Updated At = Null should be display as "Never" instead of "undefined". (fixed in [magento/magento2#25932](https://github.com/magento/magento2/pull/25932)) + * [#25925](https://github.com/magento/magento2/issues/25925) -- Dupplicate Records when sorting column in Content->Themes Grid (fixed in [magento/magento2#25926](https://github.com/magento/magento2/pull/25926)) + * [#25917](https://github.com/magento/magento2/issues/25917) -- Admin confirm password input doesn't inherit needed styles (fixed in [magento/magento2#25918](https://github.com/magento/magento2/pull/25918)) + * [#25911](https://github.com/magento/magento2/issues/25911) -- Category - Notice on incorrect price filter GET param (fixed in [magento/magento2#25912](https://github.com/magento/magento2/pull/25912)) + * [#25893](https://github.com/magento/magento2/issues/25893) -- A "500 (Internal Server Error)" appears in Developer Console if Delete the image that is added to Page Content (fixed in [magento/magento2#25924](https://github.com/magento/magento2/pull/25924)) + * [#25896](https://github.com/magento/magento2/issues/25896) -- Cannot create folder using only letters (fixed in [magento/magento2#25904](https://github.com/magento/magento2/pull/25904)) + * [#24713](https://github.com/magento/magento2/issues/24713) -- Symbol of the Belarusian currency BYR is outdated (fixed in [magento/magento2#25723](https://github.com/magento/magento2/pull/25723)) + * [#19805](https://github.com/magento/magento2/issues/19805) -- Sales order Address Information edit form layout design improvement. (fixed in [magento/magento2#25699](https://github.com/magento/magento2/pull/25699)) + * [#23481](https://github.com/magento/magento2/issues/23481) -- Billing/Shipping Address edit form design update from order backend (fixed in [magento/magento2#25699](https://github.com/magento/magento2/pull/25699)) + * [#25972](https://github.com/magento/magento2/issues/25972) -- Not required spacing in submenu on hover desktop (fixed in [magento/magento2#25973](https://github.com/magento/magento2/pull/25973)) + * [#25586](https://github.com/magento/magento2/issues/25586) -- Mixins are not applied for advanced bundled modules (fixed in [magento/magento2#25587](https://github.com/magento/magento2/pull/25587)) + * [#20379](https://github.com/magento/magento2/issues/20379) -- calendar icon not aligned inside the textbox in Add Design Change page (fixed in [magento/magento2#26063](https://github.com/magento/magento2/pull/26063)) + * [#18687](https://github.com/magento/magento2/issues/18687) -- Left Side Back End Menu Design fix (fixed in [magento/magento2#26034](https://github.com/magento/magento2/pull/26034)) + * [#24025](https://github.com/magento/magento2/issues/24025) -- Slow Performance of ProductMetadata::getVersion (fixed in [magento/magento2#26001](https://github.com/magento/magento2/pull/26001)) + * [#100](https://github.com/magento/partners-magento2ee/issues/100) -- Users can see Negotiable Quotes from other Company (fixed in [magento/magento2#25940](https://github.com/magento/magento2/pull/25940) and [magento/partners-magento2ee#134](https://github.com/magento/partners-magento2ee/pull/134)) + * [#24357](https://github.com/magento/magento2/issues/24357) -- Eav sort order by attribute option_id (fixed in [magento/magento2#24360](https://github.com/magento/magento2/pull/24360)) + * [#25930](https://github.com/magento/magento2/issues/25930) -- Integration Success Message Text Overflow Issue in Admin (fixed in [magento/magento2#26011](https://github.com/magento/magento2/pull/26011)) + * [#25433](https://github.com/magento/magento2/issues/25433) -- Close (X) not working when error come for qty (fixed in [magento/magento2#25759](https://github.com/magento/magento2/pull/25759)) + * [#26155](https://github.com/magento/magento2/issues/26155) -- Table quote column customer_note uses wrong type (fixed in [magento/magento2#26160](https://github.com/magento/magento2/pull/26160)) + * [#761](https://github.com/magento/magento2/issues/761) -- A more verbose message when the db is not up to date. (fixed in [magento/magento2#25864](https://github.com/magento/magento2/pull/25864)) + * [#25974](https://github.com/magento/magento2/issues/25974) -- Amount of characters on a 'Area' Customizable Option counted differently on backend/frontend (fixed in [magento/magento2#26033](https://github.com/magento/magento2/pull/26033)) + * [#25674](https://github.com/magento/magento2/issues/25674) -- Elasticsearch version selections in admin are overly broad (fixed in [magento/magento2#25838](https://github.com/magento/magento2/pull/25838)) + * [#13136](https://github.com/magento/magento2/issues/13136) -- Error in vendor/magento/module-shipping/Model/Config/Source/Allmethods.php - public function toOptionArray (fixed in [magento/magento2#25315](https://github.com/magento/magento2/pull/25315)) + * [#22047](https://github.com/magento/magento2/issues/22047) -- Magento CRON Job Names are missing in NewRelic: "Transaction Names" (fixed in [magento/magento2#25957](https://github.com/magento/magento2/pull/25957)) + * [#26164](https://github.com/magento/magento2/issues/26164) -- Underline should not display on hover for delete icon at shopping cart Internet explorer browser (fixed in [magento/magento2#26173](https://github.com/magento/magento2/pull/26173)) + * [#24972](https://github.com/magento/magento2/issues/24972) -- Special Price class not added in configurable product page (fixed in [magento/magento2#26170](https://github.com/magento/magento2/pull/26170)) + * [#25659](https://github.com/magento/magento2/issues/25659) -- Paypal Payments Pro IPN keeping payments marked as Pending Payment (fixed in [magento/magento2#25876](https://github.com/magento/magento2/pull/25876)) + * [#18717](https://github.com/magento/magento2/issues/18717) -- UrlRewrite removes query string from url, if url has trailing slash (fixed in [magento/magento2#25603](https://github.com/magento/magento2/pull/25603)) + * [#26176](https://github.com/magento/magento2/issues/26176) -- Footer Newsletter input field width is not identical in Internet Explorer/EDGE browser compared with chrome (fixed in [magento/magento2#26182](https://github.com/magento/magento2/pull/26182)) + * [#25390](https://github.com/magento/magento2/issues/25390) -- UPS carrier model getting error when create plugin in to Magento 2.3.3 compatibility (fixed in [magento/magento2#26130](https://github.com/magento/magento2/pull/26130)) + * [#26083](https://github.com/magento/magento2/issues/26083) -- Problem when trying to unset additional data in payment method. (fixed in [magento/magento2#26084](https://github.com/magento/magento2/pull/26084)) + * [#26064](https://github.com/magento/magento2/issues/26064) -- Incorrect Error Message While Sharing Wish list more than Specified Email Address Value in Admin Configuration (fixed in [magento/magento2#26066](https://github.com/magento/magento2/pull/26066)) + * [#14663](https://github.com/magento/magento2/issues/14663) -- Updating Customer through rest/all/V1/customers/:id resets group_id if group_id not passed in payload (fixed in [magento/magento2#25958](https://github.com/magento/magento2/pull/25958)) + * [#20966](https://github.com/magento/magento2/issues/20966) -- Elastic Search 5 Indexing Performance Issue (fixed in [magento/magento2#25452](https://github.com/magento/magento2/pull/25452)) + * [#21684](https://github.com/magento/magento2/issues/21684) -- Currency sign for "Layered Navigation Price Step" is not according to default settings (fixed in [magento/magento2#24815](https://github.com/magento/magento2/pull/24815)) + * [#24468](https://github.com/magento/magento2/issues/24468) -- Export Coupon Code Grid redirect to DashBoard when create New Cart Price Rule (fixed in [magento/magento2#24471](https://github.com/magento/magento2/pull/24471)) + * [#22856](https://github.com/magento/magento2/issues/22856) -- Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page (fixed in [magento/magento2#22917](https://github.com/magento/magento2/pull/22917)) + * [#14001](https://github.com/magento/magento2/issues/14001) -- M2.2.3 directory_country_region_name locale fix? 8bytes zh_Hans_CN(11bytes) ca_ES_VALENCIA(14bytes) (fixed in [magento/magento2#26268](https://github.com/magento/magento2/pull/26268)) + * [#23521](https://github.com/magento/magento2/issues/23521) -- Unable to run \Magento\Downloadable\Test\Unit\Helper\DownloadTest without internet connection / dns resolution (fixed in [magento/magento2#26264](https://github.com/magento/magento2/pull/26264)) + * [#25936](https://github.com/magento/magento2/issues/25936) -- Regular Price Label Alignment Issues in Frontend (fixed in [magento/magento2#26237](https://github.com/magento/magento2/pull/26237)) + * [#26227](https://github.com/magento/magento2/issues/26227) -- Need some space between input and update button Minicart (fixed in [magento/magento2#26234](https://github.com/magento/magento2/pull/26234)) + * [#26208](https://github.com/magento/magento2/issues/26208) -- Sorting issue for status column for Cache Management (fixed in [magento/magento2#26215](https://github.com/magento/magento2/pull/26215)) + * [#26206](https://github.com/magento/magento2/issues/26206) -- Missing information about currently reindexed index on failure (fixed in [magento/magento2#26207](https://github.com/magento/magento2/pull/26207)) + * [#26181](https://github.com/magento/magento2/issues/26181) -- Out of stock text is not aligned properly with add to cart button at list page in responsive (fixed in [magento/magento2#26183](https://github.com/magento/magento2/pull/26183)) + * [#26168](https://github.com/magento/magento2/issues/26168) -- Input Checkbox Alignment Issue at checkout page in Safari Browser (fixed in [magento/magento2#26169](https://github.com/magento/magento2/pull/26169)) + * [#19093](https://github.com/magento/magento2/issues/19093) -- API: salesOrderItemRepository does not include gift message (fixed in [magento/magento2#25946](https://github.com/magento/magento2/pull/25946)) + * [#23350](https://github.com/magento/magento2/issues/23350) -- Add support for catching throwables in App/Bootstrap (fixed in [magento/magento2#25250](https://github.com/magento/magento2/pull/25250)) + * [#26289](https://github.com/magento/magento2/issues/26289) -- Jump Datepicker in Catalog Price Rule (fixed in [magento/magento2#26290](https://github.com/magento/magento2/pull/26290)) + * [#22964](https://github.com/magento/magento2/issues/22964) -- Unable to save any dates if the user interface locale is not english (US) in 2.3.1 (fixed in [magento/magento2#26270](https://github.com/magento/magento2/pull/26270)) + * [#14913](https://github.com/magento/magento2/issues/14913) -- bookmark views become uneditable after deleting the first bookmark view. (fixed in [magento/magento2#26263](https://github.com/magento/magento2/pull/26263)) + * [#26217](https://github.com/magento/magento2/issues/26217) -- Wrong fields selection while using fragments on GraphQL products query (fixed in [magento/magento2#26218](https://github.com/magento/magento2/pull/26218)) + * [#23899](https://github.com/magento/magento2/issues/23899) -- system.xml file validation issue (fixed in [magento/magento2#25985](https://github.com/magento/magento2/pull/25985)) + * [#14971](https://github.com/magento/magento2/issues/14971) -- Improper Handling of Pagination SEO (fixed in [magento/magento2#25337](https://github.com/magento/magento2/pull/25337)) + * [#22988](https://github.com/magento/magento2/issues/22988) -- Wrong behavior of grid row and checkbox click (fixed in [magento/magento2#22990](https://github.com/magento/magento2/pull/22990)) + * [#7065](https://github.com/magento/magento2/issues/7065) -- page.main.title is translating title (fixed in [magento/magento2#26269](https://github.com/magento/magento2/pull/26269)) + * [#11209](https://github.com/magento/magento2/issues/11209) -- Wishlist Add grouped product Error (fixed in [magento/magento2#26258](https://github.com/magento/magento2/pull/26258)) + * [#26235](https://github.com/magento/magento2/issues/26235) -- Both Menu spacing should be same (fixed in [magento/magento2#26238](https://github.com/magento/magento2/pull/26238)) + * [#25130](https://github.com/magento/magento2/issues/25130) -- Issue with reorder when disabled reorder setting from admin (fixed in [magento/magento2#26051](https://github.com/magento/magento2/pull/26051)) + * [#25881](https://github.com/magento/magento2/issues/25881) -- Admin panel is not accessible after limited permissions set to at least one admin account (fixed in [magento/magento2#25909](https://github.com/magento/magento2/pull/25909)) + * [#25373](https://github.com/magento/magento2/issues/25373) -- The 'promotion' region of the minicart is never rendered (fixed in [magento/magento2#25375](https://github.com/magento/magento2/pull/25375)) + * [#25278](https://github.com/magento/magento2/issues/25278) -- Incorrect @return type at getSourceModel in Eav\Attribute (fixed in [magento/magento2#25333](https://github.com/magento/magento2/pull/25333)) + * [#25188](https://github.com/magento/magento2/issues/25188) -- Magento 2.3: Import fails if configurable attribute has an equal sign in its value (fixed in [magento/magento2#25194](https://github.com/magento/magento2/pull/25194)) + * [#22304](https://github.com/magento/magento2/issues/22304) -- [Grouped product] Can´t add simple products to cart if one other is out of stock (fixed in [magento/magento2#24955](https://github.com/magento/magento2/pull/24955)) + * [#26331](https://github.com/magento/magento2/issues/26331) -- [ MFTF ] Mess in ActionGroups: invalid names, multiple nodes. (fixed in [magento/partners-magento2ee#120](https://github.com/magento/partners-magento2ee/pull/120) and [magento/partners-magento2ee#108](https://github.com/magento/partners-magento2ee/pull/108) and [magento/partners-magento2ee#107](https://github.com/magento/partners-magento2ee/pull/107) and [magento/partners-magento2ee#106](https://github.com/magento/partners-magento2ee/pull/106) and [magento/partners-magento2ee#104](https://github.com/magento/partners-magento2ee/pull/104) and [magento/partners-magento2ee#105](https://github.com/magento/partners-magento2ee/pull/105) and [magento/partners-magento2ee#119](https://github.com/magento/partners-magento2ee/pull/119) and [magento/magento2#26323](https://github.com/magento/magento2/pull/26323) and [magento/magento2#26321](https://github.com/magento/magento2/pull/26321) and [magento/partners-magento2ee#111](https://github.com/magento/partners-magento2ee/pull/111) and [magento/magento2#26320](https://github.com/magento/magento2/pull/26320) and [magento/magento2#26319](https://github.com/magento/magento2/pull/26319) and [magento/partners-magento2ee#109](https://github.com/magento/partners-magento2ee/pull/109) and [magento/magento2#26322](https://github.com/magento/magento2/pull/26322) and [magento/partners-magento2ee#121](https://github.com/magento/partners-magento2ee/pull/121) and [magento/partners-magento2ee#117](https://github.com/magento/partners-magento2ee/pull/117) and [magento/partners-magento2ee#116](https://github.com/magento/partners-magento2ee/pull/116) and [magento/magento2#25828](https://github.com/magento/magento2/pull/25828) and [magento/magento2#26329](https://github.com/magento/magento2/pull/26329)) + * [#22909](https://github.com/magento/partners-magento2ee/issues/22909) -- requirejs/domReady.js can severely delay rendering of content (fixed in [magento/magento2#23313](https://github.com/magento/magento2/pull/23313) and [magento/partners-magento2ee#50](https://github.com/magento/partners-magento2ee/pull/50)) + * [#26396](https://github.com/magento/magento2/issues/26396) -- MFTF: Functional Tests are failing in Magento CI process (fixed in [magento/magento2#26407](https://github.com/magento/magento2/pull/26407) and [magento/magento2#26395](https://github.com/magento/magento2/pull/26395)) + * [#26364](https://github.com/magento/magento2/issues/26364) -- Add to Compare link not showing in mobile view under 640px (fixed in [magento/magento2#26424](https://github.com/magento/magento2/pull/26424) and [magento/magento2#26365](https://github.com/magento/magento2/pull/26365)) + * [#25968](https://github.com/magento/magento2/issues/25968) -- `getPrice()` returns a string when setting custom price in admin order (fixed in [magento/magento2#26313](https://github.com/magento/magento2/pull/26313)) + * [#26612](https://github.com/magento/magento2/issues/26612) -- MFTF: StorefrontApplyPromoCodeDuringCheckoutTest is failing in CI process (fixed in [magento/magento2#26614](https://github.com/magento/magento2/pull/26614)) + * [#26437](https://github.com/magento/magento2/issues/26437) -- Viewing customer shopping cart in admin shows all products in catalog when there is no active quote (fixed in [magento/magento2#26489](https://github.com/magento/magento2/pull/26489)) + * [#26479](https://github.com/magento/magento2/issues/26479) -- Bug: AutoloaderRegistry::getAutoloader returns array (fixed in [magento/magento2#26480](https://github.com/magento/magento2/pull/26480)) + * [#25162](https://github.com/magento/magento2/issues/25162) -- Message at Frontend has No HTML format (fixed in [magento/magento2#26455](https://github.com/magento/magento2/pull/26455)) + * [#25761](https://github.com/magento/magento2/issues/25761) -- Site map doesn't include home page (fixed in [magento/magento2#26445](https://github.com/magento/magento2/pull/26445)) + * [#18012](https://github.com/magento/magento2/issues/18012) -- Can not add string to underscore template using knockout (fixed in [magento/magento2#26435](https://github.com/magento/magento2/pull/26435)) + * [#25300](https://github.com/magento/magento2/issues/25300) -- Mobile view issue on category page - Sort By label overlaps with Shop By button (fixed in [magento/magento2#26381](https://github.com/magento/magento2/pull/26381)) + * [#26275](https://github.com/magento/magento2/issues/26275) -- Whitespace between label and required star on Checkout page (fixed in [magento/magento2#26285](https://github.com/magento/magento2/pull/26285)) + * [#26065](https://github.com/magento/magento2/issues/26065) -- Performance of isSalable method check on configurable product (fixed in [magento/magento2#26071](https://github.com/magento/magento2/pull/26071)) + * [#21014](https://github.com/magento/magento2/issues/21014) -- Gallery Thumbnail (left/right) Scroll Performance Android Chrome Sluggish and Unresponsive (fixed in [magento/magento2#25839](https://github.com/magento/magento2/pull/25839)) + * [#10518](https://github.com/magento/magento2/issues/10518) -- Mobile product page image jumps (fixed in [magento/magento2#25385](https://github.com/magento/magento2/pull/25385)) + * [#21717](https://github.com/magento/magento2/issues/21717) -- Product view page scrolls up randomly on mobile device (fixed in [magento/magento2#25385](https://github.com/magento/magento2/pull/25385)) + * [#25962](https://github.com/magento/magento2/issues/25962) -- Radio alignment issue (fixed in [magento/magento2#25966](https://github.com/magento/magento2/pull/25966)) + * [#9466](https://github.com/magento/magento2/issues/9466) -- Duplicating product copies product images couple of hundred times (fixed in [magento/magento2#25875](https://github.com/magento/magento2/pull/25875)) + * [#17125](https://github.com/magento/magento2/issues/17125) -- x-magento-init initialisation not bound to happen in the right order. (fixed in [magento/magento2#25764](https://github.com/magento/magento2/pull/25764)) + * [#26610](https://github.com/magento/magento2/issues/26610) -- MFTF: AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest is failing in CI process (fixed in [magento/magento2#26611](https://github.com/magento/magento2/pull/26611)) + * [#26240](https://github.com/magento/magento2/issues/26240) -- Minimum Advertised Price doesn't change for selected swatch option for configurable product (fixed in [magento/magento2#26241](https://github.com/magento/magento2/pull/26241) and [magento/magento2#26317](https://github.com/magento/magento2/pull/26317)) + * [#17847](https://github.com/magento/magento2/issues/17847) -- Wrong State Title, Displaying Status Label Rather than State (fixed in [magento/magento2#26569](https://github.com/magento/magento2/pull/26569)) + * [#21555](https://github.com/magento/magento2/issues/21555) -- Anonyomus classes in 2.3 (test data provider) (fixed in [magento/magento2#26533](https://github.com/magento/magento2/pull/26533)) + * [#26532](https://github.com/magento/magento2/issues/26532) -- di:setup:compile fails with anonymous classes (fixed in [magento/magento2#26533](https://github.com/magento/magento2/pull/26533)) + * [#26332](https://github.com/magento/magento2/issues/26332) -- BeforeOrderPaymentSaveObserver override payment insructions with wrong store view config (fixed in [magento/magento2#26399](https://github.com/magento/magento2/pull/26399)) + * [#25591](https://github.com/magento/magento2/issues/25591) -- & character in SKUs is shown as & in current variations list on configurable products (fixed in [magento/magento2#26007](https://github.com/magento/magento2/pull/26007)) + * [#13865](https://github.com/magento/magento2/issues/13865) -- Safari "Block all cookies" setting breaks JavaScript scripts (fixed in [magento/magento2#25324](https://github.com/magento/magento2/pull/25324)) + * [#26375](https://github.com/magento/magento2/issues/26375) -- Switching billing address causes Javascript function text to render in front-end checkout payment section (fixed in [magento/magento2#26378](https://github.com/magento/magento2/pull/26378)) + * [#25032](https://github.com/magento/magento2/issues/25032) -- Display some error "We can't update your Wish List right now." at wish list (fixed in [magento/magento2#25641](https://github.com/magento/magento2/pull/25641)) + * [#8691](https://github.com/magento/magento2/issues/8691) -- Language pack inheritance order is incorrect (fixed in [magento/magento2#26420](https://github.com/magento/magento2/pull/26420)) + * [#25195](https://github.com/magento/magento2/issues/25195) -- Issue with tier price 0 when saving product second time (fixed in [magento/magento2#26162](https://github.com/magento/magento2/pull/26162)) + * [#26622](https://github.com/magento/magento2/issues/26622) -- Fixed cart discount calculated incorrectly when product first added to cart. (fixed in [magento/magento2#26623](https://github.com/magento/magento2/pull/26623)) + * [#26543](https://github.com/magento/magento2/issues/26543) -- My Wish List Product not showing properly between >768px and <1023px (fixed in [magento/magento2#26546](https://github.com/magento/magento2/pull/26546)) + * [#25268](https://github.com/magento/magento2/issues/25268) -- $order->getCustomer() returns NULL for registered customer (fixed in [magento/magento2#26423](https://github.com/magento/magento2/pull/26423)) + * [#26338](https://github.com/magento/magento2/issues/26338) -- Code cleanup for module xml extra end tag removed (fixed in [magento/magento2#26339](https://github.com/magento/magento2/pull/26339)) + * [#26760](https://github.com/magento/magento2/issues/26760) -- Validate html error when enable critical css (fixed in [magento/magento2#26764](https://github.com/magento/magento2/pull/26764)) + * [#14885](https://github.com/magento/magento2/issues/14885) -- Refactoring: Code duplication EmailSender / ShipmentSender and so on (fixed in [magento/magento2#26714](https://github.com/magento/magento2/pull/26714)) + * [#863](https://github.com/magento/magento2/issues/863) -- How to switch base,thumbnail images in magento 2 back end (fixed in [magento/magento2#26502](https://github.com/magento/magento2/pull/26502)) + * [#26276](https://github.com/magento/magento2/issues/26276) -- Checkout. Quote Address Street cloning issue (fixed in [magento/magento2#26279](https://github.com/magento/magento2/pull/26279)) + * [#26245](https://github.com/magento/magento2/issues/26245) -- Magento does not send an email about a refunded grouped product (fixed in [magento/magento2#26246](https://github.com/magento/magento2/pull/26246)) + * [#26141](https://github.com/magento/magento2/issues/26141) -- Modal Popup and Custom subTitle erased (fixed in [magento/magento2#26142](https://github.com/magento/magento2/pull/26142)) + * [#25487](https://github.com/magento/magento2/issues/25487) -- Redis cache grows unilimmited (fixed in [magento/magento2#25488](https://github.com/magento/magento2/pull/25488)) + * [#25245](https://github.com/magento/magento2/issues/25245) -- Warning when Search Terms page is opened by clicking option at the footer (fixed in [magento/magento2#25246](https://github.com/magento/magento2/pull/25246)) + * [#24842](https://github.com/magento/magento2/issues/24842) -- Unable to delete custom option file in admin order create (fixed in [magento/magento2#24843](https://github.com/magento/magento2/pull/24843)) + * [#847](https://github.com/magento/magento2/issues/847) -- Use cursor: pointer for the product online switcher (fixed in [magento/magento2#25991](https://github.com/magento/magento2/pull/25991)) + * [#26843](https://github.com/magento/magento2/issues/26843) -- es_US Spanish (United States ) Locale is not supported in Magento 2.3.4 (fixed in [magento/magento2#26857](https://github.com/magento/magento2/pull/26857)) + * [#26054](https://github.com/magento/magento2/issues/26054) -- Do not duplicate SEO meta data when duplicating a product (fixed in [magento/magento2#26659](https://github.com/magento/magento2/pull/26659)) + * [#26314](https://github.com/magento/magento2/issues/26314) -- Minimum Advertised Prices duplicates for all configurable products with price from selected swatch (fixed in [magento/magento2#26317](https://github.com/magento/magento2/pull/26317)) + * [#24547](https://github.com/magento/magento2/issues/24547) -- Magento\Customer\Model\Account\Redirect::setRedirectCookie() not properly working (fixed in [magento/magento2#24612](https://github.com/magento/magento2/pull/24612)) + * [#26675](https://github.com/magento/magento2/issues/26675) -- Date incorrect on pdf invoice (fixed in [magento/magento2#26701](https://github.com/magento/magento2/pull/26701)) + * [#25675](https://github.com/magento/magento2/issues/25675) -- Unable add product to cart in Magento 2.3.3 backend when stock quantity is 1 - "The requested qty is not available" (fixed in [magento/magento2#26650](https://github.com/magento/magento2/pull/26650)) + * [#26583](https://github.com/magento/magento2/issues/26583) -- Product Detail Page - Tier price (fixed & discount) save percentage displaying wrong calculation (fixed in [magento/magento2#26584](https://github.com/magento/magento2/pull/26584)) + * [#25963](https://github.com/magento/magento2/issues/25963) -- Grid Export rendered data is not reflecting in the exported File, Displayed ID instead of Rendered Label (fixed in [magento/magento2#26523](https://github.com/magento/magento2/pull/26523)) + * [#26416](https://github.com/magento/magento2/issues/26416) -- Compare Products section not showing in mobile view under 767px (fixed in [magento/magento2#26418](https://github.com/magento/magento2/pull/26418)) + * [#25656](https://github.com/magento/magento2/issues/25656) -- M2.3.2 : Nullable getters in Service Contracts will throw a reflection error when used in the web API (fixed in [magento/magento2#25806](https://github.com/magento/magento2/pull/25806)) + * [#24971](https://github.com/magento/magento2/issues/24971) -- Incorrect @var reference in docBlock of a class member variable (fixed in [magento/magento2#24976](https://github.com/magento/magento2/pull/24976)) + * [#14958](https://github.com/magento/magento2/issues/14958) -- sale_sequence_* records are not removed on store view deletion (fixed in [magento/magento2#22296](https://github.com/magento/magento2/pull/22296)) + * [#26607](https://github.com/magento/partners-magento2ee/issues/26607) -- MFTF: AdminReorderWithCatalogPriceTest is failing in CI process (fixed in [magento/magento2#26608](https://github.com/magento/magento2/pull/26608) and [magento/partners-magento2ee#135](https://github.com/magento/partners-magento2ee/pull/135)) + * [#25856](https://github.com/magento/magento2/issues/25856) -- Ordered Products Report not grouping by configurable products variations (fixed in [magento/magento2#25858](https://github.com/magento/magento2/pull/25858)) + * [#26973](https://github.com/magento/magento2/issues/26973) -- Fatal error on calling ImageFactory::create() for product_page_image_large (fixed in [magento/magento2#26974](https://github.com/magento/magento2/pull/26974)) + * [#26917](https://github.com/magento/magento2/issues/26917) -- Tax rate Zip/Post range and check box alignment issue (fixed in [magento/magento2#26932](https://github.com/magento/magento2/pull/26932)) + * [#26838](https://github.com/magento/magento2/issues/26838) -- Low stock report showing disabled products (fixed in [magento/magento2#26862](https://github.com/magento/magento2/pull/26862)) + * [#26229](https://github.com/magento/magento2/issues/26229) -- Active menu is not set when opening admin path Marketing > User Content > Pending Reviews (fixed in [magento/magento2#26230](https://github.com/magento/magento2/pull/26230)) + * [#25910](https://github.com/magento/magento2/issues/25910) -- Choose drop down not close when open another for upload file for swatch (fixed in [magento/magento2#26090](https://github.com/magento/magento2/pull/26090)) + * [#13269](https://github.com/magento/magento2/issues/13269) -- Magento Framework Escaper - Critical log with special symbols (fixed in [magento/magento2#25895](https://github.com/magento/magento2/pull/25895)) + * [#25738](https://github.com/magento/magento2/issues/25738) -- DOMDocument::loadHTML(): Tag date invalid in Entity (fixed in [magento/magento2#25895](https://github.com/magento/magento2/pull/25895)) + * [#572](https://github.com/magento/magento2/issues/572) -- How do I: Bug tracking for Magento 1 + ideas for Magento 2 (fixed in [magento/magento2#25349](https://github.com/magento/magento2/pull/25349)) + * [#26800](https://github.com/magento/magento2/issues/26800) -- Undefined variable $type in Product-Link Management (fixed in [magento/magento2#26979](https://github.com/magento/magento2/pull/26979)) + * [#13252](https://github.com/magento/magento2/issues/13252) -- Fetching customer entity through API will not return 'is_subscribed' extension attribute (fixed in [magento/magento2#25311](https://github.com/magento/magento2/pull/25311)) + * [#27044](https://github.com/magento/magento2/issues/27044) -- BUG: Category Repository get()'s argument `store_id` does not work (fixed in [magento/magento2#27048](https://github.com/magento/magento2/pull/27048)) + * [#27040](https://github.com/magento/magento2/issues/27040) -- Images no longer responsive (fixed in [magento/magento2#27041](https://github.com/magento/magento2/pull/27041)) + * [#17933](https://github.com/magento/magento2/issues/17933) -- Bank Transer Payment Instuctions switch back to default (fixed in [magento/magento2#26765](https://github.com/magento/magento2/pull/26765)) + * [#23755](https://github.com/magento/magento2/issues/23755) -- Store view switcher is wrong , when each store views have different url. (fixed in [magento/magento2#26548](https://github.com/magento/magento2/pull/26548)) + * [#26384](https://github.com/magento/magento2/issues/26384) -- Store switcher redirects to homepage for multistore setup with different domains (fixed in [magento/magento2#26548](https://github.com/magento/magento2/pull/26548)) + * [#25243](https://github.com/magento/magento2/issues/25243) -- Numerical placeholder count in Phrase starts with %1, however js code assumes 0% (fixed in [magento/magento2#25359](https://github.com/magento/magento2/pull/25359)) + * [#23619](https://github.com/magento/magento2/issues/23619) -- Less compilation extend 'mixin' has no matches (fixed in [magento/magento2#24003](https://github.com/magento/magento2/pull/24003)) + * [#27032](https://github.com/magento/magento2/issues/27032) -- Add image lazy loading (fixed in [magento/magento2#27033](https://github.com/magento/magento2/pull/27033)) + * [#25834](https://github.com/magento/magento2/issues/25834) -- Discount fixed amount whole cart applied mutiple time when customer use Check Out with Multiple Addresses (fixed in [magento/magento2#26419](https://github.com/magento/magento2/pull/26419)) + * [#26989](https://github.com/magento/magento2/issues/26989) -- MFTF: Use Magento Cron for reindexing after creating data (fixed in [magento/magento2#26990](https://github.com/magento/magento2/pull/26990)) + * [#27027](https://github.com/magento/magento2/issues/27027) -- Admin date of birth doesn't factor in user locale set (fixed in [magento/magento2#27149](https://github.com/magento/magento2/pull/27149)) + * [#973](https://github.com/magento/magento2/issues/973) -- [Question] Add jenkins-ci ant build.xml and tool configurations to repository (fixed in [magento/magento2#27138](https://github.com/magento/magento2/pull/27138)) + * [#26758](https://github.com/magento/magento2/issues/26758) -- cms-page-specific layouts are not applied if FullActionName differs from page_view (fixed in [magento/magento2#27131](https://github.com/magento/magento2/pull/27131)) + * [#26847](https://github.com/magento/magento2/issues/26847) -- Hitting enter on create folder in media gallery refreshes the page (fixed in [magento/magento2#27029](https://github.com/magento/magento2/pull/27029)) + * [#27009](https://github.com/magento/magento2/issues/27009) -- Missing variable outside CATCH causing a double-fault in Renderer.php (fixed in [magento/magento2#27026](https://github.com/magento/magento2/pull/27026)) + * [#26992](https://github.com/magento/magento2/issues/26992) -- Add New ratings Is active and checkbox alignment issue (fixed in [magento/magento2#27014](https://github.com/magento/magento2/pull/27014)) + * [#20309](https://github.com/magento/magento2/issues/20309) -- URL Rewrites redirect loop (fixed in [magento/magento2#26902](https://github.com/magento/magento2/pull/26902)) + * [#26648](https://github.com/magento/magento2/issues/26648) -- Table bottom border color different then thead and tbody border color (fixed in [magento/magento2#26649](https://github.com/magento/magento2/pull/26649)) + * [#26590](https://github.com/magento/magento2/issues/26590) -- Customer registration multiple form submit (fixed in [magento/magento2#26642](https://github.com/magento/magento2/pull/26642)) + * [#24637](https://github.com/magento/magento2/issues/24637) -- Chinese input in tinymce 4 (fixed in [magento/magento2#25454](https://github.com/magento/magento2/pull/25454)) + * [#22609](https://github.com/magento/magento2/issues/22609) -- Since Magento 2.3 the wysiwyg image uploader incorrectly uses pub/media as storage root. (fixed in [magento/magento2#24878](https://github.com/magento/magento2/pull/24878)) + * [#24735](https://github.com/magento/magento2/issues/24735) -- Image in minicart is blurred on iPhone (fixed in [magento/magento2#24743](https://github.com/magento/magento2/pull/24743)) + * [#14086](https://github.com/magento/magento2/issues/14086) -- Guest cart API ignoring cartId in url for some methods (fixed in [magento/magento2#27172](https://github.com/magento/magento2/pull/27172)) + * [#25219](https://github.com/magento/magento2/issues/25219) -- Custom attributes of images generated by Block\Product\ImageFactory don't render correctly (fixed in [magento/magento2#26959](https://github.com/magento/magento2/pull/26959)) + * [#26499](https://github.com/magento/magento2/issues/26499) -- Product url key is not transliterated anymore if already set (fixed in [magento/magento2#26506](https://github.com/magento/magento2/pull/26506)) + * [#25669](https://github.com/magento/magento2/issues/25669) -- health_check.php fails if any database cache engine configured (fixed in [magento/magento2#25722](https://github.com/magento/magento2/pull/25722)) + * [#20472](https://github.com/magento/magento2/issues/20472) -- Special Price shown without currency symbol in magento backoffice (fixed in [magento/magento2#27261](https://github.com/magento/magento2/pull/27261)) + * [#20906](https://github.com/magento/magento2/issues/20906) -- Magento backend catalog "Cost" without currency symbol (fixed in [magento/magento2#27261](https://github.com/magento/magento2/pull/27261)) + * [#21910](https://github.com/magento/magento2/issues/21910) -- Magento backend catalog "MSRP" without currency symbol (fixed in [magento/magento2#27261](https://github.com/magento/magento2/pull/27261)) + * [#4112](https://github.com/magento/magento2/issues/4112) -- Wrong parent category url_key in URL (fixed in [magento/magento2#26784](https://github.com/magento/magento2/pull/26784)) + * [#11615](https://github.com/magento/magento2/issues/11615) -- URL Rewrites vs multiple storeviews - a never ending battle (fixed in [magento/magento2#26784](https://github.com/magento/magento2/pull/26784)) + * [#11616](https://github.com/magento/magento2/issues/11616) -- URL Rewrites vs multiple storeviews - too many rewrites are being generated (fixed in [magento/magento2#26784](https://github.com/magento/magento2/pull/26784)) + * [#25124](https://github.com/magento/magento2/issues/25124) -- Magento 2.3 Wrong product url for anchor categories for multiple storeviews (fixed in [magento/magento2#26784](https://github.com/magento/magento2/pull/26784)) + * [#26393](https://github.com/magento/magento2/issues/26393) -- Product category url rewrite missing storeview specific url_key (fixed in [magento/magento2#26784](https://github.com/magento/magento2/pull/26784)) + * [#26345](https://github.com/magento/magento2/issues/26345) -- Reorder in Admin panel leads to report page in case of changed product custom option max characters (fixed in [magento/magento2#26348](https://github.com/magento/magento2/pull/26348)) + * [#26117](https://github.com/magento/magento2/issues/26117) -- "Current user does not have an active cart" even when he actually has one (fixed in [magento/magento2#27187](https://github.com/magento/magento2/pull/27187)) + * [#26825](https://github.com/magento/magento2/issues/26825) -- Adding/updating image using API will not create thumbnail for admin products grid (fixed in [magento/magento2#27170](https://github.com/magento/magento2/pull/27170)) + * [#27117](https://github.com/magento/partners-magento2ee/issues/27117) -- MFTF: Test names are not following Best Practices (`Test` suffix) (fixed in [magento/magento2#27118](https://github.com/magento/magento2/pull/27118) and [magento/partners-magento2ee#151](https://github.com/magento/partners-magento2ee/pull/151)) + * [#26683](https://github.com/magento/magento2/issues/26683) -- Unable to execute addSimpleProduct mutation while other items out of stock (fixed in [magento/magento2#27015](https://github.com/magento/magento2/pull/27015)) + * [#26963](https://github.com/magento/magento2/issues/26963) -- Missing JS file (critical-css-loader) in Magento 2.3.4 (fixed in [magento/magento2#26987](https://github.com/magento/magento2/pull/26987)) + * [#26473](https://github.com/magento/magento2/issues/26473) -- BUG: Wrong selected product image when query url param configurable product (fixed in [magento/magento2#26560](https://github.com/magento/magento2/pull/26560)) + * [#26856](https://github.com/magento/magento2/issues/26856) -- Wrong gallery main image and active preview after updating for configurable products. (fixed in [magento/magento2#26560](https://github.com/magento/magento2/pull/26560)) + * [#26858](https://github.com/magento/magento2/issues/26858) -- Wrong gallery behavior when query url param configurable product (fixed in [magento/magento2#26560](https://github.com/magento/magento2/pull/26560)) + * [#22251](https://github.com/magento/magento2/issues/22251) -- Admin Order - Email is Now Required - Magento 2.2.6 (fixed in [magento/magento2#24479](https://github.com/magento/magento2/pull/24479)) + * [#24704](https://github.com/magento/magento2/issues/24704) -- Saving CMS Page Title from REST web API makes content empty (fixed in [magento/magento2#27237](https://github.com/magento/magento2/pull/27237)) + * [#26827](https://github.com/magento/magento2/issues/26827) -- 500 when creating new product after adding attribute via API and assigning to attribute set via UI (fixed in [magento/magento2#27191](https://github.com/magento/magento2/pull/27191)) + * [#27124](https://github.com/magento/magento2/issues/27124) -- Share Wishlist Email: Image Logic Issue (fixed in [magento/magento2#27125](https://github.com/magento/magento2/pull/27125)) + * [#27335](https://github.com/magento/magento2/issues/27335) -- My account Address Book Additional Address Entries table issue (fixed in [magento/magento2#27336](https://github.com/magento/magento2/pull/27336)) + * [#14080](https://github.com/magento/magento2/issues/14080) -- Category path is the same as key producing duplicate URL issue (fixed in [magento/magento2#27304](https://github.com/magento/magento2/pull/27304)) + * [#26708](https://github.com/magento/magento2/issues/26708) -- ORDER BY has two similar conditions (fixed in [magento/magento2#27263](https://github.com/magento/magento2/pull/27263)) + * [#26745](https://github.com/magento/magento2/issues/26745) -- OrderPaymentInterface is missing setAdditionalInformation() (fixed in [magento/magento2#26748](https://github.com/magento/magento2/pull/26748)) + * [#26335](https://github.com/magento/magento2/issues/26335) -- Update zendframework to laminas (fixed in [magento/magento2#26436](https://github.com/magento/magento2/pull/26436)) + * [#186](https://github.com/magento/magento2/issues/186) -- Indexing product (on save) should be done after committing the transaction (fixed in [magento/magento2#26923](https://github.com/magento/magento2/pull/26923)) + * [#26224](https://github.com/magento/magento2/issues/26224) -- Cache type without "instance" causes exception when disabling the module through "Cache Management" in the backend (fixed in [magento/magento2#27307](https://github.com/magento/magento2/pull/27307)) + * [#25540](https://github.com/magento/magento2/issues/25540) -- Products are not displaying infront end after updating product via importing CSV. (fixed in [magento/magento2#25664](https://github.com/magento/magento2/pull/25664)) + * [#22010](https://github.com/magento/magento2/issues/22010) -- 22010 -Updates AbstractExtensibleObject and AbstractExtensibleModel annotations (fixed in [magento/magento2#22011](https://github.com/magento/magento2/pull/22011)) + * [#22363](https://github.com/magento/magento2/issues/22363) -- Layered navigation breaks HTML5 Validation (fixed in [magento/magento2#26055](https://github.com/magento/magento2/pull/26055)) + * [#26884](https://github.com/magento/magento2/issues/26884) -- Customer address is duplicated after setBillingAddressOnCart GraphQL mutation. (fixed in [magento/magento2#27107](https://github.com/magento/magento2/pull/27107)) + * [#26742](https://github.com/magento/magento2/issues/26742) -- graphql mutation setShippingMethodsOnCart get wrong data in available_shipping_methods. (fixed in [magento/magento2#27004](https://github.com/magento/magento2/pull/27004)) + * [#13689](https://github.com/magento/magento2/issues/13689) -- Cannot create catagory's name with thai langauge (fixed in [magento/magento2#27412](https://github.com/magento/magento2/pull/27412)) + * [#27370](https://github.com/magento/magento2/issues/27370) -- Internet explorer issue:Default billing/shipping address not showing (fixed in [magento/magento2#27383](https://github.com/magento/magento2/pull/27383)) + * [#27086](https://github.com/magento/magento2/issues/27086) -- Report Value doesn't matching - "Year-To-Date Starts" (fixed in [magento/magento2#27088](https://github.com/magento/magento2/pull/27088)) + * [#22833](https://github.com/magento/magento2/issues/22833) -- Short-term admin accounts (fixed in [magento/magento2#22837](https://github.com/magento/magento2/pull/22837)) + * [#6310](https://github.com/magento/magento2/issues/6310) -- Changing products 'this item has weight' using 'Update Attributes' is not possible (fixed in [magento/magento2#26075](https://github.com/magento/magento2/pull/26075)) + * [#16315](https://github.com/magento/magento2/issues/16315) -- Product save with onthefly index ignores website assignments (fixed in [magento/magento2#27365](https://github.com/magento/magento2/pull/27365)) + * [#26762](https://github.com/magento/magento2/issues/26762) -- undefined index db-ssl-verify (fixed in [magento/magento2#26763](https://github.com/magento/magento2/pull/26763)) + * [#26652](https://github.com/magento/magento2/issues/26652) -- In the minicart edit and remove icon is not aligned. (fixed in [magento/magento2#27493](https://github.com/magento/magento2/pull/27493)) + * [#1002](https://github.com/magento/magento2/issues/1002) -- Database Schema: Incorrect Unique Indexes (fixed in [magento/magento2#27399](https://github.com/magento/magento2/pull/27399)) + * [#24990](https://github.com/magento/magento2/issues/24990) -- Admin Panel logo link is not directing to admin dashboard page (fixed in [magento/magento2#26100](https://github.com/magento/magento2/pull/26100)) + * [#27500](https://github.com/magento/magento2/issues/27500) -- Unit Tests incompatible with PHPUnit 8 (fixed in [magento/magento2#27521](https://github.com/magento/magento2/pull/27521) and [magento/magento2#27519](https://github.com/magento/magento2/pull/27519) and [magento/magento2#27627](https://github.com/magento/magento2/pull/27627) and [magento/magento2#27522](https://github.com/magento/magento2/pull/27522)) + * [#27496](https://github.com/magento/magento2/issues/27496) -- The store logo is missing when using the Magento_blank theme (fixed in [magento/magento2#27497](https://github.com/magento/magento2/pull/27497)) + * [#27169](https://github.com/magento/magento2/issues/27169) -- Not able to update value with "use default checkbox" for Downloadable Product's Sample and Links Title. (fixed in [magento/magento2#27295](https://github.com/magento/magento2/pull/27295)) + * [#27320](https://github.com/magento/magento2/issues/27320) -- MFTF: Functional Tests failing due to missing data in indexes (fixed in [magento/magento2#27322](https://github.com/magento/magento2/pull/27322) and [magento/magento2#27321](https://github.com/magento/magento2/pull/27321) and [magento/magento2#27323](https://github.com/magento/magento2/pull/27323)) + * [#171](https://github.com/magento/partners-magento2ee/issues/171) -- CMS Page modifications are not being reported in Action Log (fixed in [magento/magento2#27597](https://github.com/magento/magento2/pull/27597) and [magento/partners-magento2ee#172](https://github.com/magento/partners-magento2ee/pull/172)) + * [#13851](https://github.com/magento/magento2/issues/13851) -- Credit doesn't recognize amount after comma (fixed in [magento/magento2#27343](https://github.com/magento/magento2/pull/27343)) + * [#26986](https://github.com/magento/magento2/issues/26986) -- REST API Pagination Does not work as expected (fixed in [magento/magento2#26988](https://github.com/magento/magento2/pull/26988)) + * [#27638](https://github.com/magento/partners-magento2ee/issues/27638) -- PHPUnit Tests bundled with Magento fatal errors (fixed in [magento/magento2#27701](https://github.com/magento/magento2/pull/27701) and [magento/partners-magento2ee#178](https://github.com/magento/partners-magento2ee/pull/178)) + * [#27506](https://github.com/magento/magento2/issues/27506) -- Viewport resizing on search input focus on iphone (fixed in [magento/magento2#27603](https://github.com/magento/magento2/pull/27603)) + * [#27607](https://github.com/magento/magento2/issues/27607) -- Integration Tests: DOM Assertion class (fixed in [magento/magento2#27606](https://github.com/magento/magento2/pull/27606)) + * [#27299](https://github.com/magento/magento2/issues/27299) -- Integration Tests: Consecutive `dispatch($uri)` on Test AbstractController fails (fixed in [magento/magento2#27300](https://github.com/magento/magento2/pull/27300)) + * [#27920](https://github.com/magento/magento2/issues/27920) -- [2.3.5] Incorrect html structure after MC-30989 (fixed in [magento/magento2#27926](https://github.com/magento/magento2/pull/27926)) + * [#25769](https://github.com/magento/magento2/issues/25769) -- Arabic invoice pdf issue Magento 2.3.0 showing as Arabic letters but not correct (fixed in [magento/magento2#27887](https://github.com/magento/magento2/pull/27887)) + * [#27874](https://github.com/magento/magento2/issues/27874) -- Vat Validation URL for EU Vat numbers changed. (Vies Service) (fixed in [magento/magento2#27886](https://github.com/magento/magento2/pull/27886)) + * [#27089](https://github.com/magento/magento2/issues/27089) -- BUG: `getDefaultLimitPerPageValue` returns value that is not available (fixed in [magento/magento2#27093](https://github.com/magento/magento2/pull/27093)) + * [#1270](https://github.com/magento/magento2/issues/1270) -- back button not working in edit order status (fixed in [magento/magento2#27976](https://github.com/magento/magento2/pull/27976)) + * [#27897](https://github.com/magento/magento2/issues/27897) -- MFTF: Inconsistent case in Section name (fixed in [magento/magento2#27955](https://github.com/magento/magento2/pull/27955)) + * [#27503](https://github.com/magento/magento2/issues/27503) -- MFTF: Acceptance tests break the naming convention (fixed in [magento/magento2#27515](https://github.com/magento/magento2/pull/27515)) + * [#15](https://github.com/magento/magento2-login-as-customer/issues/15) -- Remove Login as Customer actions from admin grids (fixed in [magento/magento2-login-as-customer#23](https://github.com/magento/magento2-login-as-customer/pull/23)) + * [#34](https://github.com/magento/magento2-login-as-customer/issues/34) -- Remove option to merge guest cart (fixed in [magento/magento2-login-as-customer#49](https://github.com/magento/magento2-login-as-customer/pull/49)) + * [#110](https://github.com/magento/magento2-login-as-customer/issues/110) -- Need to add spinner/loader while Admin is logging in as Customer (fixed in [magento/magento2-login-as-customer#123](https://github.com/magento/magento2-login-as-customer/pull/123)) + * [#159](https://github.com/magento/magento2-login-as-customer/issues/159) -- Error is shown on the page if customer is not sign out from account (fixed in [magento/magento2-login-as-customer#164](https://github.com/magento/magento2-login-as-customer/pull/164)) + * [#102](https://github.com/magento/magento2-login-as-customer/issues/102) -- Admin user is logged into the default website if customer registered on second website (fixed in [magento/magento2-login-as-customer#148](https://github.com/magento/magento2-login-as-customer/pull/148)) + * [#59](https://github.com/magento/magento2-login-as-customer/issues/59) -- Customer data not invalidated private content after logged in (fixed in [magento/magento2-login-as-customer#68](https://github.com/magento/magento2-login-as-customer/pull/68)) + * [#33](https://github.com/magento/magento2-login-as-customer/issues/33) -- Update Readme.txt (fixed in [magento/magento2-login-as-customer#64](https://github.com/magento/magento2-login-as-customer/pull/64)) + * [#60](https://github.com/magento/magento2-login-as-customer/issues/60) -- Customer data sometimes not being cleared when logging in as customer (fixed in [magento/magento2-login-as-customer#75](https://github.com/magento/magento2-login-as-customer/pull/75)) + * [#73](https://github.com/magento/magento2-login-as-customer/issues/73) -- Page title is empty when admin login as customer (fixed in [magento/magento2-login-as-customer#92](https://github.com/magento/magento2-login-as-customer/pull/92)) + * [#55](https://github.com/magento/magento2-login-as-customer/issues/55) -- [DEV] Need to update/change titles for ACL resource tree related to Login as Customer (fixed in [magento/magento2-login-as-customer#69](https://github.com/magento/magento2-login-as-customer/pull/69)) + * [#8](https://github.com/magento/magento2-login-as-customer/issues/8) -- Merge initial module (fixed in [magento/magento2-login-as-customer#7](https://github.com/magento/magento2-login-as-customer/pull/7)) + * [#122](https://github.com/magento/magento2-login-as-customer/issues/122) -- Issue 96 (fixed in [magento/magento2-login-as-customer#123](https://github.com/magento/magento2-login-as-customer/pull/123)) + * [#71](https://github.com/magento/magento2-login-as-customer/issues/71) -- Login As Customer functionality is available when Login As Customer->Enable Extension=No (fixed in [magento/magento2-login-as-customer#121](https://github.com/magento/magento2-login-as-customer/pull/121)) + * [#16](https://github.com/magento/magento2-login-as-customer/issues/16) -- All System Configuration settings should be on Global level (fixed in [magento/magento2-login-as-customer#120](https://github.com/magento/magento2-login-as-customer/pull/120)) + * [#56](https://github.com/magento/magento2-login-as-customer/issues/56) -- [DEV] Confirmation pop-up window for "Login as Customer" if the setting "Store View To Log In" = "Manual Chooser" (fixed in [magento/magento2-login-as-customer#119](https://github.com/magento/magento2-login-as-customer/pull/119)) + * [#100](https://github.com/magento/magento2-login-as-customer/issues/100) -- Moved all UI from LoginAsCustomer to new LoginAsCustomerUi module (fixed in [magento/magento2-login-as-customer#101](https://github.com/magento/magento2-login-as-customer/pull/101)) + * [#97](https://github.com/magento/magento2-login-as-customer/issues/97) -- Refactor Magento\LoginAsCustomer\Model\Login Model (fixed in [magento/magento2-login-as-customer#99](https://github.com/magento/magento2-login-as-customer/pull/99)) + * [#17](https://github.com/magento/magento2-login-as-customer/issues/17) -- Notification banner on storefront (fixed in [magento/magento2-login-as-customer#87](https://github.com/magento/magento2-login-as-customer/pull/87)) + * [#10](https://github.com/magento/magento2-login-as-customer/issues/10) -- Controllers refactoring (fixed in [magento/magento2-login-as-customer#21](https://github.com/magento/magento2-login-as-customer/pull/21)) + +* GitHub pull requests: + * [magento/magento2#25905](https://github.com/magento/magento2/pull/25905) -- [Checkout] Cover DirectoryData by Unit Test (by @edenduong) + * [magento/magento2#25808](https://github.com/magento/magento2/pull/25808) -- No marginal white space validation added (by @ajithkumar-maragathavel) + * [magento/magento2#25790](https://github.com/magento/magento2/pull/25790) -- Don't disable FPC for maintenance, instead send "no cache" headers (by @Parakoopa) + * [magento/magento2#25774](https://github.com/magento/magento2/pull/25774) -- [Config] Giving the possibility to have a config dependency based on empty config value (by @eduard13) + * [magento/magento2#25604](https://github.com/magento/magento2/pull/25604) -- Moving Ui message.js hide speed and timeout into variables for easier… (by @edward-simpson) + * [magento/magento2#25541](https://github.com/magento/magento2/pull/25541) -- Removes hardcoded references to country selector component (by @krzksz) + * [magento/magento2#25939](https://github.com/magento/magento2/pull/25939) -- [ProductAlert] Cover Helper Data by Unit Test (by @edenduong) + * [magento/magento2#25928](https://github.com/magento/magento2/pull/25928) -- [Variable] Cover Variable Data Model by Unit Test (by @edenduong) + * [magento/magento2#25913](https://github.com/magento/magento2/pull/25913) -- [Backend] Covering the Backend Grid Decoding Helper by UnitTest (by @eduard13) + * [magento/magento2#25822](https://github.com/magento/magento2/pull/25822) -- MFTF: Extract Action Groups to separate files - magento/module-import-export (by @lbajsarowicz) + * [magento/magento2#25812](https://github.com/magento/magento2/pull/25812) -- MFTF: Extract Action Groups to separate files - magento/module-reports (by @lbajsarowicz) + * [magento/magento2#25803](https://github.com/magento/magento2/pull/25803) -- MFTF: Extract Action Groups to separate files - magento/module-shipping (by @lbajsarowicz) + * [magento/magento2#25791](https://github.com/magento/magento2/pull/25791) -- MFTF: Extract Action Groups to separate files - magento/module-widget (by @lbajsarowicz) + * [magento/magento2#25792](https://github.com/magento/magento2/pull/25792) -- MFTF: Extract Action Groups to separate files - magento/module-variable (by @lbajsarowicz) + * [magento/magento2#25765](https://github.com/magento/magento2/pull/25765) -- Magento#25739: fixed issue "grunt clean does not clean generated folder" (by @andrewbess) + * [magento/magento2#25655](https://github.com/magento/magento2/pull/25655) -- Add escaping on meta properties for open graph (by @NathMorgan) + * [magento/magento2#25952](https://github.com/magento/magento2/pull/25952) -- Resolve queue_consumer.xml doesn't allow numbers in handler class issue25731 (by @edenduong) + * [magento/magento2#25942](https://github.com/magento/magento2/pull/25942) -- Resolve Email address mismatch with text in iPad(768) view issue25935 (by @edenduong) + * [magento/magento2#25932](https://github.com/magento/magento2/pull/25932) -- Resolve Refresh Statistics: Updated At = Null should be display as "Never" instead of "undefined". issue25931 (by @edenduong) + * [magento/magento2#25926](https://github.com/magento/magento2/pull/25926) -- Resolve Duplicate Records when sorting column in Content->Themes Grid issue25925 (by @edenduong) + * [magento/magento2#25918](https://github.com/magento/magento2/pull/25918) -- [Ui] Adding admin class for password input type. (by @eduard13) + * [magento/magento2#25912](https://github.com/magento/magento2/pull/25912) -- Category filters - Fix notice on incorrect price param (by @ihor-sviziev) + * [magento/magento2#25995](https://github.com/magento/magento2/pull/25995) -- Updating wee -> weee in Magento_Weee README (by @MellenIO) + * [magento/magento2#25984](https://github.com/magento/magento2/pull/25984) -- [Customer] Cover CustomerData\Customer and CustomerData\JsLayoutDataProviderPool by Unit Test (by @edenduong) + * [magento/magento2#25982](https://github.com/magento/magento2/pull/25982) -- [Catalog] Cover Price Validation Result class by Unit Test (by @edenduong) + * [magento/magento2#25954](https://github.com/magento/magento2/pull/25954) -- Refactor: Add method hints to Tracking Status (by @lbajsarowicz) + * [magento/magento2#25924](https://github.com/magento/magento2/pull/25924) -- Resolve A "500 (Internal Server Error)" appears in Developer Console if Delete the image that is added to Page Content issue25893 (by @edenduong) + * [magento/magento2#25904](https://github.com/magento/magento2/pull/25904) -- Resolve issue 25896: Cannot create folder using only letters (by @edenduong) + * [magento/magento2#25723](https://github.com/magento/magento2/pull/25723) -- Fix #24713 - Symbol of the Belarusian currency BYR is outdated (by @Bartlomiejsz) + * [magento/magento2#25699](https://github.com/magento/magento2/pull/25699) -- magento/magento2#23481: Billing/Shipping Address edit form design update from order backend (by @alexey-rakitin) + * [magento/magento2#25262](https://github.com/magento/magento2/pull/25262) -- Allow autoplay for vimeo thumb click (by @philkun) + * [magento/magento2#26016](https://github.com/magento/magento2/pull/26016) -- [DownloadableImportExport] Cover Helper Data by Unit Test (by @edenduong) + * [magento/magento2#25997](https://github.com/magento/magento2/pull/25997) -- [Newsletter] Refactor code and Cover Model/Observer class by Unit Test (by @edenduong) + * [magento/magento2#25993](https://github.com/magento/magento2/pull/25993) -- [InstantPurchase] Cover Ui/CustomerAddressesFormatter and Ui/ShippingMethodFormatter by Unit Test (by @edenduong) + * [magento/magento2#25992](https://github.com/magento/magento2/pull/25992) -- Cover magento/magento2#25556 with jasmine test (by @Nazar65) + * [magento/magento2#25973](https://github.com/magento/magento2/pull/25973) -- [Removed spacing in submenu on hover desktop] (by @hitesh-wagento) + * [magento/magento2#25975](https://github.com/magento/magento2/pull/25975) -- phpdoc fix return type (by @maslii) + * [magento/magento2#25624](https://github.com/magento/magento2/pull/25624) -- Add right arrow to show some items have children (by @fredden) + * [magento/magento2#25114](https://github.com/magento/magento2/pull/25114) -- Added translate for strings and added missing node in existing translate attribute on xml. (by @sanganinamrata) + * [magento/magento2#25587](https://github.com/magento/magento2/pull/25587) -- Refactor JavaScript mixins module (by @krzksz) + * [magento/magento2#26069](https://github.com/magento/magento2/pull/26069) -- [CMS] Improving the test coverage for UrlBuilder ViewModel (by @eduard13) + * [magento/magento2#26067](https://github.com/magento/magento2/pull/26067) -- [Msrp] Cover MsrpPriceCalculator by Unit Test (by @edenduong) + * [magento/magento2#26063](https://github.com/magento/magento2/pull/26063) -- [Theme] Reverting removed container class (by @eduard13) + * [magento/magento2#26057](https://github.com/magento/magento2/pull/26057) -- [Contact] covered Model Config by Unit Test (by @srsathish92) + * [magento/magento2#26050](https://github.com/magento/magento2/pull/26050) -- [Catalog] covered product ViewModel AddToCompareAvailability by Unit Test (by @srsathish92) + * [magento/magento2#26044](https://github.com/magento/magento2/pull/26044) -- Set empty value to color picker when input is reset to update preview (by @gperis) + * [magento/magento2#26045](https://github.com/magento/magento2/pull/26045) -- [Downloadable] Cover Helper Data by Unit Test (by @edenduong) + * [magento/magento2#26042](https://github.com/magento/magento2/pull/26042) -- [Catalog] Cover Component/FilterFactory by Unit Test (by @edenduong) + * [magento/magento2#26043](https://github.com/magento/magento2/pull/26043) -- [Persistent] Cover CustomerData by Unit Test (by @edenduong) + * [magento/magento2#26037](https://github.com/magento/magento2/pull/26037) -- Fixes phpcs errors and warnings for Magento\Framework\View\Element (by @krisdante) + * [magento/magento2#26034](https://github.com/magento/magento2/pull/26034) -- MAGETWO-95866 Add horizontal scroll if elements extend menu's width (by @ptylek) + * [magento/magento2#26003](https://github.com/magento/magento2/pull/26003) -- [Directory] Cover action directory/json/countryRegion by Integration Test (by @edenduong) + * [magento/magento2#26001](https://github.com/magento/magento2/pull/26001) -- Fix caching Magento Metadata getVersion (by @luklewluk) + * [magento/magento2#25940](https://github.com/magento/magento2/pull/25940) -- Asynchronous operation validate (by @sedonik) + * [magento/magento2#25697](https://github.com/magento/magento2/pull/25697) -- [New Relic] Making system configs dependent by Enabled field (by @eduard13) + * [magento/magento2#25523](https://github.com/magento/magento2/pull/25523) -- Contact form > Adding ViewModel (by @rafaelstz) + * [magento/magento2#24360](https://github.com/magento/magento2/pull/24360) -- #24357 Eav sort order by attribute option_id (by @tnsezer) + * [magento/magento2#26060](https://github.com/magento/magento2/pull/26060) -- [Backend] Cover Dashboard Helper Data by Unit Test (by @edenduong) + * [magento/magento2#26059](https://github.com/magento/magento2/pull/26059) -- [Downloadable] Cover the Observer SetHasDownloadableProductsObserver by Unit Test (by @edenduong) + * [magento/magento2#26058](https://github.com/magento/magento2/pull/26058) -- Fixed typo: "reviwGrid" to "reviewGrid" (by @matheusgontijo) + * [magento/magento2#26011](https://github.com/magento/magento2/pull/26011) -- Fixed the issue 25930 (by @divyajyothi5321) + * [magento/magento2#26004](https://github.com/magento/magento2/pull/26004) -- [Backend] Cover action admin/dashboard/ajaxBlock by Integration Test (by @edenduong) + * [magento/magento2#25920](https://github.com/magento/magento2/pull/25920) -- Code refactor in Catalog ViewModel Breadcrumbs (by @srsathish92) + * [magento/magento2#26082](https://github.com/magento/magento2/pull/26082) -- [GiftMessage] Cover Observer SalesEventOrderItemToQuoteItemObserver by Unit Test (by @edenduong) + * [magento/magento2#26076](https://github.com/magento/magento2/pull/26076) -- [Search] Cover SynonymActions Column by Unit Test (by @edenduong) + * [magento/magento2#26068](https://github.com/magento/magento2/pull/26068) -- [GoogleAnalytics] covered Helper Data by Unit Test (by @srsathish92) + * [magento/magento2#26009](https://github.com/magento/magento2/pull/26009) -- Refactor: Add information about the path that is not allowed (by @lbajsarowicz) + * [magento/magento2#25759](https://github.com/magento/magento2/pull/25759) -- fixed issue 25433 (by @Ashna-Jahan) + * [magento/magento2#25854](https://github.com/magento/magento2/pull/25854) -- MFTF: Extract Action Groups to separate files - magento/module-catalog (by @lbajsarowicz) + * [magento/magento2#25846](https://github.com/magento/magento2/pull/25846) -- MFTF: Extract Action Groups to separate files - magento/module-catalog-import-export (by @lbajsarowicz) + * [magento/magento2#25845](https://github.com/magento/magento2/pull/25845) -- MFTF: Extract Action Groups to separate files - magento/module-catalog-inventory (by @lbajsarowicz) + * [magento/magento2#25844](https://github.com/magento/magento2/pull/25844) -- MFTF: Extract Action Groups to separate files - magento/module-catalog-rule (by @lbajsarowicz) + * [magento/magento2#25842](https://github.com/magento/magento2/pull/25842) -- MFTF: Extract Action Groups to separate files - magento/module-catalog-search (by @lbajsarowicz) + * [magento/magento2#25841](https://github.com/magento/magento2/pull/25841) -- MFTF: Extract Action Groups to separate files - magento/module-checkout (by @lbajsarowicz) + * [magento/magento2#25831](https://github.com/magento/magento2/pull/25831) -- MFTF: Extract Action Groups to separate files - magento/module-config (by @lbajsarowicz) + * [magento/magento2#25836](https://github.com/magento/magento2/pull/25836) -- MFTF: Extract Action Groups to separate files - magento/module-cms (by @lbajsarowicz) + * [magento/magento2#25830](https://github.com/magento/magento2/pull/25830) -- MFTF: Extract Action Groups to separate files - magento/module-configurable-product (by @lbajsarowicz) + * [magento/magento2#25829](https://github.com/magento/magento2/pull/25829) -- MFTF: Extract Action Groups to separate files - magento/module-currency-symbol (by @lbajsarowicz) + * [magento/magento2#25825](https://github.com/magento/magento2/pull/25825) -- MFTF: Extract Action Groups to separate files - magento/module-downloadable (by @lbajsarowicz) + * [magento/magento2#25823](https://github.com/magento/magento2/pull/25823) -- MFTF: Extract Action Groups to separate files - magento/module-email (by @lbajsarowicz) + * [magento/magento2#25821](https://github.com/magento/magento2/pull/25821) -- MFTF: Extract Action Groups to separate files - magento/module-grouped-product (by @lbajsarowicz) + * [magento/magento2#25819](https://github.com/magento/magento2/pull/25819) -- MFTF: Extract Action Groups to separate files - magento/module-multishipping (by @lbajsarowicz) + * [magento/magento2#25820](https://github.com/magento/magento2/pull/25820) -- MFTF: Extract Action Groups to separate files - magento/module-indexer (by @lbajsarowicz) + * [magento/magento2#25818](https://github.com/magento/magento2/pull/25818) -- MFTF: Extract Action Groups to separate files - magento/module-newsletter (by @lbajsarowicz) + * [magento/magento2#25817](https://github.com/magento/magento2/pull/25817) -- MFTF: Replace redundant Action Group with proper one - magento/module-page-cache (by @lbajsarowicz) + * [magento/magento2#25816](https://github.com/magento/magento2/pull/25816) -- MFTF: Extract Action Groups to separate files - magento/module-paypal (by @lbajsarowicz) + * [magento/magento2#25815](https://github.com/magento/magento2/pull/25815) -- MFTF: Extract Action Groups to separate files - magento/module-persistent (by @lbajsarowicz) + * [magento/magento2#25813](https://github.com/magento/magento2/pull/25813) -- MFTF: Extract Action Groups to separate files - magento/module-product-video (by @lbajsarowicz) + * [magento/magento2#25811](https://github.com/magento/magento2/pull/25811) -- MFTF: Extract Action Groups to separate files - magento/module-sales (by @lbajsarowicz) + * [magento/magento2#25807](https://github.com/magento/magento2/pull/25807) -- MFTF: Extract Action Groups to separate files - magento/module-sales-rule (by @lbajsarowicz) + * [magento/magento2#25804](https://github.com/magento/magento2/pull/25804) -- MFTF: Extract Action Groups to separate files - magento/module-search (by @lbajsarowicz) + * [magento/magento2#25802](https://github.com/magento/magento2/pull/25802) -- MFTF: Extract Action Groups to separate files - magento/module-store (by @lbajsarowicz) + * [magento/magento2#25800](https://github.com/magento/magento2/pull/25800) -- MFTF: Extract Action Groups to separate files - magento/module-swatches (by @lbajsarowicz) + * [magento/magento2#25799](https://github.com/magento/magento2/pull/25799) -- MFTF: Extract Action Groups to separate files - magento/module-tax (by @lbajsarowicz) + * [magento/magento2#25797](https://github.com/magento/magento2/pull/25797) -- MFTF: Extract Action Groups to separate files - magento/module-ui (by @lbajsarowicz) + * [magento/magento2#25794](https://github.com/magento/magento2/pull/25794) -- MFTF: Extract Action Groups to separate files - magento/module-url-rewrite (by @lbajsarowicz) + * [magento/magento2#25793](https://github.com/magento/magento2/pull/25793) -- MFTF: Extract Action Groups to separate files - magento/module-user (by @lbajsarowicz) + * [magento/magento2#25788](https://github.com/magento/magento2/pull/25788) -- MFTF: Extract Action Groups to separate files - magento/module-wishlist (by @lbajsarowicz) + * [magento/magento2#25787](https://github.com/magento/magento2/pull/25787) -- MFTF: Extract Action Groups to separate files - magento/module-bundle (by @lbajsarowicz) + * [magento/magento2#25784](https://github.com/magento/magento2/pull/25784) -- MFTF: Extract Action Groups to separate files - magento/module-braintree (by @lbajsarowicz) + * [magento/magento2#25783](https://github.com/magento/magento2/pull/25783) -- MFTF: Extract Action Groups to separate files - magento/module-backup (by @lbajsarowicz) + * [magento/magento2#26157](https://github.com/magento/magento2/pull/26157) -- Remove blank space at the end of label (by @gihovani) + * [magento/magento2#26160](https://github.com/magento/magento2/pull/26160) -- Changing the data type for quote column customer_note (by @ravi-chandra3197) + * [magento/magento2#26154](https://github.com/magento/magento2/pull/26154) -- [LayeredNavigation] Covering the ProductAttributeGridBuildObserver for LayeredNavigation … (by @eduard13) + * [magento/magento2#26150](https://github.com/magento/magento2/pull/26150) -- [CatalogInventory] Covering the InvalidatePriceIndexUponConfigChangeObserver for Catalog… (by @eduard13) + * [magento/magento2#26148](https://github.com/magento/magento2/pull/26148) -- [Bundle] Covering the SetAttributeTabBlockObserver for Bundles by Unit Test (by @eduard13) + * [magento/magento2#26140](https://github.com/magento/magento2/pull/26140) -- [ImportExport] Cover Export Source Model by Unit Test (by @edenduong) + * [magento/magento2#26136](https://github.com/magento/magento2/pull/26136) -- Code refactor, updated Unit Test with JsonHexTag Serializer (by @srsathish92) + * [magento/magento2#26128](https://github.com/magento/magento2/pull/26128) -- Refactor Magento Version module (+ Unit Tests) (by @lbajsarowicz) + * [magento/magento2#26127](https://github.com/magento/magento2/pull/26127) -- [Weee] Cover Weee Plugin by Unit Test (by @edenduong) + * [magento/magento2#26096](https://github.com/magento/magento2/pull/26096) -- [Checkout] Covering the ResetQuoteAddresses by Unit Test (by @eduard13) + * [magento/magento2#26028](https://github.com/magento/magento2/pull/26028) -- Refactor: Remove deprecated methods (by @andrewbess) + * [magento/magento2#25864](https://github.com/magento/magento2/pull/25864) -- Adobe stock integration Issue-761: Highlight the selected image in the grid (by @serhiyzhovnir) + * [magento/magento2#24849](https://github.com/magento/magento2/pull/24849) -- Simplify some conditional checks (by @DanielRuf) + * [magento/magento2#26131](https://github.com/magento/magento2/pull/26131) -- Reduce sleep time for Unit Test of Consumer to 0 seconds (by @lbajsarowicz) + * [magento/magento2#26126](https://github.com/magento/magento2/pull/26126) -- [WeeeGraphQl] Covering the FixedProductTax (by @lbajsarowicz) + * [magento/magento2#26129](https://github.com/magento/magento2/pull/26129) -- Refactor / Cleanup: `use` section does not need leading backslash (by @lbajsarowicz) + * [magento/magento2#26125](https://github.com/magento/magento2/pull/26125) -- [WishlistGraphQL] Covering the CustomerWishlistResolver (by @lbajsarowicz) + * [magento/magento2#26033](https://github.com/magento/magento2/pull/26033) -- Normalize new line symbols in Product Text Option (type=area) (by @Leone) + * [magento/magento2#25915](https://github.com/magento/magento2/pull/25915) -- Tests for: magento/magento2#24907, magento/magento2#25051, magento/magento2#25149, magento/magento2#24973, magento/magento2#25666. (by @p-bystritsky) + * [magento/magento2#25838](https://github.com/magento/magento2/pull/25838) -- [FIXES] #25674: Elasticsearch version selections in admin are overly … (by @mautz-et-tong) + * [magento/magento2#25315](https://github.com/magento/magento2/pull/25315) -- Error in vendor/magento/module-shipping/Model/Config/Source/Allmethod… (by @mrodespin) + * [magento/magento2#25957](https://github.com/magento/magento2/pull/25957) -- Add Cron Jobs names to New Relic transactions (by @lbajsarowicz) + * [magento/magento2#24103](https://github.com/magento/magento2/pull/24103) -- Refactor AdminNotification Render Blocks (by @DavidLambauer) + * [magento/magento2#26173](https://github.com/magento/magento2/pull/26173) -- Added Fix for 26164 (by @divyajyothi5321) + * [magento/magento2#26170](https://github.com/magento/magento2/pull/26170) -- Fixed Special Price class not added in configurable product page (by @ravi-chandra3197) + * [magento/magento2#25876](https://github.com/magento/magento2/pull/25876) -- Advance the order state to processing when a capture notification is received (by @azambon) + * [magento/magento2#25428](https://github.com/magento/magento2/pull/25428) -- Fixed model save and ObjectManager usage (by @drpayyne) + * [magento/magento2#25125](https://github.com/magento/magento2/pull/25125) -- Performance optimizations (by @andrey-legayev) + * [magento/magento2#26225](https://github.com/magento/magento2/pull/26225) -- Hotfix for Invalid date format in Functional and hotfix for failing Integration Tests (by @lbajsarowicz) + * [magento/magento2#25603](https://github.com/magento/magento2/pull/25603) -- Fix removing query string from url after redirect (by @arendarenko) + * [magento/magento2#26182](https://github.com/magento/magento2/pull/26182) -- Fix for footer newsletter input field length in IE/Edge (by @divyajyothi5321) + * [magento/magento2#26130](https://github.com/magento/magento2/pull/26130) -- Fix #25390 - UPS carrier model getting error when creating plugin in to Magento 2.3.3 compatibility (by @Bartlomiejsz) + * [magento/magento2#26084](https://github.com/magento/magento2/pull/26084) -- Fix #26083 - problem with unsAdditionalInformation in \Magento\Payment\Model\Info (by @marcoaacoliveira) + * [magento/magento2#26066](https://github.com/magento/magento2/pull/26066) -- 26064 issuefix (by @divyajyothi5321) + * [magento/magento2#25958](https://github.com/magento/magento2/pull/25958) -- #14663 Updating Customer through rest/all/V1/customers/:id resets group_id if group_id not passed in payload (by @MaxRomanov4669) + * [magento/magento2#25479](https://github.com/magento/magento2/pull/25479) -- JSON fields support (by @akaplya) + * [magento/magento2#25640](https://github.com/magento/magento2/pull/25640) -- set correct pram like in BlockRepository implementation (by @torhoehn) + * [magento/magento2#25478](https://github.com/magento/magento2/pull/25478) -- Clearer PHPDocs comment for AbstractBlock and Escaper (by @edward-simpson) + * [magento/magento2#25452](https://github.com/magento/magento2/pull/25452) -- Elastic Search 5 Indexing Performance Issue with product mapper (by @behnamshayani) + * [magento/magento2#24815](https://github.com/magento/magento2/pull/24815) -- Fix #21684 - Currency sign for "Layered Navigation Price Step" is not according to default settings (by @Bartlomiejsz) + * [magento/magento2#24471](https://github.com/magento/magento2/pull/24471) -- Resolve Export Coupon Code Grid redirect to DashBoard when create New Cart Price Rule issue24468 (by @edenduong) + * [magento/magento2#22917](https://github.com/magento/magento2/pull/22917) -- magento/magento2#22856: Catalog price rules are not working with custom options as expected. (by @p-bystritsky) + * [magento/magento2#26274](https://github.com/magento/magento2/pull/26274) -- MFTF: Replace incorrect URLs in Tests and ActionGroups (by @lbajsarowicz) + * [magento/magento2#26273](https://github.com/magento/magento2/pull/26273) -- MFTF: Replace with for Admin log out (by @lbajsarowicz) + * [magento/magento2#26268](https://github.com/magento/magento2/pull/26268) -- Fix #14001 - M2.2.3 directory_country_region_name locale fix? 8bytes zh_Hans_CN(11bytes) ca_ES_VALENCIA(14bytes) (by @Bartlomiejsz) + * [magento/magento2#26264](https://github.com/magento/magento2/pull/26264) -- Issue 23521 (by @aleromano89) + * [magento/magento2#26259](https://github.com/magento/magento2/pull/26259) -- Fix invalid XML Schema location (by @lbajsarowicz) + * [magento/magento2#26237](https://github.com/magento/magento2/pull/26237) -- Added Fix for 25936 (by @divyajyothi5321) + * [magento/magento2#26234](https://github.com/magento/magento2/pull/26234) -- [Align some space between input and update button Minicart] (by @hitesh-wagento) + * [magento/magento2#26215](https://github.com/magento/magento2/pull/26215) -- Disabled the sorting option in status column on cache grid (by @srsathish92) + * [magento/magento2#26207](https://github.com/magento/magento2/pull/26207) -- #26206 Add information about currently reindexed index. (by @lbajsarowicz) + * [magento/magento2#26183](https://github.com/magento/magento2/pull/26183) -- Added Fix for - 26181 (by @divyajyothi5321) + * [magento/magento2#26169](https://github.com/magento/magento2/pull/26169) -- Added Fix for issue 26168 (by @divyajyothi5321) + * [magento/magento2#26029](https://github.com/magento/magento2/pull/26029) -- Fixed keyboard arrow keys behavior for number fields in AdobeStock grid (by @rogyar) + * [magento/magento2#25946](https://github.com/magento/magento2/pull/25946) -- Add plugin for SalesOrderItemRepository gift message (#19093) (by @lfolco) + * [magento/magento2#25250](https://github.com/magento/magento2/pull/25250) -- Implement catching for all Errors - ref Magento issue #23350 (by @miszyman) + * [magento/magento2#26290](https://github.com/magento/magento2/pull/26290) -- [Fixed Jump Datepicker issue in Catalog Price Rule] (by @hitesh-wagento) + * [magento/magento2#26270](https://github.com/magento/magento2/pull/26270) -- Fix #22964 (by @marcoaacoliveira) + * [magento/magento2#26263](https://github.com/magento/magento2/pull/26263) -- Fix #14913 - bookmark views become uneditable after deleting the first bookmark view (by @Bartlomiejsz) + * [magento/magento2#26251](https://github.com/magento/magento2/pull/26251) -- [Customer] Removing the delete buttons for default customer groups (by @eduard13) + * [magento/magento2#26218](https://github.com/magento/magento2/pull/26218) -- FIX issue#26217 - Wrong fields selection while using fragments on GraphQL (by @phoenix128) + * [magento/magento2#26048](https://github.com/magento/magento2/pull/26048) -- Fixed spelling and adjusted white spaces (by @pawankparmar) + * [magento/magento2#25985](https://github.com/magento/magento2/pull/25985) -- Fixed ability to save configuration in field without label (by @AndreyChorniy) + * [magento/magento2#25337](https://github.com/magento/magento2/pull/25337) -- #14971 Improper Handling of Pagination SEO (by @chickenland) + * [magento/magento2#22990](https://github.com/magento/magento2/pull/22990) -- [Catalog|Sales] Fix wrong behavior of grid row click event (by @Den4ik) + * [magento/magento2#26360](https://github.com/magento/magento2/pull/26360) -- System xml cleanup (by @Bartlomiejsz) + * [magento/magento2#26359](https://github.com/magento/magento2/pull/26359) -- Remove Filename Normalization in Delete Controller (by @pmclain) + * [magento/magento2#26354](https://github.com/magento/magento2/pull/26354) -- Make WYSIWYG configuration options depend on wysiwyg being enabled (by @Bartlomiejsz) + * [magento/magento2#26312](https://github.com/magento/magento2/pull/26312) -- magento/magento2#: Unit test for \Magento\Review\Observer\CatalogProductListCollectionAppendSummaryFieldsObserver (by @atwixfirster) + * [magento/magento2#26311](https://github.com/magento/magento2/pull/26311) -- [CurrencySymbol] Fixing the redirect after saving the currency symbols (by @eduard13) + * [magento/magento2#26305](https://github.com/magento/magento2/pull/26305) -- [GoogleAdWords] Conversion ID client validation (by @eduard13) + * [magento/magento2#26269](https://github.com/magento/magento2/pull/26269) -- Fix #7065 - page.main.title is translating title (by @Bartlomiejsz) + * [magento/magento2#26258](https://github.com/magento/magento2/pull/26258) -- #11209 Wishlist Add grouped product Error (by @MaxRomanov4669) + * [magento/magento2#26238](https://github.com/magento/magento2/pull/26238) -- [Correct both Menu spacing issue] (by @hitesh-wagento) + * [magento/magento2#26185](https://github.com/magento/magento2/pull/26185) -- Allow wishlist share when all items are out of stock (by @pmclain) + * [magento/magento2#26051](https://github.com/magento/magento2/pull/26051) -- Issue with reorder when disabled reorder setting from admin issue25130 (by @edenduong) + * [magento/magento2#25909](https://github.com/magento/magento2/pull/25909) -- Resolve Admin panel is not accessible after limited permissions set to at least one admin account issue25881 (by @edenduong) + * [magento/magento2#25718](https://github.com/magento/magento2/pull/25718) -- add the possibility to add display mode dependant layout handles (by @brosenberger) + * [magento/magento2#25716](https://github.com/magento/magento2/pull/25716) -- add check if attribute value is possible to be set as configurable option (by @brosenberger) + * [magento/magento2#25375](https://github.com/magento/magento2/pull/25375) -- Fix minicart promotion region not rendering #25373 (by @mattijv) + * [magento/magento2#25333](https://github.com/magento/magento2/pull/25333) -- Fixed issue magento#25278:Incorrect @return type at getSourceModel in… (by @mkalakailo) + * [magento/magento2#25194](https://github.com/magento/magento2/pull/25194) -- Limit the php explode to 2 to prevent extra '=' sign content in the a… (by @dhoang89) + * [magento/magento2#25083](https://github.com/magento/magento2/pull/25083) -- Cleanup search api di (by @thomas-kl1) + * [magento/magento2#24955](https://github.com/magento/magento2/pull/24955) -- Fix: add to cart grouped product when exists a sold out option (by @gihovani) + * [magento/magento2#23313](https://github.com/magento/magento2/pull/23313) -- Trigger page load listeners when no longer loading (by @johnhughes1984) + * [magento/magento2#26407](https://github.com/magento/magento2/pull/26407) -- MFTF: Set of fixes for failing Functional Tests (by @lbajsarowicz) + * [magento/magento2#26395](https://github.com/magento/magento2/pull/26395) -- HotFix: Failing Magento EE check on Layered Navigation (by @lbajsarowicz) + * [magento/magento2#26323](https://github.com/magento/magento2/pull/26323) -- MFTF: Extract Action Groups to separate files - magento/module-ui (by @lbajsarowicz) + * [magento/magento2#26321](https://github.com/magento/magento2/pull/26321) -- MFTF: Extract Action Groups to separate files - magento/module-shipping (by @lbajsarowicz) + * [magento/magento2#26320](https://github.com/magento/magento2/pull/26320) -- MFTF: Extract Action Groups to separate files - magento/module-sales (by @lbajsarowicz) + * [magento/magento2#26319](https://github.com/magento/magento2/pull/26319) -- MFTF: Extract Action Groups to separate files - magento/module-catalog (by @lbajsarowicz) + * [magento/magento2#26424](https://github.com/magento/magento2/pull/26424) -- Add to Compare link does not show in mobile view under 640px in blank theme (by @ptylek) + * [magento/magento2#26402](https://github.com/magento/magento2/pull/26402) -- magento/magento2#: Unit test for \Magento\AdminNotification\Observer\PredispatchAdminActionControllerObserver (by @atwixfirster) + * [magento/magento2#26365](https://github.com/magento/magento2/pull/26365) -- Add to Compare link not showing in mobile view under 640px (by @tejash-wagento) + * [magento/magento2#26313](https://github.com/magento/magento2/pull/26313) -- Issue-25968 - Added additional checking for returning needed variable… (by @AndreyChorniy) + * [magento/magento2#26495](https://github.com/magento/magento2/pull/26495) -- Fix confusing phpdoc in curl client (by @tdgroot) + * [magento/magento2#26464](https://github.com/magento/magento2/pull/26464) -- magento/magento2#: GraphQl. RevokeCustomerToken. Test coverage. (by @atwixfirster) + * [magento/magento2#26452](https://github.com/magento/magento2/pull/26452) -- magento/magento2#: GraphQl. DeletePaymentToken. Remove redundant validation logic. Test coverage. (by @atwixfirster) + * [magento/magento2#26322](https://github.com/magento/magento2/pull/26322) -- MFTF: Extract Action Groups to separate files for dev/tests (by @lbajsarowicz) + * [magento/magento2#26391](https://github.com/magento/magento2/pull/26391) -- MFTF: Add missing tests annotations (by @lbajsarowicz) + * [magento/magento2#26628](https://github.com/magento/magento2/pull/26628) -- Fixed #26513 (by @vikalps4) + * [magento/magento2#26614](https://github.com/magento/magento2/pull/26614) -- #26612 Fix failure on Coupon Apply procedure when loading mask still visible (by @lbajsarowicz) + * [magento/magento2#26558](https://github.com/magento/magento2/pull/26558) -- [Csp] Covering the model classes by Unit Tests (by @eduard13) + * [magento/magento2#26540](https://github.com/magento/magento2/pull/26540) -- Added action group for cms block duplication test (by @ajithkumar-maragathavel) + * [magento/magento2#26537](https://github.com/magento/magento2/pull/26537) -- Covered admin cms block creatation with MFTF test (by @ajithkumar-maragathavel) + * [magento/magento2#26512](https://github.com/magento/magento2/pull/26512) -- Extend exception message (by @oroskodias) + * [magento/magento2#26511](https://github.com/magento/magento2/pull/26511) -- Extend exception message (by @oroskodias) + * [magento/magento2#26509](https://github.com/magento/magento2/pull/26509) -- Update PHP Docs (by @oroskodias) + * [magento/magento2#26490](https://github.com/magento/magento2/pull/26490) -- Fixed type issue. Create unit test for customer data model (by @AndreyChorniy) + * [magento/magento2#26489](https://github.com/magento/magento2/pull/26489) -- Checked if quote object contains id before looking for quote items (by @rav-redchamps) + * [magento/magento2#26480](https://github.com/magento/magento2/pull/26480) -- Bugfix #26479 Exception when Autoloader was not registered properly (by @lbajsarowicz) + * [magento/magento2#26478](https://github.com/magento/magento2/pull/26478) -- Unit Test for Magento\Fedex\Plugin\Block\DataProviders\Tracking\ChangeTitle (by @karyna-tsymbal-atwix) + * [magento/magento2#26455](https://github.com/magento/magento2/pull/26455) -- 25162 fixed wrong format link (by @Usik2203) + * [magento/magento2#26445](https://github.com/magento/magento2/pull/26445) -- Fix #25761: Site map doesn't include home page (by @deepaksnair) + * [magento/magento2#26435](https://github.com/magento/magento2/pull/26435) -- #18012: added i18n wrapper to be used in underscore templates for translation (by @sergiy-v) + * [magento/magento2#26434](https://github.com/magento/magento2/pull/26434) -- Fix typo in sitemap product collection docblock (by @Tjitse-E) + * [magento/magento2#26381](https://github.com/magento/magento2/pull/26381) -- #25300 Mobile view issue on category page - Sort By label overlaps (by @akartavtsev) + * [magento/magento2#26327](https://github.com/magento/magento2/pull/26327) -- Fix the wrong behavior of validation scroll on the iPhone X (by @iGerchak) + * [magento/magento2#26285](https://github.com/magento/magento2/pull/26285) -- Remove extraneous whitespace - #26275 (by @DanielRuf) + * [magento/magento2#26071](https://github.com/magento/magento2/pull/26071) -- #26065 isSaleable cache and optimize result for configurable products (by @ilnytskyi) + * [magento/magento2#25994](https://github.com/magento/magento2/pull/25994) -- Extend exception messages (by @oroskodias) + * [magento/magento2#25839](https://github.com/magento/magento2/pull/25839) -- Fix gallery thumbs navigation scrolling (by @iGerchak) + * [magento/magento2#25385](https://github.com/magento/magento2/pull/25385) -- Prevent page scroll jumping when product gallery loads (by @krzksz) + * [magento/magento2#26355](https://github.com/magento/magento2/pull/26355) -- Performance: Getting rid of `array_merge` in loop (by @lbajsarowicz) + * [magento/magento2#26296](https://github.com/magento/magento2/pull/26296) -- Add Visual Code catalog generator (by @manuelcanepa) + * [magento/magento2#26000](https://github.com/magento/magento2/pull/26000) -- magento/magento2#: Remove unused “Default Email Domain” option and related to it code (by @atwixfirster) + * [magento/magento2#25966](https://github.com/magento/magento2/pull/25966) -- [Fixed Radio alignment issue] (by @hitesh-wagento) + * [magento/magento2#25875](https://github.com/magento/magento2/pull/25875) -- Prevent endless loop when duplicating product (by @JeroenVanLeusden) + * [magento/magento2#25764](https://github.com/magento/magento2/pull/25764) -- Cleanup, refactor and cover with tests section-config module (by @krzksz) + * [magento/magento2#24460](https://github.com/magento/magento2/pull/24460) -- Allow construction of products with custom_attributes in $data (by @Vinai) + * [magento/magento2#26634](https://github.com/magento/magento2/pull/26634) -- Xml fixes for Magento_AdvancedPricingImportExport module (by @sanganinamrata) + * [magento/magento2#26611](https://github.com/magento/magento2/pull/26611) -- #26610 Fix failing CI due to invalid variable handler (by @lbajsarowicz) + * [magento/magento2#26549](https://github.com/magento/magento2/pull/26549) -- [Fedex] covered Model/Source/Generic.php by unit test (by @srsathish92) + * [magento/magento2#26525](https://github.com/magento/magento2/pull/26525) -- Unit test for Magento\Reports\Observer\EventSaver (by @karyna-tsymbal-atwix) + * [magento/magento2#26487](https://github.com/magento/magento2/pull/26487) -- Unit test for Magento\Fedex\Plugin\Block\Tracking\PopupDeliveryDate (by @karyna-tsymbal-atwix) + * [magento/magento2#26439](https://github.com/magento/magento2/pull/26439) -- magento/magento2#: Unit test for \Magento\Bundle\Observer\InitOptionRendererObserver (by @atwixfirster) + * [magento/magento2#26429](https://github.com/magento/magento2/pull/26429) -- magento/magento2#: Unit test for \Magento\Bundle\Observer\AppendUpsellProductsObserver (by @atwixfirster) + * [magento/magento2#26241](https://github.com/magento/magento2/pull/26241) -- #26240: Fixed logic for getting option price index for selected swatch option (by @sergiy-v) + * [magento/magento2#26641](https://github.com/magento/magento2/pull/26641) -- Correct doc url added to README (by @rishatiwari) + * [magento/magento2#26579](https://github.com/magento/magento2/pull/26579) -- Unit test for Magento\Reports\Observer\CheckoutCartAddProductObserver (by @karyna-tsymbal-atwix) + * [magento/magento2#26574](https://github.com/magento/magento2/pull/26574) -- Cover Search Term Entity Redirect Works on Store Front by MFTF Test (by @DmitryTsymbal) + * [magento/magento2#26569](https://github.com/magento/magento2/pull/26569) -- 17847 Fixed wrong state title (by @Usik2203) + * [magento/magento2#26568](https://github.com/magento/magento2/pull/26568) -- Action group added for existing test (by @ajithkumar-maragathavel) + * [magento/magento2#26542](https://github.com/magento/magento2/pull/26542) -- Typo Mistake (by @mayankzalavadia) + * [magento/magento2#26533](https://github.com/magento/magento2/pull/26533) -- Github #26532: di:setup:compile fails with anonymous classes (by @joni-jones) + * [magento/magento2#26496](https://github.com/magento/magento2/pull/26496) -- [CurrencySymbol] Fixing the disabled currency inputs (by @eduard13) + * [magento/magento2#26476](https://github.com/magento/magento2/pull/26476) -- magento/magento2#: Remove a redundant call to DB for guest session (by @atwixfirster) + * [magento/magento2#26462](https://github.com/magento/magento2/pull/26462) -- Escape dollar sign for saving content into crontab (by @Erfans) + * [magento/magento2#26451](https://github.com/magento/magento2/pull/26451) -- Add frontend template hints status command (by @WaPoNe) + * [magento/magento2#26430](https://github.com/magento/magento2/pull/26430) -- Unit Test for Magento\Sitemap\Model\Config\Backend\Priority (by @karyna-tsymbal-atwix) + * [magento/magento2#26399](https://github.com/magento/magento2/pull/26399) -- Issue #26332 BeforeOrderPaymentSaveObserver override payment instructions (by @karyna-tsymbal-atwix) + * [magento/magento2#26213](https://github.com/magento/magento2/pull/26213) -- SEO: Do not follow links on filter options (by @paveq) + * [magento/magento2#26007](https://github.com/magento/magento2/pull/26007) -- #25591 & character in SKUs is shown as & in current variations li… (by @KaushikChavda) + * [magento/magento2#25860](https://github.com/magento/magento2/pull/25860) -- Add mass action to invalidate indexes via admin (by @fredden) + * [magento/magento2#25851](https://github.com/magento/magento2/pull/25851) -- Fix SearchResult isCacheable performance (by @wigman) + * [magento/magento2#25742](https://github.com/magento/magento2/pull/25742) -- Http adapter curl missing delete method (by @jimuld) + * [magento/magento2#25324](https://github.com/magento/magento2/pull/25324) -- 13865 safari block cookies breaks javascript scripts (by @raulvOnestic91) + * [magento/magento2#24648](https://github.com/magento/magento2/pull/24648) -- reduce reset data actions on DeploymentConfig (by @georgebabarus) + * [magento/magento2#24485](https://github.com/magento/magento2/pull/24485) -- Fix return type of price currency format method (by @avstudnitz) + * [magento/magento2#26378](https://github.com/magento/magento2/pull/26378) -- 26375 braintree payment address issue (by @chris-pook) + * [magento/magento2#25641](https://github.com/magento/magento2/pull/25641) -- M2C-21768 Validate product quantity on Wishlist update (by @ptylek) + * [magento/magento2#25285](https://github.com/magento/magento2/pull/25285) -- Add lib wrapper for UUID validation. (by @nikolaevas) + * [magento/magento2#26420](https://github.com/magento/magento2/pull/26420) -- #8691: improved language pack inheritance order (by @sergiy-v) + * [magento/magento2#26413](https://github.com/magento/magento2/pull/26413) -- #895 Fix for Media Gallery buttons are shifted to the left (by @diazwatson) + * [magento/magento2#26162](https://github.com/magento/magento2/pull/26162) -- Fixed Issue with tier price 0 when saving product second time (by @ravi-chandra3197) + * [magento/magento2#26623](https://github.com/magento/magento2/pull/26623) -- #26622 - Check quote item for parentItem instead of parentItemId (by @aligent-lturner) + * [magento/magento2#26621](https://github.com/magento/magento2/pull/26621) -- Set of fixes introduced during #CoreReview 31.01.2020 (by @lbajsarowicz) + * [magento/magento2#26546](https://github.com/magento/magento2/pull/26546) -- [fixed My Wish List Product not showing properly between >768px and <… (by @hitesh-wagento) + * [magento/magento2#26423](https://github.com/magento/magento2/pull/26423) -- Update getCustomer method in order class (by @sertlab) + * [magento/magento2#26339](https://github.com/magento/magento2/pull/26339) -- Module xml extra end tag removed (by @tejash-wagento) + * [magento/magento2#24691](https://github.com/magento/magento2/pull/24691) -- Allows additional payment checks in payment method list (by @jensscherbl) + * [magento/magento2#26782](https://github.com/magento/magento2/pull/26782) -- Module_Cms MFTF test improvements (by @ajithkumar-maragathavel) + * [magento/magento2#26781](https://github.com/magento/magento2/pull/26781) -- Code hygeine in bundle option graphql resolver (by @moloughlin) + * [magento/magento2#26770](https://github.com/magento/magento2/pull/26770) -- Unit tests for Magento\Csp\Model\Mode\ConfigManager and Magento\Csp\Observer\Render (by @karyna-tsymbal-atwix) + * [magento/magento2#26764](https://github.com/magento/magento2/pull/26764) -- LoadCssAsync html format fixed for critical css (by @srsathish92) + * [magento/magento2#26714](https://github.com/magento/magento2/pull/26714) -- Deprecated redundant class (by @drpayyne) + * [magento/magento2#26715](https://github.com/magento/magento2/pull/26715) -- Unit test for \Magento\Captcha\Observer\ResetAttemptForBackendObserver and ResetAttemptForFrontendObserver (by @karyna-tsymbal-atwix) + * [magento/magento2#26502](https://github.com/magento/magento2/pull/26502) -- {ASI} :- Error message to be cached for grid data storage component (by @konarshankar07) + * [magento/magento2#26279](https://github.com/magento/magento2/pull/26279) -- Fix issue #26276 with clonning quote billing address street (by @yutv) + * [magento/magento2#26246](https://github.com/magento/magento2/pull/26246) -- magento/magento2#26245: Magento does not send an email about a refunded grouped product (by @atwixfirster) + * [magento/magento2#26142](https://github.com/magento/magento2/pull/26142) -- Textarea patch 1 (by @textarea) + * [magento/magento2#25488](https://github.com/magento/magento2/pull/25488) -- Update composer dependency to fix Redis Key Expiery (by @toxix) + * [magento/magento2#25249](https://github.com/magento/magento2/pull/25249) -- upgrade method delete by ids to inject array skus (by @sarron93) + * [magento/magento2#25246](https://github.com/magento/magento2/pull/25246) -- Warning when Search Terms page is opened by clicking option at the footer (by @vishalverma279) + * [magento/magento2#24843](https://github.com/magento/magento2/pull/24843) -- Issue #24842: Unable to delete custom option file in admin order create (by @adrian-martinez-interactiv4) + * [magento/magento2#26820](https://github.com/magento/magento2/pull/26820) -- [Theme] Covered Unit Test for \Magento\Theme\Controller\Result\JsFooterPlugin (by @srsathish92) + * [magento/magento2#26816](https://github.com/magento/magento2/pull/26816) -- Unit Test for \Magento\Directory\Block\Adminhtml\Frontend\Currency\Base (by @karyna-tsymbal-atwix) + * [magento/magento2#26771](https://github.com/magento/magento2/pull/26771) -- Removed unnecessary function argument (by @ajithkumar-maragathavel) + * [magento/magento2#26684](https://github.com/magento/magento2/pull/26684) -- Move additional dependencies from private getters to constructor - Magento_PageCache (by @Bartlomiejsz) + * [magento/magento2#26674](https://github.com/magento/magento2/pull/26674) -- Comment add in translate. (by @pratikhmehta) + * [magento/magento2#26342](https://github.com/magento/magento2/pull/26342) -- Remove extra space before semicolon and remove extra comma in php, phtml and js files (by @tejash-wagento) + * [magento/magento2#25991](https://github.com/magento/magento2/pull/25991) -- Fixed issue when the preview images navigation is triggered by moving the input filed cursor using arrow keys (by @drpayyne) + * [magento/magento2#26857](https://github.com/magento/magento2/pull/26857) -- Issue/26843: Fix es_US Spanish (United States ) Locale is not support… (by @vincent-le89) + * [magento/magento2#26846](https://github.com/magento/magento2/pull/26846) -- magento/magento2#: GraphQL. setPaymentMethodOnCart mutation. Extend list of required parameters for testSetPaymentMethodWithoutRequiredParameters (by @atwixfirster) + * [magento/magento2#26844](https://github.com/magento/magento2/pull/26844) -- Unit Tests for observers from Magento_Reports (by @karyna-tsymbal-atwix) + * [magento/magento2#26835](https://github.com/magento/magento2/pull/26835) -- Unit test for Magento\Downloadable\Model\Sample\DeleteHandler (by @karyna-tsymbal-atwix) + * [magento/magento2#26839](https://github.com/magento/magento2/pull/26839) -- Unit test for \Magento\MediaGallery\Plugin\Wysiwyg\Images\Storage (by @karyna-tsymbal-atwix) + * [magento/magento2#26769](https://github.com/magento/magento2/pull/26769) -- Fix return type in ResetAttemptForFrontendObserver and ResetAttemptForBackendObserver (by @karyna-tsymbal-atwix) + * [magento/magento2#26768](https://github.com/magento/magento2/pull/26768) -- Marginal space validation (by @ajithkumar-maragathavel) + * [magento/magento2#26712](https://github.com/magento/magento2/pull/26712) -- Unit test for \Magento\Captcha\Observer\CheckUserForgotPasswordBackendObserver (by @karyna-tsymbal-atwix) + * [magento/magento2#26688](https://github.com/magento/magento2/pull/26688) -- Use '===' operator to check if file was written. (by @vovayatsyuk) + * [magento/magento2#26659](https://github.com/magento/magento2/pull/26659) -- [26054-Do not duplicate SEO meta data when duplicating a product] (by @dasharath-wagento) + * [magento/magento2#26398](https://github.com/magento/magento2/pull/26398) -- Move additional dependencies from private getters to constructor - Magento_Captcha (by @Bartlomiejsz) + * [magento/magento2#26317](https://github.com/magento/magento2/pull/26317) -- #26314: fixed logic for updating map price for selected swatch (by @sergiy-v) + * [magento/magento2#24612](https://github.com/magento/magento2/pull/24612) -- Fix for the issue #24547 Magento\Customer\Model\Account\Redirect::setRedirectCookie() not properly working (by @sashas777) + * [magento/magento2#26904](https://github.com/magento/magento2/pull/26904) -- [Layout] Adding 'article' as an additional supported layout tag (by @eduard13) + * [magento/magento2#26899](https://github.com/magento/magento2/pull/26899) -- Unit test for Magento\Vault\Plugin\PaymentVaultAttributesLoad (by @karyna-tsymbal-atwix) + * [magento/magento2#26897](https://github.com/magento/magento2/pull/26897) -- Swatches options: eliminate objects instantiation since only their data needed (by @ilnytskyi) + * [magento/magento2#26894](https://github.com/magento/magento2/pull/26894) -- Unit test for Magento\GiftMessage\Model\Plugin\MergeQuoteItems (by @karyna-tsymbal-atwix) + * [magento/magento2#26878](https://github.com/magento/magento2/pull/26878) -- [NewRelicReporting] Covering the New Relic plugins by Unit Tests (by @eduard13) + * [magento/magento2#26869](https://github.com/magento/magento2/pull/26869) -- Update MockObject class in Widget module (by @hws47a) + * [magento/magento2#26868](https://github.com/magento/magento2/pull/26868) -- Update MockObject class in Wishlist module (by @hws47a) + * [magento/magento2#26863](https://github.com/magento/magento2/pull/26863) -- TierPriceBox toHtml method should be return with string (by @tufahu) + * [magento/magento2#26790](https://github.com/magento/magento2/pull/26790) -- Disabled flat: cast $attributeId as (int) in selects (by @ilnytskyi) + * [magento/magento2#26761](https://github.com/magento/magento2/pull/26761) -- [Analytics] Code refactor & covered unit test in Analytics/ReportXml/QueryFactory (by @srsathish92) + * [magento/magento2#26710](https://github.com/magento/magento2/pull/26710) -- [Customer] Fixing the code style for account edit template and Integration test (by @eduard13) + * [magento/magento2#26701](https://github.com/magento/magento2/pull/26701) -- Date incorrect on pdf invoice issue26675 (by @edenduong) + * [magento/magento2#26650](https://github.com/magento/magento2/pull/26650) -- issue-#25675 Added fix for #25675 issue to the 2.4 Magento version (by @molneek) + * [magento/magento2#26617](https://github.com/magento/magento2/pull/26617) -- Unit Test for Magento\LayeredNavigation\Observer\Edit\Tab\Front\ProductAttributeFormBuildFrontTabObserver (by @karyna-tsymbal-atwix) + * [magento/magento2#26584](https://github.com/magento/magento2/pull/26584) -- #26583 Tier pricing save percent showing logic updated in product detail page (by @srsathish92) + * [magento/magento2#26523](https://github.com/magento/magento2/pull/26523) -- [#25963] Fixed grids export: option labels are taken from grid filters and columns now. (by @novikor) + * [magento/magento2#26418](https://github.com/magento/magento2/pull/26418) -- [Fixed Compare Products section not showing in mobile view under 767px] (by @hitesh-wagento) + * [magento/magento2#25806](https://github.com/magento/magento2/pull/25806) -- Reflection: Fix null as first return type (by @Parakoopa) + * [magento/magento2#25626](https://github.com/magento/magento2/pull/25626) -- Fix translation retrieval (by @brosenberger) + * [magento/magento2#25426](https://github.com/magento/magento2/pull/25426) -- Ui: log exception correctly (by @bchatard) + * [magento/magento2#25417](https://github.com/magento/magento2/pull/25417) -- README > Improving the apresentation (by @rafaelstz) + * [magento/magento2#25321](https://github.com/magento/magento2/pull/25321) -- Changing input phone type to tel in Contact, Error Report and Customer widget pages (by @rafaelstz) + * [magento/magento2#24976](https://github.com/magento/magento2/pull/24976) -- Fix doc block for $queueIterator Magento\Framework\MessageQueue\Topology\Config (by @UncleTioma) + * [magento/magento2#22296](https://github.com/magento/magento2/pull/22296) -- Fix #14958 - remove sales sequence data on store view delete (by @Bartlomiejsz) + * [magento/magento2#26833](https://github.com/magento/magento2/pull/26833) -- magento/magento2#: GraphQL. MergeCarts mutation. Add additional API-functional test cases (by @atwixfirster) + * [magento/magento2#26608](https://github.com/magento/magento2/pull/26608) -- HOTFIX: #26607 Fix failing CI due to Functional Tests (by @lbajsarowicz) + * [magento/magento2#26772](https://github.com/magento/magento2/pull/26772) -- Clean up 'isset' coding style (by @GraysonChiang) + * [magento/magento2#25858](https://github.com/magento/magento2/pull/25858) -- FIX #25856 / Group Ordered Products report by SKU (by @lbajsarowicz) + * [magento/magento2#23570](https://github.com/magento/magento2/pull/23570) -- Improve: [UrlRewrite] Move grid implementation to ui components (by @Den4ik) + * [magento/magento2#26995](https://github.com/magento/magento2/pull/26995) -- Fix typo in description node. (by @BorisovskiP) + * [magento/magento2#26982](https://github.com/magento/magento2/pull/26982) -- Remove app/functions.php (by @Bartlomiejsz) + * [magento/magento2#26974](https://github.com/magento/magento2/pull/26974) -- Fix: #26973 Fatal error when Product Image size is not defined (by @lbajsarowicz) + * [magento/magento2#26947](https://github.com/magento/magento2/pull/26947) -- Unit test for AssignCouponDataAfterOrderCustomerAssignObserver (by @mmezhensky) + * [magento/magento2#26944](https://github.com/magento/magento2/pull/26944) -- Unit test for CatalogProductCompareClearObserver (by @mmezhensky) + * [magento/magento2#26932](https://github.com/magento/magento2/pull/26932) -- Fix #26917 Tax rate zip/post range check box alignment issue (by @srsathish92) + * [magento/magento2#26928](https://github.com/magento/magento2/pull/26928) -- Module_Cms MFTF test improvements (by @nandhini-nagaraj) + * [magento/magento2#26916](https://github.com/magento/magento2/pull/26916) -- Move cache cleanup operation on state modification operation (by @kandy) + * [magento/magento2#26912](https://github.com/magento/magento2/pull/26912) -- Unit test for ProductProcessUrlRewriteRemovingObserver (by @mmezhensky) + * [magento/magento2#26862](https://github.com/magento/magento2/pull/26862) -- Removed disabled products from low stock report grid (by @Mohamed-Asar) + * [magento/magento2#26821](https://github.com/magento/magento2/pull/26821) -- Reduce requirements for parameter in catalog product type factory (by @hws47a) + * [magento/magento2#26755](https://github.com/magento/magento2/pull/26755) -- Fix wrong type of argument appendSummaryFieldsToCollection (by @Usik2203) + * [magento/magento2#26697](https://github.com/magento/magento2/pull/26697) -- Update the product model custom option methods PHPdoc (by @hws47a) + * [magento/magento2#26586](https://github.com/magento/magento2/pull/26586) -- Improve exception message (by @oroskodias) + * [magento/magento2#26230](https://github.com/magento/magento2/pull/26230) -- Activated "Pending Reviews" menu item when merchant opens 'Pending Reviews' section (by @rav-redchamps) + * [magento/magento2#26090](https://github.com/magento/magento2/pull/26090) -- Fixed issue 25910 choose drop down not close when open another (by @Usik2203) + * [magento/magento2#25895](https://github.com/magento/magento2/pull/25895) -- Change tag name (by @AndreyChorniy) + * [magento/magento2#25349](https://github.com/magento/magento2/pull/25349) -- Fixed issue when escape key is pressed to close prompt (by @konarshankar07) + * [magento/magento2#25161](https://github.com/magento/magento2/pull/25161) -- fixed confusing grammar in the backend formatDate() function (by @princefishthrower) + * [magento/magento2#26979](https://github.com/magento/magento2/pull/26979) -- #26800 Fixed Undefined variable in ProductLink/Management (by @srsathish92) + * [magento/magento2#26842](https://github.com/magento/magento2/pull/26842) -- Unit test for Magento\Catalog\Observer\SetSpecialPriceStartDate has added (by @mmezhensky) + * [magento/magento2#26615](https://github.com/magento/magento2/pull/26615) -- Add missing annotations to MFTF tests (by @sta1r) + * [magento/magento2#25828](https://github.com/magento/magento2/pull/25828) -- MFTF: Extract Action Groups to separate files - magento/module-customer (by @lbajsarowicz) + * [magento/magento2#25311](https://github.com/magento/magento2/pull/25311) -- Add afterGetList method in CustomerRepository plugin to retrieve is_s… (by @enriquei4) + * [magento/magento2#27054](https://github.com/magento/magento2/pull/27054) -- Cleanup ObjectManager usage - Magento_Authorization (by @Bartlomiejsz) + * [magento/magento2#27048](https://github.com/magento/magento2/pull/27048) -- #27044 Integration Test to cover `$storeId` on Category Repository `get()` (by @lbajsarowicz) + * [magento/magento2#27041](https://github.com/magento/magento2/pull/27041) -- FIX: responsiveness for images (by @GrimLink) + * [magento/magento2#27021](https://github.com/magento/magento2/pull/27021) -- Unit test for \Magento\MediaGallery\Plugin\Product\Gallery\Processor (by @karyna-tsymbal-atwix) + * [magento/magento2#27010](https://github.com/magento/magento2/pull/27010) -- Unit test for UpdateElementTypesObserver (by @mmezhensky) + * [magento/magento2#26779](https://github.com/magento/magento2/pull/26779) -- Fix failure for missing product on Storefront (by @lbajsarowicz) + * [magento/magento2#26765](https://github.com/magento/magento2/pull/26765) -- Fix #17933 - Bank Transfer Payment Instructions switch back to default (by @Bartlomiejsz) + * [magento/magento2#26548](https://github.com/magento/magento2/pull/26548) -- issue/26384 Fix store switcher when using different base url on stores (by @TobiasCodeNull) + * [magento/magento2#26329](https://github.com/magento/magento2/pull/26329) -- MFTF: Replace invalid ActionGroup for AdminLogin (by @lbajsarowicz) + * [magento/magento2#25359](https://github.com/magento/magento2/pull/25359) -- Fix #25243 (by @korostii) + * [magento/magento2#24003](https://github.com/magento/magento2/pull/24003) -- Fixes less compilation problems in the Magento/luma theme (by @hostep) + * [magento/magento2#27114](https://github.com/magento/magento2/pull/27114) -- magento/magento2#: Remove a redundant PHP5 directives from .htaccess (by @atwixfirster) + * [magento/magento2#27057](https://github.com/magento/magento2/pull/27057) -- Removed redundant method _beforeToHtml (by @Usik2203) + * [magento/magento2#27033](https://github.com/magento/magento2/pull/27033) -- Catalog image lazy load (by @tdgroot) + * [magento/magento2#26907](https://github.com/magento/magento2/pull/26907) -- Open separate page when click 'View' in CMS pages(Grid) (by @dominicfernando) + * [magento/magento2#26619](https://github.com/magento/magento2/pull/26619) -- Cover CartTotalRepositoryPlugin by unit test and correct docblock (by @mrtuvn) + * [magento/magento2#26778](https://github.com/magento/magento2/pull/26778) -- Eliminate the need for inheritance for action controllers. (by @lbajsarowicz) + * [magento/magento2#26990](https://github.com/magento/magento2/pull/26990) -- #26989 MFTF: Use for reindex (by @lbajsarowicz) + * [magento/magento2#27196](https://github.com/magento/magento2/pull/27196) -- Remove @author annotation from Magento framework (by @diazwatson) + * [magento/magento2#27149](https://github.com/magento/magento2/pull/27149) -- 27027 added date format adjustment for 'validate-dob' rule (by @sergiy-v) + * [magento/magento2#27138](https://github.com/magento/magento2/pull/27138) -- Removed unnecessary tabindex property (by @drpayyne) + * [magento/magento2#27131](https://github.com/magento/magento2/pull/27131) -- 26758 improved cms page custom layout update logic (by @sergiy-v) + * [magento/magento2#27084](https://github.com/magento/magento2/pull/27084) -- Cleanup ObjectManager usage - Magento_CacheInvalidate (by @Bartlomiejsz) + * [magento/magento2#27083](https://github.com/magento/magento2/pull/27083) -- Cleanup ObjectManager usage - Magento_AsynchronousOperations (by @Bartlomiejsz) + * [magento/magento2#27082](https://github.com/magento/magento2/pull/27082) -- Cleanup ObjectManager usage - Magento_Analytics (by @Bartlomiejsz) + * [magento/magento2#27080](https://github.com/magento/magento2/pull/27080) -- Cleanup ObjectManager usage - Magento_EncryptionKey (by @Bartlomiejsz) + * [magento/magento2#27029](https://github.com/magento/magento2/pull/27029) -- #26847: Added 'enterKey' event handler to prompt widget (by @sergiy-v) + * [magento/magento2#27026](https://github.com/magento/magento2/pull/27026) -- Issue 27009: Fix error fire on catch when create new theme (by @vincent-le89) + * [magento/magento2#27014](https://github.com/magento/magento2/pull/27014) -- Fix #26992 Add new rating is active checkbox alignment issue (by @srsathish92) + * [magento/magento2#26964](https://github.com/magento/magento2/pull/26964) -- Cleanup ObjectManager usage - Magento_Elasticsearch (by @Bartlomiejsz) + * [magento/magento2#26939](https://github.com/magento/magento2/pull/26939) -- ObjectManager cleanup - Remove usage from AdminNotification module (by @ihor-sviziev) + * [magento/magento2#26902](https://github.com/magento/magento2/pull/26902) -- Fix #20309 - URL Rewrites redirect loop (by @Bartlomiejsz) + * [magento/magento2#26649](https://github.com/magento/magento2/pull/26649) -- table bottom color different then thead and tbody border color (by @tejash-wagento) + * [magento/magento2#26642](https://github.com/magento/magento2/pull/26642) -- MAG-251090-26590: Fixed Customer registration multiple form submit (by @princeCB) + * [magento/magento2#26563](https://github.com/magento/magento2/pull/26563) -- magento/magento2#: Test Coverage. API functional tests. removeItemFromCart (by @atwixfirster) + * [magento/magento2#25454](https://github.com/magento/magento2/pull/25454) -- TinyMCE4 hard to input double byte characters on chrome (by @HirokazuNishi) + * [magento/magento2#24878](https://github.com/magento/magento2/pull/24878) -- Create missing directories in imageuploader tree if they don't alread… (by @hostep) + * [magento/magento2#24743](https://github.com/magento/magento2/pull/24743) -- fix issue 24735 (by @dmdanilchenko) + * [magento/magento2#23742](https://github.com/magento/magento2/pull/23742) -- Add Header (h1 - h6) tags to layout xml htmlTags Allowed types (by @furan917) + * [magento/magento2#22442](https://github.com/magento/magento2/pull/22442) -- Add support for char element to dto factory (by @wardcapp) + * [magento/magento2#27172](https://github.com/magento/magento2/pull/27172) -- magento/magento2#14086: Guest cart API ignoring cartId in url for some methods (by @engcom-Charlie) + * [magento/magento2#27179](https://github.com/magento/magento2/pull/27179) -- improve Magento\Catalog\Model\ImageUploader error handler (by @fsw) + * [magento/magento2#27145](https://github.com/magento/magento2/pull/27145) -- Cleanup ObjectManager usage - Magento_WebapiAsync (by @Bartlomiejsz) + * [magento/magento2#26959](https://github.com/magento/magento2/pull/26959) -- Correctly escape custom product image attributes (by @alexander-aleman) + * [magento/magento2#26506](https://github.com/magento/magento2/pull/26506) -- #26499 Always transliterate product url key (by @DanieliMi) + * [magento/magento2#25722](https://github.com/magento/magento2/pull/25722) -- Magento#25669: fixed issue "health_check.php fails if any database cache engine configured" (by @andrewbess) + * [magento/magento2#27284](https://github.com/magento/magento2/pull/27284) -- Fix static test failures for class annotaions (by @ihor-sviziev) + * [magento/magento2#27281](https://github.com/magento/magento2/pull/27281) -- TYPO: Fix annoying typo in Quantity word (by @lbajsarowicz) + * [magento/magento2#27277](https://github.com/magento/magento2/pull/27277) -- MFTF: Rename and rewrite Test that fake expired session (by @lbajsarowicz) + * [magento/magento2#27274](https://github.com/magento/magento2/pull/27274) -- MFTF: Create Account tests (Success & Failure) with `extend` (by @lbajsarowicz) + * [magento/magento2#27261](https://github.com/magento/magento2/pull/27261) -- 20472 added product list price modifier (by @sergiy-v) + * [magento/magento2#27249](https://github.com/magento/magento2/pull/27249) -- Update Frontend Development Workflow type's comment to be clearer (by @navarr) + * [magento/magento2#26784](https://github.com/magento/magento2/pull/26784) -- [Forward Port PR-14344] Fix generating product URL rewrites for anchor categories (by @hostep) + * [magento/magento2#26746](https://github.com/magento/magento2/pull/26746) -- In System/Export controlers use MessageManager instead of throwing exceptions (by @pmarki) + * [magento/magento2#26348](https://github.com/magento/magento2/pull/26348) -- Fixed #26345 Reorder in Admin panel leads to report page in case of changed product custom option max characters (by @cedmudit) + * [magento/magento2#27187](https://github.com/magento/magento2/pull/27187) -- 26117: "Current user does not have an active cart" even when he actually has one (by @engcom-Charlie) + * [magento/magento2#27170](https://github.com/magento/magento2/pull/27170) -- 26825 add all image roles for first product entity (by @sergiy-v) + * [magento/magento2#25733](https://github.com/magento/magento2/pull/25733) -- Resolve Mass Delete Widget should have "Confirmation Modal" (by @edenduong) + * [magento/magento2#27118](https://github.com/magento/magento2/pull/27118) -- FIX #27117 Invalid functional Test names (by @lbajsarowicz) + * [magento/magento2#27266](https://github.com/magento/magento2/pull/27266) -- MFTF: Enable Persistent Shopping Cart. Assert Options (by @DmitryTsymbal) + * [magento/magento2#27255](https://github.com/magento/magento2/pull/27255) -- MFTF: Replace fragile test `AdminLoginTest` with `AdminLoginSuccessfulTest` (by @lbajsarowicz) + * [magento/magento2#27165](https://github.com/magento/magento2/pull/27165) -- [feature] Display category filter item in layered navigation based on the system configuration from admin area (by @vasilii-b) + * [magento/magento2#27015](https://github.com/magento/magento2/pull/27015) -- MC-26683: Removed get errors of cart allowing to add product to cart (by @AleksLi) + * [magento/magento2#26987](https://github.com/magento/magento2/pull/26987) -- Remove unused requirejs alias defined (by @mrtuvn) + * [magento/magento2#26560](https://github.com/magento/magento2/pull/26560) -- #26473: Improved logic for product image updating for configurable products (by @sergiy-v) + * [magento/magento2#25297](https://github.com/magento/magento2/pull/25297) -- Add 'schedule status' column to admin indexer grid (by @fredden) + * [magento/magento2#24479](https://github.com/magento/magento2/pull/24479) -- 22251 admin order email is now required 1 (by @solwininfotech) + * [magento/magento2#27273](https://github.com/magento/magento2/pull/27273) -- MFTF: Test isolation, consistent naming (backwards-compatible) (by @lbajsarowicz) + * [magento/magento2#27237](https://github.com/magento/magento2/pull/27237) -- magento/magento2#26749: Saving CMS Page Title from REST web API makes content empty (by @engcom-Charlie) + * [magento/magento2#27215](https://github.com/magento/magento2/pull/27215) -- Cleanup ObjectManager usage - Magento_Translation (by @Bartlomiejsz) + * [magento/magento2#27191](https://github.com/magento/magento2/pull/27191) -- 26827 Added improvements to product attribute repository (save method) (by @sergiy-v) + * [magento/magento2#27125](https://github.com/magento/magento2/pull/27125) -- #27124: Update wishlist image logic to match logic on wishlist page (by @mtbottens) + * [magento/magento2#26015](https://github.com/magento/magento2/pull/26015) -- Remove media gallery assets metadata when a directory removed (by @rogyar) + * [magento/magento2#25734](https://github.com/magento/magento2/pull/25734) -- Experius 2.3 patch catalog flat (by @lewisvoncken) + * [magento/magento2#23191](https://github.com/magento/magento2/pull/23191) -- Refactor addlinks to own class take 3 (follows #21658) (by @amenk) + * [magento/magento2#27336](https://github.com/magento/magento2/pull/27336) -- fixed My account Address Book Additional Address Entries table issue #27335 (by @abrarpathan19) + * [magento/magento2#27304](https://github.com/magento/magento2/pull/27304) -- FIX #14080 Added improvements to Category repository (save method) (by @sergiy-v) + * [magento/magento2#27298](https://github.com/magento/magento2/pull/27298) -- Implement ActionInterface for `cms/page/view` (by @lbajsarowicz) + * [magento/magento2#27292](https://github.com/magento/magento2/pull/27292) -- Magento_Bundle / Remove `cache:flush` and extract Tests to separate files (by @lbajsarowicz) + * [magento/magento2#27263](https://github.com/magento/magento2/pull/27263) -- #26708 Fix: ORDER BY has two similar conditions in the SQL query (by @vasilii-b) + * [magento/magento2#27214](https://github.com/magento/magento2/pull/27214) -- Mark AbstractAccount as DEPRECATED for Magento_Customer controllers (by @lbajsarowicz) + * [magento/magento2#27116](https://github.com/magento/magento2/pull/27116) -- Add Italy States (by @WaPoNe) + * [magento/magento2#26748](https://github.com/magento/magento2/pull/26748) -- magento#26745 add method setAdditionalInformation to OrderPaymentInte… (by @antoninobonumore) + * [magento/magento2#26923](https://github.com/magento/magento2/pull/26923) -- Improve dashboard charts - migrate to chart.js (by @Bartlomiejsz) + * [magento/magento2#27390](https://github.com/magento/magento2/pull/27390) -- magento/magento2: fixes PHPDocs for module Magento_reports (by @andrewbess) + * [magento/magento2#27375](https://github.com/magento/magento2/pull/27375) -- Updating link to Adobe CLA in contributing.md (by @filmaj) + * [magento/magento2#27353](https://github.com/magento/magento2/pull/27353) -- Add xml declaration for catalog_widget_product_list.xml file (by @Usik2203) + * [magento/magento2#27334](https://github.com/magento/magento2/pull/27334) -- MFTF: Customer Subscribes To Newsletter Subscription On StoreFront (by @DmitryTsymbal) + * [magento/magento2#27319](https://github.com/magento/magento2/pull/27319) -- Cleanup ObjectManager usage - Magento_Catalog ViewModel,Plugin (by @Bartlomiejsz) + * [magento/magento2#27307](https://github.com/magento/magento2/pull/27307) -- magento/magento2: Fixes for the schema cache.xsd (by @andrewbess) + * [magento/magento2#27276](https://github.com/magento/magento2/pull/27276) -- Add "Admin" prefix to Test and ActionGroup (by @lbajsarowicz) + * [magento/magento2#27000](https://github.com/magento/magento2/pull/27000) -- MFTF FIX: Remove Customer by e-mail does not filter by e-mail (by @lbajsarowicz) + * [magento/magento2#26538](https://github.com/magento/magento2/pull/26538) -- Refactor datetime class (by @Tjitse-E) + * [magento/magento2#25664](https://github.com/magento/magento2/pull/25664) -- magento/magento2#25540: Products are not displaying infront end after updating product via importing CSV. (by @p-bystritsky) + * [magento/magento2#22011](https://github.com/magento/magento2/pull/22011) -- magento/magento2#22010: Updates AbstractExtensibleObject and AbstractExtensibleModel annotations (by @atwixfirster) + * [magento/magento2#27378](https://github.com/magento/magento2/pull/27378) -- MFTF: Refactor `amOnPage` for Admin product edit page (by @lbajsarowicz) + * [magento/magento2#26055](https://github.com/magento/magento2/pull/26055) -- [Fixed] - HTML Validation issue Replace Attribute with data-* attribute (by @niravkrish) + * [magento/magento2#27412](https://github.com/magento/magento2/pull/27412) -- Added improvements to category url key validation logic (by @sergiy-v) + * [magento/magento2#27393](https://github.com/magento/magento2/pull/27393) -- Implement ActionInterface for /robots/index/index (by @Bartlomiejsz) + * [magento/magento2#27385](https://github.com/magento/magento2/pull/27385) -- Cleanup ObjectManager usage - Magento_SendFriend (by @Bartlomiejsz) + * [magento/magento2#27384](https://github.com/magento/magento2/pull/27384) -- Cleanup ObjectManager usage - Magento_Sitemap (by @Bartlomiejsz) + * [magento/magento2#27383](https://github.com/magento/magento2/pull/27383) -- #27370 Internet explorer issue:Default billing/shipping address not showing (by @vasilii-b) + * [magento/magento2#27381](https://github.com/magento/magento2/pull/27381) -- Implement ActionInterface for /captcha/refresh (by @lbajsarowicz) + * [magento/magento2#27360](https://github.com/magento/magento2/pull/27360) -- Move JS module initialization to separate tasks (by @krzksz) + * [magento/magento2#27088](https://github.com/magento/magento2/pull/27088) -- Fix Report date doesn't matching in configuration setting (by @Priya-V-Panchal) + * [magento/magento2#22837](https://github.com/magento/magento2/pull/22837) -- Short-term admin accounts #22833 (by @lfolco) + * [magento/magento2#26075](https://github.com/magento/magento2/pull/26075) -- Fix #6310 - Changing products 'this item has weight' using 'Update Attributes' is not possible (by @Bartlomiejsz) + * [magento/magento2#27388](https://github.com/magento/magento2/pull/27388) -- {ASI} :- Image size is not passed to image-uploader when inserting an image from new media gallery (by @konarshankar07) + * [magento/magento2#26999](https://github.com/magento/magento2/pull/26999) -- Fixed URL Rewrite addition/removal on product website add/remove (by @gwharton) + * [magento/magento2#27371](https://github.com/magento/magento2/pull/27371) -- [Admin] Do not allow HTML tags for the Product Attribute labels on save (by @vasilii-b) + * [magento/magento2#27509](https://github.com/magento/magento2/pull/27509) -- [MFTF] fixed test `AdminLoginWithRestrictPermissionTest` (by @engcom-Charlie) + * [magento/magento2#27462](https://github.com/magento/magento2/pull/27462) -- Implement ActionInterface for /search/term/popular (by @Bartlomiejsz) + * [magento/magento2#27427](https://github.com/magento/magento2/pull/27427) -- Implement ActionInterface for /swagger/ (by @lbajsarowicz) + * [magento/magento2#27425](https://github.com/magento/magento2/pull/27425) -- Implement ActionInterface for /version/ (by @lbajsarowicz) + * [magento/magento2#27413](https://github.com/magento/magento2/pull/27413) -- Add follow symlinks to support linked folders (by @Nazar65) + * [magento/magento2#27365](https://github.com/magento/magento2/pull/27365) -- Fix issue 16315: Product save with onthefly index ignores website assignments (by @tna274) + * [magento/magento2#27257](https://github.com/magento/magento2/pull/27257) -- Save Asynchronous Operations with one Batch improvement (by @nuzil) + * [magento/magento2#26763](https://github.com/magento/magento2/pull/26763) -- fix: prevent undefined index error - closes #26762 (by @DanielRuf) + * [magento/magento2#26736](https://github.com/magento/magento2/pull/26736) -- {ASI} : SortBy component added (by @konarshankar07) + * [magento/magento2#26618](https://github.com/magento/magento2/pull/26618) -- Correct docblock CartTotalRepository get method (by @mrtuvn) + * [magento/magento2#26417](https://github.com/magento/magento2/pull/26417) -- translate.js Not shows empty values (by @ilnytskyi) + * [magento/magento2#27493](https://github.com/magento/magento2/pull/27493) -- Fix the minicart items actions alignment for tablet and desktop devices (by @vasilii-b) + * [magento/magento2#27492](https://github.com/magento/magento2/pull/27492) -- Fixed tests for Magento\Framework\Stdlib\DateTime\DateTime (by @andrewbess) + * [magento/magento2#27399](https://github.com/magento/magento2/pull/27399) -- Fixed the wrong behavior for a prompt modal when a user clicks on the modal overlay (by @serhiyzhovnir) + * [magento/magento2#26397](https://github.com/magento/magento2/pull/26397) -- Cleanup ObjectManager usage - Magento_Bundle (by @Bartlomiejsz) + * [magento/magento2#26100](https://github.com/magento/magento2/pull/26100) -- Fixed 24990: link doesn't redirect to dashboard (by @Usik2203) + * [magento/magento2#27545](https://github.com/magento/magento2/pull/27545) -- Fix XML Schema Location (by @sprankhub) + * [magento/magento2#27544](https://github.com/magento/magento2/pull/27544) -- Fix incorrect alignment element in login container theme blank (by @mrtuvn) + * [magento/magento2#27526](https://github.com/magento/magento2/pull/27526) -- [MFTF] using StorefrontOpenHomePageActionGroup for navigation to Home Page (by @Usik2203) + * [magento/magento2#27521](https://github.com/magento/magento2/pull/27521) -- PhpUnit 8 Migration - AdminNotification (by @ihor-sviziev) + * [magento/magento2#27497](https://github.com/magento/magento2/pull/27497) -- [bugfix] The store logo is missing when using the Magento_blank theme (by @vasilii-b) + * [magento/magento2#27495](https://github.com/magento/magento2/pull/27495) -- Make the header switcher styles more flexible (by @vasilii-b) + * [magento/magento2#27463](https://github.com/magento/magento2/pull/27463) -- Implement ActionInterface for /checkout/sidebar/removeItem (by @Bartlomiejsz) + * [magento/magento2#27295](https://github.com/magento/magento2/pull/27295) -- Fix the error that is wrong link title of a downloadable product when enabling "Use Default Value" (by @tna274) + * [magento/magento2#26900](https://github.com/magento/magento2/pull/26900) -- Removed references to '%context%' (dead code) (by @markshust) + * [magento/magento2#26801](https://github.com/magento/magento2/pull/26801) -- Prevent resizing an image if it was already resized before (by @hostep) + * [magento/magento2#27519](https://github.com/magento/magento2/pull/27519) -- PhpUnit 8 Migration - Framework & AdminAnalytics (by @ihor-sviziev) + * [magento/magento2#27322](https://github.com/magento/magento2/pull/27322) -- MFTF: Add ` - - Magento - -

-

-

+ + Magento Commerce + +
+
Open Source Helpers diff --git a/app/autoload.php b/app/autoload.php index d6407083dc0b3..6a0a95347a681 100644 --- a/app/autoload.php +++ b/app/autoload.php @@ -5,36 +5,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + use Magento\Framework\Autoload\AutoloaderRegistry; use Magento\Framework\Autoload\ClassLoaderWrapper; /** * Shortcut constant for the root directory */ -define('BP', dirname(__DIR__)); +\define('BP', \dirname(__DIR__)); -define('VENDOR_PATH', BP . '/app/etc/vendor_path.php'); +\define('VENDOR_PATH', BP . '/app/etc/vendor_path.php'); -if (!file_exists(VENDOR_PATH)) { +if (!\is_readable(VENDOR_PATH)) { throw new \Exception( 'We can\'t read some files that are required to run the Magento application. ' . 'This usually means file permissions are set incorrectly.' ); } -$vendorDir = require VENDOR_PATH; -$vendorAutoload = BP . "/{$vendorDir}/autoload.php"; +$vendorAutoload = ( + static function (): ?string { + $vendorDir = require VENDOR_PATH; + + $vendorAutoload = BP . "/{$vendorDir}/autoload.php"; + if (\is_readable($vendorAutoload)) { + return $vendorAutoload; + } + + $vendorAutoload = "{$vendorDir}/autoload.php"; + if (\is_readable($vendorAutoload)) { + return $vendorAutoload; + } + + return null; + } +)(); -/* 'composer install' validation */ -if (file_exists($vendorAutoload)) { - $composerAutoloader = include $vendorAutoload; -} else if (file_exists("{$vendorDir}/autoload.php")) { - $vendorAutoload = "{$vendorDir}/autoload.php"; - $composerAutoloader = include $vendorAutoload; -} else { +if ($vendorAutoload === null) { throw new \Exception( 'Vendor autoload is not found. Please run \'composer install\' under application root directory.' ); } +$composerAutoloader = include $vendorAutoload; AutoloaderRegistry::registerAutoloader(new ClassLoaderWrapper($composerAutoloader)); diff --git a/app/bootstrap.php b/app/bootstrap.php index 4974acdf0fc80..4c96d084f4bb6 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -14,15 +14,15 @@ #ini_set('display_errors', 1); /* PHP version validation */ -if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 70103) { +if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 70300) { if (PHP_SAPI == 'cli') { - echo 'Magento supports PHP 7.1.3 or later. ' . - 'Please read https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements-tech.html'; + echo 'Magento supports PHP 7.3.0 or later. ' . + 'Please read https://devdocs.magento.com/guides/v2.4/install-gde/system-requirements-tech.html'; } else { echo << -

Magento supports PHP 7.1.3 or later. Please read - +

Magento supports PHP 7.3.0 or later. Please read + Magento System Requirements. HTML; 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/Test/Mftf/Test/TrackingScriptTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml deleted file mode 100644 index e02c34fd8868e..0000000000000 --- a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="TrackingScriptTest"> - <annotations> - <features value="Backend"/> - <stories value="Checks to see if the tracking script is in the dom of admin and if setting is turned to no it checks if the tracking script in the dom was removed"/> - <title value="Checks to see if the tracking script is in the dom of admin and if setting is turned to no it checks if the tracking script in the dom was removed"/> - <description value="Checks to see if the tracking script is in the dom of admin and if setting is turned to no it checks if the tracking script in the dom was removed"/> - <severity value="CRITICAL"/> - <testCaseId value="MC-18192"/> - <group value="backend"/> - <group value="login"/> - </annotations> - - <!-- Logging in Magento admin --> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - </test> -</tests> \ No newline at end of file diff --git a/app/code/Magento/AdminAnalytics/etc/csp_whitelist.xml b/app/code/Magento/AdminAnalytics/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..f16a66aa090e3 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/etc/csp_whitelist.xml @@ -0,0 +1,36 @@ +<?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="script-src"> + <values> + <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/AdminAnalytics/view/adminhtml/templates/notification.phtml b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/notification.phtml index 4b1f971670184..6d56cd4452a91 100644 --- a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/notification.phtml +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/notification.phtml @@ -4,13 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php +$isAnaliticsVisible = $block->getNotification()->isAnalyticsVisible() ? 1 : 0; +$isReleaseVisible = $block->getNotification()->isReleaseVisible() ? 1 : 0; +$scriptString = <<<script define('analyticsPopupConfig', function () { return { - analyticsVisible: <?= $block->getNotification()->isAnalyticsVisible() ? 1 : 0; ?>, - releaseVisible: <?= $block->getNotification()->isReleaseVisible() ? 1 : 0; ?>, + analyticsVisible: {$isAnaliticsVisible}, + releaseVisible: {$isReleaseVisible}, } }); -</script> +script; +?> + +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml index 0ea5c753c9337..bfe58de1eac5f 100644 --- a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml @@ -3,13 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script src="<?= $block->escapeUrl($block->getTrackingUrl()) ?>" async></script> -<script> +<?= /* @noEscape */ $secureRenderer->renderTag( + 'script', + [ + 'src' => $block->getTrackingUrl(), + 'async' => true, + ], + ' ', + false +) ?> + +<?php $scriptString = ' var adminAnalyticsMetadata = { - "version": "<?= $block->escapeJs($block->getMetadata()->getMagentoVersion()) ?>", - "user": "<?= $block->escapeJs($block->getMetadata()->getCurrentUser()) ?>", - "mode": "<?= $block->escapeJs($block->getMetadata()->getMode()) ?>" + "version": "' . $block->escapeJs($block->getMetadata()->getMagentoVersion()) . '", + "user": "' . $block->escapeJs($block->getMetadata()->getCurrentUser()) . '", + "mode": "' . $block->escapeJs($block->getMetadata()->getMode()) . '" }; -</script> +'; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/AdminNotification/Block/System/Messages.php b/app/code/Magento/AdminNotification/Block/System/Messages.php index c9b3a0b8844cc..c99a71a51e6ea 100644 --- a/app/code/Magento/AdminNotification/Block/System/Messages.php +++ b/app/code/Magento/AdminNotification/Block/System/Messages.php @@ -26,7 +26,7 @@ class Messages extends Template /** * @var JsonDataHelper - * @deprecated + * @deprecated 100.3.0 */ protected $jsonHelper; diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php index d58a7ec31f77d..f3d3cd64ddc64 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php @@ -17,7 +17,7 @@ class ListAction extends \Magento\Backend\App\AbstractAction /** * @var \Magento\Framework\Json\Helper\Data - * @deprecated + * @deprecated 100.3.0 */ protected $jsonHelper; 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/etc/csp_whitelist.xml b/app/code/Magento/AdminNotification/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..c3327a716947b --- /dev/null +++ b/app/code/Magento/AdminNotification/etc/csp_whitelist.xml @@ -0,0 +1,18 @@ +<?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="commerce_widgets" type="host">widgets.magentocommerce.com</value> + </values> + </policy> + </policies> +</csp_whitelist> + 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/AdminNotification/view/adminhtml/templates/notification/window.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml index b4f19bda36cbf..f2e8e96fa2585 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml +++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml @@ -6,10 +6,10 @@ /** * @see \Magento\AdminNotification\Block\Window + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <ul class="message-system-list" - style="display: none;" data-mage-init='{ "Magento_Ui/js/modal/modal": { "autoOpen": true, @@ -25,3 +25,4 @@ </a> </li> </ul> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '.message-system-list'); ?> diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml index 494e60865623b..2217d441d96ad 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml +++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml @@ -5,18 +5,20 @@ */ /** @var $block \Magento\AdminNotification\Block\System\Messages\UnreadMessagePopup */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div style="display:none" id="system_messages_list" data-role="system_messages_list" +<div id="system_messages_list" data-role="system_messages_list" title="<?= $block->escapeHtmlAttr($block->getPopupTitle()) ?>"> <ul class="message-system-list messages"> - <?php foreach ($block->getUnreadMessages() as $message) : ?> + <?php foreach ($block->getUnreadMessages() as $message): ?> <li class="message message-warning <?= $block->escapeHtmlAttr($block->getItemClass($message)) ?>"> <?= $block->escapeHtml($message->getText()) ?> </li> <?php endforeach;?> </ul> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '#system_messages_list'); ?> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index 39009e5c7b4e3..27e2713995653 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php @@ -381,7 +381,7 @@ private function prepareExportData( * @param array $exportData * @return array * @SuppressWarnings(PHPMD.UnusedLocalVariable) - * @deprecated + * @deprecated 100.3.0 * @see prepareExportData */ protected function correctExportData($exportData) @@ -510,7 +510,7 @@ private function fetchTierPrices(array $productIds): array * @return array|bool * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @deprecated + * @deprecated 100.3.0 * @see fetchTierPrices */ protected function getTierPrices(array $listSku, $table) 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/Model/Import/AdvancedPricing/Validator/TierPrice.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php index b1f99bb1fc05f..2ad96cfeab1d9 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php @@ -3,15 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator; use Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing; +use Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; +use Magento\CatalogImportExport\Model\Import\Product\StoreResolver; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractPrice; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\LocalizedException; -class TierPrice extends \Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractPrice +class TierPrice extends AbstractPrice { /** - * @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver + * @var StoreResolver */ protected $storeResolver; @@ -27,21 +37,26 @@ class TierPrice extends \Magento\CatalogImportExport\Model\Import\Product\Valida ]; /** - * @param \Magento\Customer\Api\GroupRepositoryInterface $groupRepository - * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder - * @param \Magento\CatalogImportExport\Model\Import\Product\StoreResolver $storeResolver + * @param GroupRepositoryInterface $groupRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param StoreResolver $storeResolver */ public function __construct( - \Magento\Customer\Api\GroupRepositoryInterface $groupRepository, - \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, - \Magento\CatalogImportExport\Model\Import\Product\StoreResolver $storeResolver + GroupRepositoryInterface $groupRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + StoreResolver $storeResolver ) { $this->storeResolver = $storeResolver; parent::__construct($groupRepository, $searchCriteriaBuilder); } /** - * {@inheritdoc} + * Initialize method + * + * @param Product $context + * + * @return RowValidatorInterface|AbstractImportValidator|void + * @throws LocalizedException */ public function init($context) { @@ -52,7 +67,10 @@ public function init($context) } /** + * Add decimal error + * * @param string $attribute + * * @return void */ protected function addDecimalError($attribute) @@ -83,12 +101,12 @@ public function getCustomerGroups() } /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * Validation * * @param mixed $value * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ public function isValid($value) { @@ -133,6 +151,7 @@ public function isValid($value) * Check if at list one value and length are valid * * @param array $value + * * @return bool */ protected function isValidValueAndLength(array $value) @@ -150,6 +169,7 @@ protected function isValidValueAndLength(array $value) * Check if value has empty columns * * @param array $value + * * @return bool */ protected function hasEmptyColumns(array $value) diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php index 6aa59e6227a05..71b5271a90fa2 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php @@ -4,28 +4,24 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator; use Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; /** * Class TierPriceType validates tier price type. */ -class TierPriceType extends \Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator +class TierPriceType extends AbstractImportValidator { - /** - * {@inheritdoc} - */ - public function init($context) - { - return parent::init($context); - } - /** * Validate tier price type. * * @param array $value + * * @return bool */ public function isValid($value) diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php index 0f3f8b3389c7d..93c63dcbcab28 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php @@ -3,49 +3,47 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator; use Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing; -use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; +use Magento\CatalogImportExport\Model\Import\Product\StoreResolver; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; +use Magento\Store\Model\Website as WebsiteModel; class Website extends AbstractImportValidator implements RowValidatorInterface { /** - * @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver + * @var StoreResolver */ protected $storeResolver; /** - * @var \Magento\Store\Model\Website + * @var WebsiteModel */ protected $websiteModel; /** - * @param \Magento\CatalogImportExport\Model\Import\Product\StoreResolver $storeResolver - * @param \Magento\Store\Model\Website $websiteModel + * @param StoreResolver $storeResolver + * @param WebsiteModel $websiteModel */ public function __construct( - \Magento\CatalogImportExport\Model\Import\Product\StoreResolver $storeResolver, - \Magento\Store\Model\Website $websiteModel + StoreResolver $storeResolver, + WebsiteModel $websiteModel ) { $this->storeResolver = $storeResolver; $this->websiteModel = $websiteModel; } - /** - * {@inheritdoc} - */ - public function init($context) - { - return parent::init($context); - } - /** * Validate by website type * * @param array $value * @param string $websiteCode + * * @return bool */ protected function isWebsiteValid($value, $websiteCode) @@ -62,7 +60,8 @@ protected function isWebsiteValid($value, $websiteCode) /** * Validate value * - * @param mixed $value + * @param array $value + * * @return bool */ public function isValid($value) @@ -85,6 +84,7 @@ public function isValid($value) */ public function getAllWebsitesValue() { - return AdvancedPricing::VALUE_ALL_WEBSITES . ' ['.$this->websiteModel->getBaseCurrency()->getCurrencyCode().']'; + return AdvancedPricing::VALUE_ALL_WEBSITES . + ' [' . $this->websiteModel->getBaseCurrency()->getCurrencyCode() . ']'; } } 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/AdvancedSearch/Model/Client/ClientResolver.php b/app/code/Magento/AdvancedSearch/Model/Client/ClientResolver.php index 84933c8584bb3..33e99a0a0d0ee 100644 --- a/app/code/Magento/AdvancedSearch/Model/Client/ClientResolver.php +++ b/app/code/Magento/AdvancedSearch/Model/Client/ClientResolver.php @@ -20,7 +20,7 @@ class ClientResolver * * @var ScopeConfigInterface * @since 100.1.0 - * @deprecated since it is not used anymore + * @deprecated 100.3.0 since it is not used anymore */ protected $scopeConfig; @@ -56,14 +56,14 @@ class ClientResolver * * @var string * @since 100.1.0 - * @deprecated since it is not used anymore + * @deprecated 100.3.0 since it is not used anymore */ protected $path; /** * Config Scope * @since 100.1.0 - * @deprecated since it is not used anymore + * @deprecated 100.3.0 since it is not used anymore */ protected $scope; diff --git a/app/code/Magento/Analytics/Model/Connector/Http/ConverterInterface.php b/app/code/Magento/Analytics/Model/Connector/Http/ConverterInterface.php index ddd9fcba21109..f6abb0f1ab2d1 100644 --- a/app/code/Magento/Analytics/Model/Connector/Http/ConverterInterface.php +++ b/app/code/Magento/Analytics/Model/Connector/Http/ConverterInterface.php @@ -9,6 +9,7 @@ * Represents converter interface for http request and response body. * * @api + * @since 100.2.0 */ interface ConverterInterface { @@ -16,6 +17,7 @@ interface ConverterInterface * @param string $body * * @return array + * @since 100.2.0 */ public function fromBody($body); @@ -23,16 +25,19 @@ public function fromBody($body); * @param array $data * * @return string + * @since 100.2.0 */ public function toBody(array $data); /** * @return string + * @since 100.2.0 */ public function getContentTypeHeader(); /** * @return string + * @since 100.3.0 */ public function getContentMediaType(): string; } diff --git a/app/code/Magento/Analytics/Model/ReportWriter.php b/app/code/Magento/Analytics/Model/ReportWriter.php index 7128658947908..d5bd36d068d20 100644 --- a/app/code/Magento/Analytics/Model/ReportWriter.php +++ b/app/code/Magento/Analytics/Model/ReportWriter.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Analytics\Model; use Magento\Analytics\ReportXml\DB\ReportValidator; @@ -10,7 +12,6 @@ /** * Writes reports in files in csv format - * @inheritdoc */ class ReportWriter implements ReportWriterInterface { @@ -54,7 +55,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function write(WriteInterface $directory, $path) { @@ -81,7 +82,7 @@ public function write(WriteInterface $directory, $path) $headers = array_keys($row); $stream->writeCsv($headers); } - $stream->writeCsv($row); + $stream->writeCsv($this->prepareRow($row)); } $stream->unlock(); $stream->close(); @@ -98,4 +99,18 @@ public function write(WriteInterface $directory, $path) return true; } + + /** + * Replace wrong symbols in row + * + * @param array $row + * @return array + */ + private function prepareRow(array $row): array + { + $row = preg_replace('/(?<!\\\\)"/', '\\"', $row); + $row = preg_replace('/[\\\\]+/', '\\', $row); + + return $row; + } } diff --git a/app/code/Magento/Analytics/ReportXml/Query.php b/app/code/Magento/Analytics/ReportXml/Query.php index edf5ed08ee55f..b7c31d4334e20 100644 --- a/app/code/Magento/Analytics/ReportXml/Query.php +++ b/app/code/Magento/Analytics/ReportXml/Query.php @@ -81,7 +81,6 @@ public function getConfig() * @link http://php.net/manual/en/jsonserializable.jsonserialize.php * @return mixed data which can be serialized by <b>json_encode</b>, * which is a value of any type other than a resource. - * @since 5.4.0 */ public function jsonSerialize() { 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/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php index 89595d21a10f0..074e27d75ea3c 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php @@ -59,7 +59,7 @@ class CollectionTimeLabelTest extends TestCase protected function setUp(): void { $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) - ->setMethods(['getComment']) + ->setMethods(['getComment', 'getElementHtml']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php index 0dc2671adf2d7..ac56f2b15fcd4 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php @@ -49,7 +49,7 @@ protected function setUp(): void $this->subscriptionStatusProviderMock = $this->createMock(SubscriptionStatusProvider::class); $this->contextMock = $this->createMock(Context::class); $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) - ->setMethods(['getComment']) + ->setMethods(['getComment', 'getElementHtml']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php index 6e315643ade1f..18b90df630c6b 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php @@ -41,7 +41,7 @@ class VerticalTest extends TestCase protected function setUp(): void { $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) - ->setMethods(['getComment', 'getLabel', 'getHint']) + ->setMethods(['getComment', 'getLabel', 'getHint', 'getElementHtml']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php index b68e26f98a397..8fb135fb4d9ed 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php @@ -12,7 +12,8 @@ use Magento\Analytics\Model\ReportWriter; use Magento\Analytics\ReportXml\DB\ReportValidator; use Magento\Analytics\ReportXml\ReportProvider; -use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Directory\WriteInterface as DirectoryWriteInterface; +use Magento\Framework\Filesystem\File\WriteInterface as FileWriteInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -48,7 +49,7 @@ class ReportWriterTest extends TestCase private $objectManagerHelper; /** - * @var WriteInterface|MockObject + * @var DirectoryWriteInterface|MockObject */ private $directoryMock; @@ -82,7 +83,7 @@ protected function setUp(): void $this->reportValidatorMock = $this->createMock(ReportValidator::class); $this->providerFactoryMock = $this->createMock(ProviderFactory::class); $this->reportProviderMock = $this->createMock(ReportProvider::class); - $this->directoryMock = $this->getMockBuilder(WriteInterface::class) + $this->directoryMock = $this->getMockBuilder(DirectoryWriteInterface::class) ->getMockForAbstractClass(); $this->objectManagerHelper = new ObjectManagerHelper($this); @@ -98,16 +99,15 @@ protected function setUp(): void /** * @param array $configData + * @param array $fileData + * @param array $expectedFileData * @return void * * @dataProvider configDataProvider */ - public function testWrite(array $configData) + public function testWrite(array $configData, array $fileData, array $expectedFileData): void { $errors = []; - $fileData = [ - ['number' => 1, 'type' => 'Shoes Usual'] - ]; $this->configInterfaceMock ->expects($this->once()) ->method('get') @@ -126,7 +126,7 @@ public function testWrite(array $configData) ->with($parameterName ?: null) ->willReturn($fileData); $errorStreamMock = $this->getMockBuilder( - \Magento\Framework\Filesystem\File\WriteInterface::class + FileWriteInterface::class )->getMockForAbstractClass(); $errorStreamMock ->expects($this->once()) @@ -136,8 +136,8 @@ public function testWrite(array $configData) ->expects($this->exactly(2)) ->method('writeCsv') ->withConsecutive( - [array_keys($fileData[0])], - [$fileData[0]] + [array_keys($expectedFileData[0])], + [$expectedFileData[0]] ); $errorStreamMock->expects($this->once())->method('unlock'); $errorStreamMock->expects($this->once())->method('close'); @@ -164,12 +164,12 @@ public function testWrite(array $configData) * * @dataProvider configDataProvider */ - public function testWriteErrorFile($configData) + public function testWriteErrorFile(array $configData): void { $errors = ['orders', 'SQL Error: test']; $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([$configData]); $errorStreamMock = $this->getMockBuilder( - \Magento\Framework\Filesystem\File\WriteInterface::class + FileWriteInterface::class )->getMockForAbstractClass(); $errorStreamMock->expects($this->once())->method('lock'); $errorStreamMock->expects($this->once())->method('writeCsv')->with($errors); @@ -184,7 +184,7 @@ public function testWriteErrorFile($configData) /** * @return void */ - public function testWriteEmptyReports() + public function testWriteEmptyReports(): void { $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([]); $this->reportValidatorMock->expects($this->never())->method('validate'); @@ -195,11 +195,11 @@ public function testWriteEmptyReports() /** * @return array */ - public function configDataProvider() + public function configDataProvider(): array { return [ 'reportProvider' => [ - [ + 'configData' => [ 'providers' => [ [ 'name' => $this->providerName, @@ -209,6 +209,12 @@ public function configDataProvider() ], ] ] + ], + 'fileData' => [ + ['number' => 1, 'type' => 'Shoes\"" Usual\\\\"'] + ], + 'expectedFileData' => [ + ['number' => 1, 'type' => 'Shoes\"\" Usual\\"'] ] ], ]; diff --git a/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php index 76410794900e2..b40fdf19d466f 100644 --- a/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php @@ -14,6 +14,7 @@ * Bulk summary data with list of operations items short data. * * @api + * @since 100.2.3 */ interface BulkStatusInterface extends \Magento\Framework\Bulk\BulkStatusInterface { @@ -23,6 +24,7 @@ interface BulkStatusInterface extends \Magento\Framework\Bulk\BulkStatusInterfac * @param string $bulkUuid * @return \Magento\AsynchronousOperations\Api\Data\DetailedBulkOperationsStatusInterface * @throws \Magento\Framework\Exception\NoSuchEntityException + * @since 100.2.3 */ public function getBulkDetailedStatus($bulkUuid); @@ -32,6 +34,7 @@ public function getBulkDetailedStatus($bulkUuid); * @param string $bulkUuid * @return \Magento\AsynchronousOperations\Api\Data\BulkOperationsStatusInterface * @throws \Magento\Framework\Exception\NoSuchEntityException + * @since 100.2.3 */ public function getBulkShortStatus($bulkUuid); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php index c7edd5c8ff9cd..c0390e40899e8 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php @@ -13,6 +13,7 @@ * Temporary data object to give response from webapi async router * * @api + * @since 100.2.3 */ interface AsyncResponseInterface { @@ -24,6 +25,7 @@ interface AsyncResponseInterface * Gets the bulk uuid. * * @return string Bulk Uuid. + * @since 100.2.3 */ public function getBulkUuid(); @@ -32,6 +34,7 @@ public function getBulkUuid(); * * @param string $bulkUuid * @return $this + * @since 100.2.3 */ public function setBulkUuid($bulkUuid); @@ -39,6 +42,7 @@ public function setBulkUuid($bulkUuid); * Gets the list of request items with status data. * * @return \Magento\AsynchronousOperations\Api\Data\ItemStatusInterface[] + * @since 100.2.3 */ public function getRequestItems(); @@ -47,12 +51,14 @@ public function getRequestItems(); * * @param \Magento\AsynchronousOperations\Api\Data\ItemStatusInterface[] $requestItems * @return $this + * @since 100.2.3 */ public function setRequestItems($requestItems); /** * @param bool $isErrors * @return $this + * @since 100.2.3 */ public function setErrors($isErrors = false); @@ -60,6 +66,7 @@ public function setErrors($isErrors = false); * Is there errors during processing bulk * * @return boolean + * @since 100.2.3 */ public function isErrors(); @@ -67,6 +74,7 @@ public function isErrors(); * Retrieve existing extension attributes object. * * @return \Magento\AsynchronousOperations\Api\Data\AsyncResponseExtensionInterface|null + * @since 100.2.3 */ public function getExtensionAttributes(); @@ -75,6 +83,7 @@ public function getExtensionAttributes(); * * @param \Magento\AsynchronousOperations\Api\Data\AsyncResponseExtensionInterface $extensionAttributes * @return $this + * @since 100.2.3 */ public function setExtensionAttributes( \Magento\AsynchronousOperations\Api\Data\AsyncResponseExtensionInterface $extensionAttributes diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php index f8b7e389d387d..5fedf675e5579 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php @@ -14,6 +14,7 @@ * Bulk summary data with list of operations items summary data. * * @api + * @since 100.2.3 */ interface BulkOperationsStatusInterface extends BulkSummaryInterface { @@ -24,6 +25,7 @@ interface BulkOperationsStatusInterface extends BulkSummaryInterface * Retrieve list of operation with statuses (short data). * * @return \Magento\AsynchronousOperations\Api\Data\SummaryOperationStatusInterface[] + * @since 100.2.3 */ public function getOperationsList(); @@ -32,6 +34,7 @@ public function getOperationsList(); * * @param \Magento\AsynchronousOperations\Api\Data\SummaryOperationStatusInterface[] $operationStatusList * @return $this + * @since 100.2.3 */ public function setOperationsList($operationStatusList); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php index a433ec0953a83..5e2cff0b6da3d 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php @@ -38,6 +38,7 @@ public function setExtensionAttributes( * Get user type * * @return int + * @since 100.3.0 */ public function getUserType(); @@ -46,6 +47,7 @@ public function getUserType(); * * @param int $userType * @return $this + * @since 100.3.0 */ public function setUserType($userType); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php index 6e39177630857..62bead9f9956e 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php @@ -14,6 +14,7 @@ * Bulk summary data with list of operations items full data. * * @api + * @since 100.2.3 */ interface DetailedBulkOperationsStatusInterface extends BulkSummaryInterface { @@ -24,6 +25,7 @@ interface DetailedBulkOperationsStatusInterface extends BulkSummaryInterface * Retrieve operations list. * * @return \Magento\AsynchronousOperations\Api\Data\OperationInterface[] + * @since 100.2.3 */ public function getOperationsList(); @@ -32,6 +34,7 @@ public function getOperationsList(); * * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface[] $operationStatusList * @return $this + * @since 100.2.3 */ public function setOperationsList($operationStatusList); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php index 3294078c2c1ea..8919e87c55bec 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php @@ -14,6 +14,7 @@ * Indicate if entity param was Accepted|Rejected to bulk schedule * * @api + * @since 100.2.3 */ interface ItemStatusInterface { @@ -30,6 +31,7 @@ interface ItemStatusInterface * Get entity Id. * * @return int + * @since 100.2.3 */ public function getId(); @@ -38,6 +40,7 @@ public function getId(); * * @param int $entityId * @return $this + * @since 100.2.3 */ public function setId($entityId); @@ -45,6 +48,7 @@ public function setId($entityId); * Get hash of entity data. * * @return string md5 hash of entity params array. + * @since 100.2.3 */ public function getDataHash(); @@ -53,6 +57,7 @@ public function getDataHash(); * * @param string $hash md5 hash of entity params array. * @return $this + * @since 100.2.3 */ public function setDataHash($hash); @@ -60,6 +65,7 @@ public function setDataHash($hash); * Get status. * * @return string accepted|rejected + * @since 100.2.3 */ public function getStatus(); @@ -68,6 +74,7 @@ public function getStatus(); * * @param string $status accepted|rejected * @return $this + * @since 100.2.3 */ public function setStatus($status = self::STATUS_ACCEPTED); @@ -75,6 +82,7 @@ public function setStatus($status = self::STATUS_ACCEPTED); * Get error information. * * @return string|null + * @since 100.2.3 */ public function getErrorMessage(); @@ -83,6 +91,7 @@ public function getErrorMessage(); * * @param string|null|\Exception $error * @return $this + * @since 100.2.3 */ public function setErrorMessage($error = null); @@ -90,6 +99,7 @@ public function setErrorMessage($error = null); * Get error code. * * @return int|null + * @since 100.2.3 */ public function getErrorCode(); @@ -98,6 +108,7 @@ public function getErrorCode(); * * @param int|null|\Exception $errorCode Default: null * @return $this + * @since 100.2.3 */ public function setErrorCode($errorCode = null); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php index c3d221b7ef4f8..f8e1457366777 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php @@ -13,6 +13,7 @@ * * An bulk is a group of queue messages. An bulk operation item is a queue message. * @api + * @since 100.3.0 */ interface OperationSearchResultsInterface extends \Magento\Framework\Api\SearchResultsInterface { @@ -20,6 +21,7 @@ interface OperationSearchResultsInterface extends \Magento\Framework\Api\SearchR * Get list of operations. * * @return \Magento\AsynchronousOperations\Api\Data\OperationInterface[] + * @since 100.3.0 */ public function getItems(); @@ -28,6 +30,7 @@ public function getItems(); * * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface[] $items * @return $this + * @since 100.3.0 */ public function setItems(array $items); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php index 3b9f53b34162a..051dbd955c4a9 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php @@ -15,6 +15,7 @@ * without serialized_data and result_serialized_data * * @api + * @since 100.2.3 */ interface SummaryOperationStatusInterface { @@ -22,6 +23,7 @@ interface SummaryOperationStatusInterface * Operation id * * @return int + * @since 100.2.3 */ public function getId(); @@ -31,6 +33,7 @@ public function getId(); * OPEN | COMPLETE | RETRIABLY_FAILED | NOT_RETRIABLY_FAILED * * @return int + * @since 100.2.3 */ public function getStatus(); @@ -38,6 +41,7 @@ public function getStatus(); * Get result message * * @return string + * @since 100.2.3 */ public function getResultMessage(); @@ -45,6 +49,7 @@ public function getResultMessage(); * Get error code * * @return int + * @since 100.2.3 */ public function getErrorCode(); } diff --git a/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php b/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php index 17547321b827f..6cb6a93143918 100644 --- a/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php @@ -13,6 +13,7 @@ * * An bulk is a group of queue messages. An bulk operation item is a queue message. * @api + * @since 100.3.0 */ interface OperationRepositoryInterface { @@ -21,6 +22,7 @@ interface OperationRepositoryInterface * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \Magento\AsynchronousOperations\Api\Data\OperationSearchResultsInterface + * @since 100.3.0 */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria); } diff --git a/app/code/Magento/AsynchronousOperations/Api/SaveMultipleOperationsInterface.php b/app/code/Magento/AsynchronousOperations/Api/SaveMultipleOperationsInterface.php index 8563ab6541a0c..12abdc04bb165 100644 --- a/app/code/Magento/AsynchronousOperations/Api/SaveMultipleOperationsInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/SaveMultipleOperationsInterface.php @@ -14,6 +14,7 @@ * Interface for saving multiple operations * * @api + * @since 100.4.0 */ interface SaveMultipleOperationsInterface { @@ -22,6 +23,7 @@ interface SaveMultipleOperationsInterface * * @param OperationInterface[] $operations * @return void + * @since 100.4.0 */ public function execute(array $operations): void; } diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php index b47bb26985df0..6cf0611eb28ec 100644 --- a/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php +++ b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php @@ -140,8 +140,8 @@ public function scheduleBulk($bulkUuid, array $operations, $description, $userId public function retryBulk($bulkUuid, array $errorCodes) { $metadata = $this->metadataPool->getMetadata(BulkSummaryInterface::class); - $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); + $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); /** @var \Magento\AsynchronousOperations\Model\ResourceModel\Operation[] $retriablyFailedOperations */ $retriablyFailedOperations = $this->operationCollectionFactory->create() ->addFieldToFilter('error_code', ['in' => $errorCodes]) @@ -157,23 +157,27 @@ public function retryBulk($bulkUuid, array $errorCodes) /** @var OperationInterface $operation */ foreach ($retriablyFailedOperations as $operation) { if ($currentBatchSize === $maxBatchSize) { + $whereCondition = $connection->quoteInto('operation_key IN (?)', $operationIds) + . " AND " + . $connection->quoteInto('bulk_uuid = ?', $bulkUuid); $connection->delete( $this->resourceConnection->getTableName('magento_operation'), - $connection->quoteInto('id IN (?)', $operationIds) + $whereCondition ); $operationIds = []; $currentBatchSize = 0; } $currentBatchSize++; $operationIds[] = $operation->getId(); - // Rescheduled operations must be put in queue in 'open' state (i.e. without ID) - $operation->setId(null); } // remove operations from the last batch if (!empty($operationIds)) { + $whereCondition = $connection->quoteInto('operation_key IN (?)', $operationIds) + . " AND " + . $connection->quoteInto('bulk_uuid = ?', $bulkUuid); $connection->delete( $this->resourceConnection->getTableName('magento_operation'), - $connection->quoteInto('id IN (?)', $operationIds) + $whereCondition ); } diff --git a/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php b/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php index de0f89a71650a..593ab52bbdf29 100644 --- a/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php +++ b/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php @@ -15,6 +15,7 @@ * Class for accessing to Webapi_Async configuration. * * @api + * @since 100.2.3 */ interface ConfigInterface { @@ -45,6 +46,7 @@ interface ConfigInterface * Get array of generated topics name and related to this topic service class and methods * * @return array + * @since 100.2.3 */ public function getServices(); @@ -55,6 +57,7 @@ public function getServices(); * @param string $httpMethod GET|POST|PUT|DELETE * @return string * @throws \Magento\Framework\Exception\LocalizedException + * @since 100.2.3 */ public function getTopicName($routeUrl, $httpMethod); } diff --git a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php index e2f756a9e8fcd..4d83c03507f9c 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php @@ -69,11 +69,19 @@ public function process($maxNumberOfMessages = null) $this->registry->register('isSecureArea', true, true); $queue = $this->configuration->getQueue(); + $maxIdleTime = $this->configuration->getMaxIdleTime(); + $sleep = $this->configuration->getSleep(); if (!isset($maxNumberOfMessages)) { $queue->subscribe($this->getTransactionCallback($queue)); } else { - $this->invoker->invoke($queue, $maxNumberOfMessages, $this->getTransactionCallback($queue)); + $this->invoker->invoke( + $queue, + $maxNumberOfMessages, + $this->getTransactionCallback($queue), + $maxIdleTime, + $sleep + ); } $this->registry->unregister('isSecureArea'); diff --git a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php index 4dcaf7279a570..d8efed5562131 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php @@ -11,7 +11,6 @@ use Magento\AsynchronousOperations\Api\Data\AsyncResponseInterfaceFactory; use Magento\AsynchronousOperations\Api\Data\ItemStatusInterface; use Magento\AsynchronousOperations\Api\Data\ItemStatusInterfaceFactory; -use Magento\AsynchronousOperations\Model\ResourceModel\Operation\OperationRepository; use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\Bulk\BulkManagementInterface; use Magento\Framework\DataObject\IdentityGeneratorInterface; @@ -144,7 +143,6 @@ public function publishMass($topicName, array $entitiesArray, $groupId = null, $ foreach ($entitiesArray as $key => $entityParams) { /** @var \Magento\AsynchronousOperations\Api\Data\ItemStatusInterface $requestItem */ $requestItem = $this->itemStatusInterfaceFactory->create(); - try { $operation = $this->operationRepository->create($topicName, $entityParams, $groupId, $key); $operations[] = $operation; diff --git a/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php b/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php index 74740cba9a6d8..7575257555fae 100644 --- a/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php +++ b/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php @@ -7,17 +7,19 @@ namespace Magento\AsynchronousOperations\Model; use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; -use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\App\ResourceConnection; +use Psr\Log\LoggerInterface; +use Magento\Framework\Bulk\OperationManagementInterface; /** * Class for managing Bulk Operations */ -class OperationManagement implements \Magento\Framework\Bulk\OperationManagementInterface +class OperationManagement implements OperationManagementInterface { /** - * @var EntityManager + * @var ResourceConnection */ - private $entityManager; + private $connection; /** * @var OperationInterfaceFactory @@ -32,25 +34,26 @@ class OperationManagement implements \Magento\Framework\Bulk\OperationManagement /** * OperationManagement constructor. * - * @param EntityManager $entityManager * @param OperationInterfaceFactory $operationFactory - * @param \Psr\Log\LoggerInterface $logger + * @param LoggerInterface $logger + * @param ResourceConnection $connection */ public function __construct( - EntityManager $entityManager, OperationInterfaceFactory $operationFactory, - \Psr\Log\LoggerInterface $logger + LoggerInterface $logger, + ResourceConnection $connection ) { - $this->entityManager = $entityManager; $this->operationFactory = $operationFactory; $this->logger = $logger; + $this->connection = $connection; } /** * @inheritDoc */ public function changeOperationStatus( - $operationId, + $bulkUuid, + $operationKey, $status, $errorCode = null, $message = null, @@ -58,14 +61,17 @@ public function changeOperationStatus( $resultData = null ) { try { - $operationEntity = $this->operationFactory->create(); - $this->entityManager->load($operationEntity, $operationId); - $operationEntity->setErrorCode($errorCode); - $operationEntity->setStatus($status); - $operationEntity->setResultMessage($message); - $operationEntity->setSerializedData($data); - $operationEntity->setResultSerializedData($resultData); - $this->entityManager->save($operationEntity); + $connection = $this->connection->getConnection(); + $table = $this->connection->getTableName('magento_operation'); + $bind = [ + 'error_code' => $errorCode, + 'status' => $status, + 'result_message' => $message, + 'serialized_data' => $data, + 'result_serialized_data' => $resultData + ]; + $where = ['bulk_uuid = ?' => $bulkUuid, 'operation_key = ?' => $operationKey]; + $connection->update($table, $bind, $where); } catch (\Exception $exception) { $this->logger->critical($exception->getMessage()); return false; diff --git a/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php b/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php index 453f786bdf47b..5c5619a4b41d1 100644 --- a/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php +++ b/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php @@ -163,6 +163,7 @@ public function process(string $encodedMessage) $serializedData = (isset($errorCode)) ? $operation->getSerializedData() : null; $this->operationManagement->changeOperationStatus( + $operation->getBulkUuid(), $operation->getId(), $status, $errorCode, diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php index 0eaa5315af614..b5c33af1470f3 100644 --- a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php @@ -6,10 +6,12 @@ namespace Magento\AsynchronousOperations\Model\ResourceModel; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; + /** * Resource class for Bulk Operations */ -class Operation extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Operation extends AbstractDb { public const TABLE_NAME = "magento_operation"; diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php index b189d81d31636..6757b0c8f0a5c 100644 --- a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\AsynchronousOperations\Model\ResourceModel\Operation; @@ -11,6 +10,7 @@ use Magento\AsynchronousOperations\Api\Data\OperationInterface; use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; use Magento\AsynchronousOperations\Model\OperationRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\MessageQueue\MessageValidator; use Magento\Framework\MessageQueue\MessageEncoder; use Magento\Framework\Serialize\Serializer\Json; @@ -73,11 +73,13 @@ public function __construct( * @param string $topicName * @param array $entityParams * @param string $groupId + * @param string $operationId * @return OperationInterface - * @deprecated No longer used. + * @throws LocalizedException + * @deprecated 100.4.0 No longer used. * @see create() */ - public function createByTopic($topicName, $entityParams, $groupId) + public function createByTopic($topicName, $entityParams, $groupId, $operationId) { $this->messageValidator->validate($topicName, $entityParams); $encodedMessage = $this->messageEncoder->encode($topicName, $entityParams); @@ -89,10 +91,11 @@ public function createByTopic($topicName, $entityParams, $groupId) ]; $data = [ 'data' => [ - OperationInterface::BULK_ID => $groupId, - OperationInterface::TOPIC_NAME => $topicName, + OperationInterface::ID => $operationId, + OperationInterface::BULK_ID => $groupId, + OperationInterface::TOPIC_NAME => $topicName, OperationInterface::SERIALIZED_DATA => $this->jsonSerializer->serialize($serializedData), - OperationInterface::STATUS => OperationInterface::STATUS_TYPE_OPEN, + OperationInterface::STATUS => OperationInterface::STATUS_TYPE_OPEN, ], ]; @@ -103,9 +106,11 @@ public function createByTopic($topicName, $entityParams, $groupId) /** * @inheritDoc + * + * @throws LocalizedException */ public function create($topicName, $entityParams, $groupId, $operationId): OperationInterface { - return $this->createByTopic($topicName, $entityParams, $groupId); + return $this->createByTopic($topicName, $entityParams, $groupId, $operationId); } } diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php index 724871f216472..14abb41c77fc4 100644 --- a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php @@ -196,7 +196,7 @@ public function testRetryBulk() $bulkUuid = 'bulk-001'; $errorCodes = ['errorCode']; $connectionName = 'default'; - $operationId = 1; + $operationId = 0; $operationTable = 'magento_operation'; $topicName = 'topic.name'; $metadata = $this->getMockForAbstractClass(EntityMetadataInterface::class); @@ -216,13 +216,20 @@ public function testRetryBulk() $operationCollection->expects($this->once())->method('getItems')->willReturn([$operation]); $connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); $operation->expects($this->once())->method('getId')->willReturn($operationId); - $operation->expects($this->once())->method('setId')->with(null)->willReturnSelf(); $this->resourceConnection->expects($this->once()) ->method('getTableName')->with($operationTable)->willReturn($operationTable); + $connection->expects($this->at(1)) + ->method('quoteInto') + ->with('operation_key IN (?)', [$operationId]) + ->willReturn('operation_key IN (' . $operationId . ')'); + $connection->expects($this->at(2)) + ->method('quoteInto') + ->with('bulk_uuid = ?', $bulkUuid) + ->willReturn("bulk_uuid = '$bulkUuid'"); $connection->expects($this->once()) - ->method('quoteInto')->with('id IN (?)', [$operationId])->willReturn('id IN (' . $operationId . ')'); - $connection->expects($this->once()) - ->method('delete')->with($operationTable, 'id IN (' . $operationId . ')')->willReturn(1); + ->method('delete') + ->with($operationTable, 'operation_key IN (' . $operationId . ') AND bulk_uuid = \'' . $bulkUuid . '\'') + ->willReturn(1); $connection->expects($this->once())->method('commit')->willReturnSelf(); $operation->expects($this->once())->method('getTopicName')->willReturn($topicName); $this->publisher->expects($this->once())->method('publish')->with($topicName, [$operation])->willReturn(null); @@ -239,7 +246,7 @@ public function testRetryBulkWithException() $bulkUuid = 'bulk-001'; $errorCodes = ['errorCode']; $connectionName = 'default'; - $operationId = 1; + $operationId = 0; $operationTable = 'magento_operation'; $exceptionMessage = 'Exception message'; $metadata = $this->getMockForAbstractClass(EntityMetadataInterface::class); @@ -259,13 +266,19 @@ public function testRetryBulkWithException() $operationCollection->expects($this->once())->method('getItems')->willReturn([$operation]); $connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); $operation->expects($this->once())->method('getId')->willReturn($operationId); - $operation->expects($this->once())->method('setId')->with(null)->willReturnSelf(); $this->resourceConnection->expects($this->once()) ->method('getTableName')->with($operationTable)->willReturn($operationTable); + $connection->expects($this->at(1)) + ->method('quoteInto') + ->with('operation_key IN (?)', [$operationId]) + ->willReturn('operation_key IN (' . $operationId . ')'); + $connection->expects($this->at(2)) + ->method('quoteInto') + ->with('bulk_uuid = ?', $bulkUuid) + ->willReturn("bulk_uuid = '$bulkUuid'"); $connection->expects($this->once()) - ->method('quoteInto')->with('id IN (?)', [$operationId])->willReturn('id IN (' . $operationId . ')'); - $connection->expects($this->once()) - ->method('delete')->with($operationTable, 'id IN (' . $operationId . ')') + ->method('delete') + ->with($operationTable, 'operation_key IN (' . $operationId . ') AND bulk_uuid = \'' . $bulkUuid . '\'') ->willThrowException(new \Exception($exceptionMessage)); $connection->expects($this->once())->method('rollBack')->willReturnSelf(); $this->logger->expects($this->once())->method('critical')->with($exceptionMessage); diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php index 476bad2d0ee04..0f437cefd3fca 100644 --- a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php @@ -7,11 +7,10 @@ namespace Magento\AsynchronousOperations\Test\Unit\Model; -use Magento\AsynchronousOperations\Api\Data\OperationInterface; use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; use Magento\AsynchronousOperations\Model\OperationManagement; -use Magento\Framework\EntityManager\EntityManager; -use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -24,79 +23,116 @@ class OperationManagementTest extends TestCase private $model; /** - * @var MockObject - */ - private $entityManagerMock; - - /** - * @var MockObject + * @var OperationInterfaceFactory|MockObject */ private $operationFactoryMock; /** - * @var MockObject - */ - private $operationMock; - - /** - * @var MockObject + * @var LoggerInterface|MockObject */ private $loggerMock; + /** - * @var MetadataPool|MockObject + * @var ResourceConnection|MockObject */ - private $metadataPoolMock; + private $resourceConnectionMock; protected function setUp(): void { - $this->entityManagerMock = $this->createMock(EntityManager::class); - $this->metadataPoolMock = $this->createMock(MetadataPool::class); $this->operationFactoryMock = $this->createPartialMock( OperationInterfaceFactory::class, ['create'] ); - $this->operationMock = - $this->getMockForAbstractClass(OperationInterface::class); $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->setMethods(['getConnection', 'getTableName']) + ->getMock(); + $this->model = new OperationManagement( - $this->entityManagerMock, $this->operationFactoryMock, - $this->loggerMock + $this->loggerMock, + $this->resourceConnectionMock ); } + /** + * Test change operation status. + */ public function testChangeOperationStatus() { - $operationId = 1; + $operationKey = 1; $status = 1; $message = 'Message'; $data = 'data'; $errorCode = 101; - $this->operationFactoryMock->expects($this->once())->method('create')->willReturn($this->operationMock); - $this->entityManagerMock->expects($this->once())->method('load')->with($this->operationMock, $operationId); - $this->operationMock->expects($this->once())->method('setStatus')->with($status)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setResultMessage')->with($message)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setSerializedData')->with($data)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setErrorCode')->with($errorCode)->willReturnSelf(); - $this->entityManagerMock->expects($this->once())->method('save')->with($this->operationMock); - $this->assertTrue($this->model->changeOperationStatus($operationId, $status, $errorCode, $message, $data)); + $bulkUuid = '13f85e88-be1d-4ce7-8570-88637a589930'; + + $tableName = 'magento_operation'; + + $bind = [ + 'error_code' => $errorCode, + 'status' => $status, + 'result_message' => $message, + 'serialized_data' => $data, + 'result_serialized_data' => '' + ]; + $where = ['bulk_uuid = ?' => $bulkUuid, 'operation_key = ?' => $operationKey]; + + $connection = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->resourceConnectionMock->expects($this->atLeastOnce()) + ->method('getConnection')->with('default') + ->willReturn($connection); + $this->resourceConnectionMock->expects($this->once())->method('getTableName')->with($tableName) + ->willReturn($tableName); + + $connection->expects($this->once())->method('update')->with($tableName, $bind, $where) + ->willReturn(1); + $this->assertTrue( + $this->model->changeOperationStatus($bulkUuid, $operationKey, $status, $errorCode, $message, $data) + ); } + /** + * Test generic exception throw case. + */ public function testChangeOperationStatusIfExceptionWasThrown() { - $operationId = 1; + $operationKey = 1; $status = 1; $message = 'Message'; $data = 'data'; $errorCode = 101; - $this->operationFactoryMock->expects($this->once())->method('create')->willReturn($this->operationMock); - $this->entityManagerMock->expects($this->once())->method('load')->with($this->operationMock, $operationId); - $this->operationMock->expects($this->once())->method('setStatus')->with($status)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setResultMessage')->with($message)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setSerializedData')->with($data)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setErrorCode')->with($errorCode)->willReturnSelf(); - $this->entityManagerMock->expects($this->once())->method('save')->willThrowException(new \Exception()); + $bulkUuid = '13f85e88-be1d-4ce7-8570-88637a589930'; + + $tableName = 'magento_operation'; + + $bind = [ + 'error_code' => $errorCode, + 'status' => $status, + 'result_message' => $message, + 'serialized_data' => $data, + 'result_serialized_data' => '' + ]; + $where = ['bulk_uuid = ?' => $bulkUuid, 'operation_key = ?' => $operationKey]; + + $connection = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->resourceConnectionMock->expects($this->atLeastOnce()) + ->method('getConnection')->with('default') + ->willReturn($connection); + $this->resourceConnectionMock->expects($this->once()) + ->method('getTableName')->with($tableName) + ->willReturn($tableName); + + $connection->expects($this->once())->method('update')->with($tableName, $bind, $where) + ->willThrowException(new \Exception()); $this->loggerMock->expects($this->once())->method('critical'); - $this->assertFalse($this->model->changeOperationStatus($operationId, $status, $errorCode, $message, $data)); + $this->assertFalse( + $this->model->changeOperationStatus($bulkUuid, $operationKey, $status, $errorCode, $message, $data) + ); } } diff --git a/app/code/Magento/AsynchronousOperations/etc/db_schema.xml b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml index f287a368c72fb..5d49d71ee46b0 100644 --- a/app/code/Magento/AsynchronousOperations/etc/db_schema.xml +++ b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml @@ -9,15 +9,15 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="magento_bulk" resource="default" engine="innodb" comment="Bulk entity that represents set of related asynchronous operations"> - <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Bulk Internal ID (must not be exposed)"/> <column xsi:type="varbinary" name="uuid" nullable="true" length="39" comment="Bulk UUID (can be exposed to reference bulk entity)"/> - <column xsi:type="int" name="user_id" unsigned="true" nullable="true" identity="false" + <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="true" identity="false" comment="ID of the WebAPI user that performed an action"/> <column xsi:type="int" name="user_type" nullable="true" comment="Which type of user"/> <column xsi:type="varchar" name="description" nullable="true" length="255" comment="Bulk Description"/> - <column xsi:type="int" name="operation_count" unsigned="true" nullable="false" identity="false" + <column xsi:type="int" name="operation_count" padding="10" unsigned="true" nullable="false" identity="false" comment="Total number of operations scheduled within this bulk"/> <column xsi:type="timestamp" name="start_time" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Bulk start time"/> @@ -32,8 +32,10 @@ </index> </table> <table name="magento_operation" resource="default" engine="innodb" comment="Operation entity"> - <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Operation ID"/> + <column xsi:type="int" name="operation_key" padding="10" unsigned="true" nullable="false" + comment="Operation Key"/> <column xsi:type="varbinary" name="bulk_uuid" nullable="true" length="39" comment="Related Bulk UUID"/> <column xsi:type="varchar" name="topic_name" nullable="true" length="255" comment="Name of the related message queue topic"/> @@ -41,9 +43,9 @@ comment="Data (serialized) required to perform an operation"/> <column xsi:type="blob" name="result_serialized_data" nullable="true" comment="Result data (serialized) after perform an operation"/> - <column xsi:type="smallint" name="status" unsigned="false" nullable="true" identity="false" + <column xsi:type="smallint" name="status" padding="6" unsigned="false" nullable="true" identity="false" default="0" comment="Operation status (OPEN | COMPLETE | RETRIABLY_FAILED | NOT_RETRIABLY_FAILED)"/> - <column xsi:type="smallint" name="error_code" unsigned="false" nullable="true" identity="false" + <column xsi:type="smallint" name="error_code" padding="6" unsigned="false" nullable="true" identity="false" comment="Code of the error that appeared during operation execution (used to aggregate related failed operations)"/> <column xsi:type="varchar" name="result_message" nullable="true" length="255" comment="Operation result message"/> @@ -59,7 +61,7 @@ </table> <table name="magento_acknowledged_bulk" resource="default" engine="innodb" comment="Bulk that was viewed by user from notification area"> - <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Internal ID"/> <column xsi:type="varbinary" name="bulk_uuid" nullable="true" length="39" comment="Related Bulk UUID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json index 9b6c0709e1916..6cbb3c664a50f 100644 --- a/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json +++ b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json @@ -22,6 +22,7 @@ "magento_operation": { "column": { "id": true, + "operation_key": true, "bulk_uuid": true, "topic_name": true, "serialized_data": true, @@ -35,7 +36,8 @@ }, "constraint": { "PRIMARY": true, - "MAGENTO_OPERATION_BULK_UUID_MAGENTO_BULK_UUID": true + "MAGENTO_OPERATION_BULK_UUID_MAGENTO_BULK_UUID": true, + "UUID": true } }, "magento_acknowledged_bulk": { @@ -49,4 +51,4 @@ "MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/Authorization/Test/Unit/Model/ResourceModel/RulesTest.php b/app/code/Magento/Authorization/Test/Unit/Model/ResourceModel/RulesTest.php index 6560b3be3b947..cbd9012e1a80d 100644 --- a/app/code/Magento/Authorization/Test/Unit/Model/ResourceModel/RulesTest.php +++ b/app/code/Magento/Authorization/Test/Unit/Model/ResourceModel/RulesTest.php @@ -182,7 +182,7 @@ public function testSaveRelNoResources() /** * Test LocalizedException throw case. */ - public function testLocalizedExceptionOccurance() + public function testLocalizedExceptionOccurrence() { $this->expectException(LocalizedException::class); $this->expectExceptionMessage("TestException"); @@ -212,7 +212,7 @@ public function testLocalizedExceptionOccurance() /** * Test generic exception throw case. */ - public function testGenericExceptionOccurance() + public function testGenericExceptionOccurrence() { $exception = new \Exception('GenericException'); diff --git a/app/code/Magento/Backend/App/AbstractAction.php b/app/code/Magento/Backend/App/AbstractAction.php index 2f01700bdf51c..0e0b34f168c05 100644 --- a/app/code/Magento/Backend/App/AbstractAction.php +++ b/app/code/Magento/Backend/App/AbstractAction.php @@ -16,11 +16,12 @@ use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\View\Element\AbstractBlock; +use Magento\Framework\Encryption\Helper\Security; /** * Generic backend controller * - * @deprecated Use \Magento\Framework\App\ActionInterface + * @deprecated 102.0.0 Use \Magento\Framework\App\ActionInterface * * phpcs:disable Magento2.Classes.AbstractApi * @api @@ -386,7 +387,7 @@ protected function _validateSecretKey() } $secretKey = $this->getRequest()->getParam(UrlInterface::SECRET_KEY_PARAM_NAME, null); - if (!$secretKey || $secretKey != $this->_backendUrl->getSecretKey()) { + if (!$secretKey || !Security::compareStrings($secretKey, $this->_backendUrl->getSecretKey())) { return false; } return true; diff --git a/app/code/Magento/Backend/Block/AbstractBlock.php b/app/code/Magento/Backend/Block/AbstractBlock.php index fc91f99e3dbaf..bfac54f8c555c 100644 --- a/app/code/Magento/Backend/Block/AbstractBlock.php +++ b/app/code/Magento/Backend/Block/AbstractBlock.php @@ -22,10 +22,10 @@ class AbstractBlock extends \Magento\Framework\View\Element\AbstractBlock protected $_authorization; /** - * @param \Magento\Backend\Block\Context $context + * @param Context $context * @param array $data */ - public function __construct(\Magento\Backend\Block\Context $context, array $data = []) + public function __construct(Context $context, array $data = []) { $this->_authorization = $context->getAuthorization(); parent::__construct($context, $data); diff --git a/app/code/Magento/Backend/Block/Dashboard.php b/app/code/Magento/Backend/Block/Dashboard.php index 28d3eeae9a1c6..511e393610b1e 100644 --- a/app/code/Magento/Backend/Block/Dashboard.php +++ b/app/code/Magento/Backend/Block/Dashboard.php @@ -9,7 +9,7 @@ /** * Class used to initialize layout for MBO Dashboard - * @deprecated dashboard graphs were migrated to dynamic chart.js solution + * @deprecated 102.0.0 dashboard graphs were migrated to dynamic chart.js solution * @see dashboard in adminhtml_dashboard_index.xml * * @api diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php index db95a64636c3a..7811ee948f763 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Graph.php +++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php @@ -77,7 +77,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard /** * Google chart api data encoding * - * @deprecated since the Google Image Charts API not accessible from March 14, 2019 + * @deprecated 101.0.2 since the Google Image Charts API not accessible from March 14, 2019 * @var string */ protected $_encoding = 'e'; diff --git a/app/code/Magento/Backend/Block/Dashboard/Grids.php b/app/code/Magento/Backend/Block/Dashboard/Grids.php index f40aaaf33fed7..9820d8b868d86 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Grids.php +++ b/app/code/Magento/Backend/Block/Dashboard/Grids.php @@ -15,6 +15,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Grids extends Tabs { diff --git a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php index 8b3574e223236..dd21a215ea6fe 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php +++ b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php @@ -16,6 +16,7 @@ * @api * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.DepthOfInheritance) + * @since 100.0.2 */ class Grid extends \Magento\Backend\Block\Dashboard\Grid { diff --git a/app/code/Magento/Backend/Block/Dashboard/Sales.php b/app/code/Magento/Backend/Block/Dashboard/Sales.php index ebe0932c3fa3b..098580b1369e9 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Sales.php +++ b/app/code/Magento/Backend/Block/Dashboard/Sales.php @@ -16,6 +16,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Sales extends Bar { diff --git a/app/code/Magento/Backend/Block/Dashboard/Totals.php b/app/code/Magento/Backend/Block/Dashboard/Totals.php index 7da109c2fb602..73e6bc1ab9e8a 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Totals.php +++ b/app/code/Magento/Backend/Block/Dashboard/Totals.php @@ -17,6 +17,7 @@ /** * Adminhtml dashboard totals bar * @api + * @since 100.0.2 */ class Totals extends Bar { diff --git a/app/code/Magento/Backend/Block/Media/Uploader.php b/app/code/Magento/Backend/Block/Media/Uploader.php index e95b6397cd244..40cc68e04bf51 100644 --- a/app/code/Magento/Backend/Block/Media/Uploader.php +++ b/app/code/Magento/Backend/Block/Media/Uploader.php @@ -46,7 +46,7 @@ class Uploader extends \Magento\Backend\Block\Widget /** * @var UploadConfigInterface - * @deprecated + * @deprecated 101.0.1 * @see \Magento\Backend\Model\Image\UploadResizeConfigInterface */ private $imageConfig; @@ -120,6 +120,7 @@ public function getFileSizeService() * Get Image Upload Maximum Width Config. * * @return int + * @since 100.2.7 */ public function getImageUploadMaxWidth() { @@ -130,6 +131,7 @@ public function getImageUploadMaxWidth() * Get Image Upload Maximum Height Config. * * @return int + * @since 100.2.7 */ public function getImageUploadMaxHeight() { diff --git a/app/code/Magento/Backend/Block/Page/Footer.php b/app/code/Magento/Backend/Block/Page/Footer.php index e0c173a4cbfec..610d28b0f53e3 100644 --- a/app/code/Magento/Backend/Block/Page/Footer.php +++ b/app/code/Magento/Backend/Block/Page/Footer.php @@ -60,6 +60,7 @@ public function getMagentoVersion() /** * @inheritdoc + * @since 101.0.0 */ protected function getCacheLifetime() { diff --git a/app/code/Magento/Backend/Block/Page/System/Config/Robots/Reset.php b/app/code/Magento/Backend/Block/Page/System/Config/Robots/Reset.php index 2abb987db0723..d290b89b2a6bc 100644 --- a/app/code/Magento/Backend/Block/Page/System/Config/Robots/Reset.php +++ b/app/code/Magento/Backend/Block/Page/System/Config/Robots/Reset.php @@ -11,7 +11,7 @@ /** * "Reset to Defaults" button renderer * - * @deprecated 100.2.0 + * @deprecated 100.1.6 * @author Magento Core Team <core@magentocommerce.com> */ class Reset extends \Magento\Config\Block\System\Config\Form\Field diff --git a/app/code/Magento/Backend/Block/Template.php b/app/code/Magento/Backend/Block/Template.php index 3ae4451a2592f..b4c41645d7f65 100644 --- a/app/code/Magento/Backend/Block/Template.php +++ b/app/code/Magento/Backend/Block/Template.php @@ -8,6 +8,10 @@ namespace Magento\Backend\Block; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Directory\Helper\Data as DirectoryHelper; + /** * Standard admin block. Adds admin-specific behavior and event. * Should be used when you declare a block in admin layout handle. @@ -60,15 +64,23 @@ class Template extends \Magento\Framework\View\Element\Template /** * @param \Magento\Backend\Block\Template\Context $context * @param array $data + * @param JsonHelper|null $jsonHelper + * @param DirectoryHelper|null $directoryHelper */ - public function __construct(\Magento\Backend\Block\Template\Context $context, array $data = []) - { + public function __construct( + \Magento\Backend\Block\Template\Context $context, + array $data = [], + ?JsonHelper $jsonHelper = null, + ?DirectoryHelper $directoryHelper = null + ) { $this->_localeDate = $context->getLocaleDate(); $this->_authorization = $context->getAuthorization(); $this->mathRandom = $context->getMathRandom(); $this->_backendSession = $context->getBackendSession(); $this->formKey = $context->getFormKey(); $this->nameBuilder = $context->getNameBuilder(); + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); + $data['directoryHelper']= $directoryHelper ?? ObjectManager::getInstance()->get(DirectoryHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Backend/Block/Widget/Button.php b/app/code/Magento/Backend/Block/Widget/Button.php index 8385ecaa40a8b..3b5eca6a61779 100644 --- a/app/code/Magento/Backend/Block/Widget/Button.php +++ b/app/code/Magento/Backend/Block/Widget/Button.php @@ -5,6 +5,11 @@ */ namespace Magento\Backend\Block\Widget; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Button widget * @@ -15,6 +20,33 @@ */ class Button extends \Magento\Backend\Block\Widget { + /** + * @var Random + */ + private $random; + + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param Random|null $random + * @param SecureHtmlRenderer|null $htmlRenderer + */ + public function __construct( + Context $context, + array $data = [], + ?Random $random = null, + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct($context, $data); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Define block template * @@ -90,11 +122,12 @@ protected function _prepareAttributes($title, $classes, $disabled) 'title' => $title, 'type' => $this->getType(), 'class' => join(' ', $classes), - 'onclick' => $this->getOnClick(), - 'style' => $this->getStyle(), 'value' => $this->getValue(), 'disabled' => $disabled, ]; + if ($this->hasData('backend_button_widget_hook_id')) { + $attributes['backend-button-widget-hook-id'] = $this->getData('backend_button_widget_hook_id'); + } if ($this->getDataAttribute()) { foreach ($this->getDataAttribute() as $key => $attr) { $attributes['data-' . $key] = is_scalar($attr) ? $attr : json_encode($attr); @@ -121,4 +154,30 @@ protected function _attributesToHtml($attributes) return $html; } + + /** + * @inheritDoc + */ + protected function _beforeToHtml() + { + parent::_beforeToHtml(); + + $buttonId = 'buttonId' .$this->random->getRandomString(10); + $this->setData('backend_button_widget_hook_id', $buttonId); + + $afterHtml = $this->getAfterHtml(); + if ($this->getOnClick()) { + $afterHtml .= $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + $this->getOnClick(), + "*[backend-button-widget-hook-id='$buttonId']" + ); + } + if ($this->getStyle()) { + $afterHtml .= $this->secureRenderer->renderStyleAsTag($this->getStyle(), "#{$this->getId()}"); + } + $this->setAfterHtml($afterHtml); + + return $this; + } } diff --git a/app/code/Magento/Backend/Block/Widget/Button/SplitButton.php b/app/code/Magento/Backend/Block/Widget/Button/SplitButton.php index db3f5466fbacb..8075139368ab1 100644 --- a/app/code/Magento/Backend/Block/Widget/Button/SplitButton.php +++ b/app/code/Magento/Backend/Block/Widget/Button/SplitButton.php @@ -5,6 +5,11 @@ */ namespace Magento\Backend\Block\Widget\Button; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Split button widget * @@ -21,6 +26,33 @@ */ class SplitButton extends \Magento\Backend\Block\Widget { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random + */ + public function __construct( + Context $context, + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { + parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + } + /** * Define block template * @@ -61,6 +93,16 @@ public function getAttributesHtml() return $html; } + /** + * Get main button's "id" attribute value. + * + * @return string + */ + private function getButtonId(): string + { + return $this->getId() .'-button'; + } + /** * Retrieve button attributes html * @@ -84,11 +126,10 @@ public function getButtonAttributesHtml() $classes[] = $disabled; } $attributes = [ - 'id' => $this->getId() . '-button', + 'id' => $this->getButtonId(), 'title' => $title, 'class' => join(' ', $classes), - 'disabled' => $disabled, - 'style' => $this->getStyle(), + 'disabled' => $disabled ]; //TODO perhaps we need to skip data-mage-init when disabled="disabled" @@ -180,7 +221,7 @@ public function hasSplit() * Add data attributes to $attributes array * * @param array $data - * @param array &$attributes + * @param array $attributes * @return void */ protected function _getDataAttributes($data, &$attributes) @@ -190,6 +231,21 @@ protected function _getDataAttributes($data, &$attributes) } } + /** + * Retrieve "id" attribute value for an option. + * + * @param array $option + * @return string + */ + private function identifyOption(array $option): string + { + return isset($option['id']) + ? $this->getId() .'-' .$option['id'] + : (isset($option['id_attribute']) ? + $option['id_attribute'] + : $this->getId() .'-optId' .$this->random->getRandomString(10)); + } + /** * Prepare option attributes * @@ -203,11 +259,9 @@ protected function _getDataAttributes($data, &$attributes) protected function _prepareOptionAttributes($option, $title, $classes, $disabled) { $attributes = [ - 'id' => isset($option['id']) ? $this->getId() . '-' . $option['id'] : '', + 'id' => $this->identifyOption($option), 'title' => $title, 'class' => join(' ', $classes), - 'onclick' => isset($option['onclick']) ? $option['onclick'] : '', - 'style' => isset($option['style']) ? $option['style'] : '', 'disabled' => $disabled, ]; @@ -235,4 +289,29 @@ protected function _getAttributesString($attributes) } return join(' ', $html); } + + /** + * @inheritDoc + */ + protected function _beforeToHtml() + { + parent::_beforeToHtml(); + + $afterHtml = $this->getAfterHtml(); + /** @var array|null $options */ + $options = $this->getOptions() ?? []; + foreach ($options as &$option) { + $id = $option['id_attribute'] = $this->identifyOption($option); + if (!empty($option['onclick'])) { + $afterHtml .= $this->secureRenderer->renderEventListenerAsTag('onclick', $option['onclick'], "#$id"); + } + if (!empty($option['style'])) { + $afterHtml .= $this->secureRenderer->renderStyleAsTag($option['style'], "#$id"); + } + } + $this->setOptions($options); + $this->setAfterHtml($afterHtml); + + return $this; + } } diff --git a/app/code/Magento/Backend/Block/Widget/Form/Container.php b/app/code/Magento/Backend/Block/Widget/Form/Container.php index febaae3861688..6d92d2bfb0396 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Container.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Container.php @@ -5,6 +5,10 @@ */ namespace Magento\Backend\Block\Widget\Form; +use Magento\Backend\Block\Widget\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Backend form container block * @@ -39,7 +43,7 @@ class Container extends \Magento\Backend\Block\Widget\Container * @var string */ protected $_blockGroup = 'Magento_Backend'; - + /** * @var string */ @@ -55,6 +59,25 @@ class Container extends \Magento\Backend\Block\Widget\Container */ protected $_template = 'Magento_Backend::widget/form/container.phtml'; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Context $context, + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($context, $data); + } + /** * Initialize form. * @@ -205,8 +228,14 @@ public function getFormHtml() public function getFormInitScripts() { if (!empty($this->_formInitScripts) && is_array($this->_formInitScripts)) { - return '<script>' . implode("\n", $this->_formInitScripts) . '</script>'; + return $this->secureRenderer->renderTag( + 'script', + [], + implode("\n", $this->_formInitScripts), + false + ); } + return ''; } @@ -218,8 +247,14 @@ public function getFormInitScripts() public function getFormScripts() { if (!empty($this->_formScripts) && is_array($this->_formScripts)) { - return '<script>' . implode("\n", $this->_formScripts) . '</script>'; + return $this->secureRenderer->renderTag( + 'script', + [], + implode("\n", $this->_formScripts), + false + ); } + return ''; } diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php index d599d5fbad5e0..5517cb8d4d617 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php @@ -6,6 +6,9 @@ namespace Magento\Backend\Block\Widget\Form\Element; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Form element dependencies mapper * Assumes that one element may depend on other element values. @@ -52,21 +55,29 @@ class Dependence extends \Magento\Backend\Block\AbstractBlock */ protected $_jsonEncoder; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Config\Model\Config\Structure\Element\Dependency\FieldFactory $fieldFactory * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Config\Model\Config\Structure\Element\Dependency\FieldFactory $fieldFactory, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_fieldFactory = $fieldFactory; parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -131,11 +142,11 @@ protected function _toHtml() $params .= ', ' . $this->_jsonEncoder->encode($this->_configOptions); } - return "<script> -require(['mage/adminhtml/form'], function(){ - new FormElementDependenceController({$params}); -}); -</script>"; + $scriptString = 'require([\'mage/adminhtml/form\'], function(){ + new FormElementDependenceController(' . $params . '); +});'; + + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } /** diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php b/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php index b9cdd259796d0..1b89746b3a98a 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php @@ -14,7 +14,7 @@ /** * Class ElementCreator * - * @deprecated 100.3.0 in favour of UI component implementation + * @deprecated 101.0.1 in favour of UI component implementation * @package Magento\Backend\Block\Widget\Form\Element */ class ElementCreator diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/Gallery.php b/app/code/Magento/Backend/Block/Widget/Form/Element/Gallery.php index aa0b0c3352ebe..25ea5b6100e28 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Element/Gallery.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/Gallery.php @@ -6,7 +6,10 @@ namespace Magento\Backend\Block\Widget\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Backend\Block\Template\Context; /** * Backend image gallery item renderer @@ -27,6 +30,18 @@ class Gallery extends \Magento\Backend\Block\Template implements protected $_template = 'Magento_Backend::widget/form/element/gallery.phtml'; /** + * @param Context $context + * @param array $data + */ + public function __construct(Context $context, array $data = []) + { + $data['jsonHelper'] = ObjectManager::getInstance()->get(JsonHelper::class); + parent::__construct($context, $data); + } + + /** + * Renderer. + * * @param AbstractElement $element * @return string */ @@ -37,6 +52,8 @@ public function render(AbstractElement $element) } /** + * Set element. + * * @param AbstractElement $element * @return $this */ @@ -47,6 +64,8 @@ public function setElement(AbstractElement $element) } /** + * Get element. + * * @return AbstractElement|null */ public function getElement() @@ -55,6 +74,8 @@ public function getElement() } /** + * Get value. + * * @return array */ public function getValues() @@ -63,7 +84,7 @@ public function getValues() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareLayout() { @@ -82,6 +103,8 @@ protected function _prepareLayout() } /** + * Return add button. + * * @return string */ public function getAddButtonHtml() @@ -90,6 +113,8 @@ public function getAddButtonHtml() } /** + * Return delete button. + * * @param string $image * @return string|string[] */ diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php index 632603d389d21..65c63c9689fc5 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php @@ -7,6 +7,8 @@ namespace Magento\Backend\Block\Widget\Grid\Column\Filter; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Date grid column filter @@ -30,6 +32,11 @@ class Date extends \Magento\Backend\Block\Widget\Grid\Column\Filter\AbstractFilt */ protected $dateTimeFormatter; + /** + * @var SecureHtmlRenderer + */ + protected $secureHtmlRenderer; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Framework\DB\Helper $resourceHelper @@ -37,6 +44,7 @@ class Date extends \Magento\Backend\Block\Widget\Grid\Column\Filter\AbstractFilt * @param \Magento\Framework\Locale\ResolverInterface $localeResolver * @param DateTimeFormatterInterface $dateTimeFormatter * @param array $data + * @param SecureHtmlRenderer|null $secureHtmlRenderer */ public function __construct( \Magento\Backend\Block\Context $context, @@ -44,16 +52,18 @@ public function __construct( \Magento\Framework\Math\Random $mathRandom, \Magento\Framework\Locale\ResolverInterface $localeResolver, DateTimeFormatterInterface $dateTimeFormatter, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureHtmlRenderer = null ) { $this->mathRandom = $mathRandom; $this->localeResolver = $localeResolver; parent::__construct($context, $resourceHelper, $data); $this->dateTimeFormatter = $dateTimeFormatter; + $this->secureHtmlRenderer = $secureHtmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** - * @return string + * @inheritDoc */ public function getHtml() { @@ -99,7 +109,7 @@ public function getHtml() ' value="' . $this->localeResolver->getLocale() . '"/>'; - $html .= '<script> + $scriptString = ' require(["jquery", "mage/calendar"], function($){ $("#' . $htmlId . @@ -120,12 +130,15 @@ public function getHtml() '_to" } }) - }); - </script>'; + });'; + $html .= $this->secureHtmlRenderer->renderTag('script', [], $scriptString, false); + return $html; } /** + * Return escaped value. + * * @param string|null $index * @return array|string|int|float|null */ @@ -147,6 +160,8 @@ public function getEscapedValue($index = null) } /** + * Return value. + * * @param string|null $index * @return array|string|int|float|null */ @@ -166,6 +181,8 @@ public function getValue($index = null) } /** + * Return conditions. + * * @return array|string|int|float|null */ public function getCondition() @@ -176,6 +193,8 @@ public function getCondition() } /** + * Set value. + * * @param array|string|int|float $value * @return $this */ diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php index 1d8d658267020..a139d20191b57 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php @@ -20,7 +20,7 @@ class Datetime extends \Magento\Backend\Block\Widget\Grid\Column\Filter\Date const END_OF_DAY_IN_SECONDS = 86399; /** - * {@inheritdoc} + * @inheritdoc */ public function getValue($index = null) { @@ -117,8 +117,7 @@ public function getHtml() ) . '/>' . '</div></div>'; $html .= '<input type="hidden" name="' . $this->_getHtmlName() . '[locale]"' . ' value="' . $this->localeResolver->getLocale() . '"/>'; - $html .= '<script> - require(["jquery", "mage/calendar"],function($){ + $scriptString = 'require(["jquery", "mage/calendar"],function($){ $("#' . $htmlId . '_range").dateRange({ dateFormat: "' . $format . '", timeFormat: "' . $timeFormat . '", @@ -131,8 +130,9 @@ public function getHtml() id: "' . $htmlId . '_to" } }) - }); - </script>'; + });'; + $html .= $this->secureHtmlRenderer->renderTag('script', [], $scriptString, false); + return $html; } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Price.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Price.php index c6e271c3ec304..40106833b6a9a 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Price.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Price.php @@ -162,7 +162,7 @@ protected function _getCurrencyList() /** * Retrieve filter value * - * @param null $index + * @param string|null $index * @return array|null */ public function getValue($index = null) @@ -194,11 +194,11 @@ public function getCondition() $rate = $this->_getRate($displayCurrency, $this->_getColumnCurrencyCode()); if (isset($value['from'])) { - $value['from'] *= $rate; + $value['from'] = (float) $value['from'] * $rate; } if (isset($value['to'])) { - $value['to'] *= $rate; + $value['to'] = (float) $value['to'] * $rate; } $this->prepareRates($displayCurrency); diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php index a7d85a4cfef4c..0da7e4db9b983 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php @@ -6,6 +6,10 @@ namespace Magento\Backend\Block\Widget\Grid\Column\Renderer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Grid column widget for rendering action grid cells * @@ -20,18 +24,34 @@ class Action extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Text */ protected $_jsonEncoder; + /** + * @var SecureHtmlRenderer + */ + private $secureHtmlRenderer; + + /** + * @var Random + */ + private $random; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param array $data + * @param SecureHtmlRenderer|null $secureHtmlRenderer + * @param Random|null $random */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureHtmlRenderer = null, + ?Random $random = null ) { $this->_jsonEncoder = $jsonEncoder; parent::__construct($context, $data); + $this->secureHtmlRenderer = $secureHtmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); } /** @@ -111,8 +131,22 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row) unset($action['confirm']); } + if (empty($action['id'])) { + $action['id'] = 'id' .$this->random->getRandomString(10); + } $actionAttributes->setData($action); - return '<a ' . $actionAttributes->serialize() . '>' . $actionCaption . '</a>'; + $onclick = $actionAttributes->getData('onclick'); + $style = $actionAttributes->getData('style'); + $actionAttributes->unsetData(['onclick', 'style']); + $html = '<a ' . $actionAttributes->serialize() . '>' . $actionCaption . '</a>'; + if ($onclick) { + $html .= $this->secureHtmlRenderer->renderEventListenerAsTag('onclick', $onclick, "#{$action['id']}"); + } + if ($style) { + $html .= $this->secureHtmlRenderer->renderStyleAsTag($style, "#{$action['id']}"); + } + + return $html; } /** diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Checkbox.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Checkbox.php index 1297f5cd330b8..013c3b7e105f5 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Checkbox.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Checkbox.php @@ -5,6 +5,10 @@ */ namespace Magento\Backend\Block\Widget\Grid\Column\Renderer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Grid checkbox column renderer * @@ -29,18 +33,34 @@ class Checkbox extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abstra */ protected $_converter; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + /** * @param \Magento\Backend\Block\Context $context - * @param \Magento\Backend\Block\Widget\Grid\Column\Renderer\Options\Converter $converter + * @param Options\Converter $converter * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Backend\Block\Widget\Grid\Column\Renderer\Options\Converter $converter, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null ) { parent::__construct($context, $data); $this->_converter = $converter; + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); } /** @@ -112,6 +132,8 @@ public function render(\Magento\Framework\DataObject $row) } /** + * Render checkbox HTML. + * * @param string $value Value of the element * @param bool $checked Whether it is checked * @return string @@ -154,11 +176,18 @@ public function renderHeader() if ($this->getColumn()->getDisabled()) { $disabled = ' disabled="disabled"'; } + $id = 'id' .$this->random->getRandomString(10); $html = '<th class="data-grid-th data-grid-actions-cell"><input type="checkbox" '; + $html .= 'id="' .$id .'" '; $html .= 'name="' . $this->getColumn()->getFieldName() . '" '; - $html .= 'onclick="' . $this->getColumn()->getGrid()->getJsObjectName() . '.checkCheckboxes(this)" '; $html .= 'class="admin__control-checkbox"' . $checked . $disabled . ' '; $html .= 'title="' . __('Select All') . '"/><label></label></th>'; + $html .= $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + $this->getColumn()->getGrid()->getJsObjectName() . '.checkCheckboxes(this)', + "#$id" + ); + return $html; } } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Export.php b/app/code/Magento/Backend/Block/Widget/Grid/Export.php index 7b7f6cc14799c..11b24539e54f5 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Export.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Export.php @@ -9,6 +9,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; /** + * Class Export for exporting grid data as CSV file or MS Excel 2003 XML Document file + * * @api * @deprecated 100.2.0 in favour of UI component implementation * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -69,6 +71,8 @@ public function __construct( } /** + * Internal constructor, that is called from real constructor + * * @return void * @throws \Magento\Framework\Exception\LocalizedException */ @@ -242,6 +246,7 @@ protected function _getExportTotals() /** * Iterate collection and call callback method per item + * * For callback method first argument always is item object * * @param string $callback @@ -273,7 +278,12 @@ public function _exportIterateCollection($callback, array $args) $collection = $this->_getRowCollection($originalCollection); foreach ($collection as $item) { - call_user_func_array([$this, $callback], array_merge([$item], $args)); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func_array( + [$this, $callback], + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + array_merge([$item], $args) + ); } } } @@ -307,7 +317,7 @@ protected function _exportCsvItem( */ public function getCsvFile() { - $name = md5(microtime()); + $name = hash('sha256', microtime()); $file = $this->_path . '/' . $name . '.csv'; $this->_directory->create($this->_path); @@ -432,11 +442,11 @@ public function getRowRecord(\Magento\Framework\DataObject $data) */ public function getExcelFile($sheetName = '') { - $collection = $this->_getRowCollection(); + $collection = $this->_getPreparedCollection(); $convert = new \Magento\Framework\Convert\Excel($collection->getIterator(), [$this, 'getRowRecord']); - $name = md5(microtime()); + $name = hash('sha256', microtime()); $file = $this->_path . '/' . $name . '.xml'; $this->_directory->create($this->_path); @@ -551,6 +561,8 @@ public function _getPreparedCollection() } /** + * Get export page size + * * @return int */ public function getExportPageSize() diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Extended.php b/app/code/Magento/Backend/Block/Widget/Grid/Extended.php index 40e87171e82cc..539b208436e87 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Extended.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Extended.php @@ -8,6 +8,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; /** + * Extended Grid Widget + * * @api * @deprecated 100.2.0 in favour of UI component implementation * @SuppressWarnings(PHPMD.ExcessivePublicCount) @@ -177,7 +179,10 @@ class Extended extends \Magento\Backend\Block\Widget\Grid implements \Magento\Ba protected $_path = 'export'; /** + * Initialization + * * @return void + * @throws \Magento\Framework\Exception\FileSystemException */ protected function _construct() { @@ -297,6 +302,7 @@ public function addColumn($columnId, $column) ); $this->getColumnSet()->getChildBlock($columnId)->setGrid($this); } else { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception(__('Please correct the column format and try again.')); } @@ -471,10 +477,6 @@ protected function _prepareMassactionColumn() protected function _prepareCollection() { if ($this->getCollection()) { - if ($this->getCollection()->isLoaded()) { - $this->getCollection()->clear(); - } - parent::_prepareCollection(); if (!$this->_isExport) { @@ -663,6 +665,7 @@ public function setEmptyCellLabel($label) */ public function getRowUrl($item) { + // phpstan:ignore "Call to an undefined static method" $res = parent::getRowUrl($item); return $res ? $res : '#'; } @@ -680,6 +683,7 @@ public function getMultipleRows($item) /** * Retrieve columns for multiple rows + * * @return array */ public function getMultipleRowColumns() @@ -943,6 +947,7 @@ protected function _getExportTotals() /** * Iterate collection and call callback method per item + * * For callback method first argument always is item object * * @param string $callback @@ -972,7 +977,12 @@ public function _exportIterateCollection($callback, array $args) $page++; foreach ($collection as $item) { - call_user_func_array([$this, $callback], array_merge([$item], $args)); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func_array( + [$this, $callback], + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + array_merge([$item], $args) + ); } } } @@ -1009,6 +1019,7 @@ public function getCsvFile() $this->_isExport = true; $this->_prepareGrid(); + // phpcs:ignore Magento2.Security.InsecureFunction $name = md5(microtime()); $file = $this->_path . '/' . $name . '.csv'; @@ -1153,6 +1164,7 @@ public function getExcelFile($sheetName = '') [$this, 'getRowRecord'] ); + // phpcs:ignore Magento2.Security.InsecureFunction $name = md5(microtime()); $file = $this->_path . '/' . $name . '.xml'; @@ -1244,7 +1256,7 @@ public function setCollection($collection) } /** - * get collection object + * Get collection object * * @return \Magento\Framework\Data\Collection */ diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php index 662cbedaed8db..53e52fc7252b3 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php @@ -64,6 +64,7 @@ public function __construct( * @param array|DataObject $item * * @return $this + * @since 100.2.3 */ public function addItem($itemId, $item) { diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php index a9be14b77b29c..7bef74862f029 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php @@ -88,7 +88,7 @@ protected function createPage() * * @return bool * - * @deprecated Backup module is to be removed. + * @deprecated 100.2.7 Backup module is to be removed. */ protected function _backupDatabase() { diff --git a/app/code/Magento/Backend/Helper/Dashboard/Data.php b/app/code/Magento/Backend/Helper/Dashboard/Data.php index f691d2b7cd4b9..39ea634320b41 100644 --- a/app/code/Magento/Backend/Helper/Dashboard/Data.php +++ b/app/code/Magento/Backend/Helper/Dashboard/Data.php @@ -88,7 +88,7 @@ public function countStores() /** * Prepare array with periods for dashboard graphs * - * @deprecated periods were moved to it's own class + * @deprecated 102.0.0 periods were moved to it's own class * @see Period::getDatePeriods() * * @return array diff --git a/app/code/Magento/Backend/Model/Url.php b/app/code/Magento/Backend/Model/Url.php index 97f82647d9445..8948961be8875 100644 --- a/app/code/Magento/Backend/Model/Url.php +++ b/app/code/Magento/Backend/Model/Url.php @@ -372,6 +372,7 @@ protected function _getMenu() * * @param mixed $scopeId * @return \Magento\Framework\UrlInterface + * @since 101.0.3 */ public function setScope($scopeId) { 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/Section/AdminMenuSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml index e6782dca897d7..f9d3c49d509e9 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml @@ -17,11 +17,11 @@ <element name="widgets" type="button" selector="#nav li[data-ui-id='menu-magento-widget-cms-widget-instance']"/> <element name="stores" type="button" selector="#menu-magento-backend-stores"/> <element name="configuration" type="button" selector="#nav li[data-ui-id='menu-magento-config-system-config']"/> - <element name="dashboard" type="button" selector="//li[@id='menu-magento-backend-dashboard']"/> - <element name="sales" type="button" selector="//li[@id='menu-magento-sales-sales']"/> - <element name="marketing" type="button" selector="//li[@id='menu-magento-backend-marketing']"/> - <element name="system" type="button" selector="//li[@id='menu-magento-backend-system']"/> - <element name="findPartners" type="button" selector="//li[@id='menu-magento-marketplace-partners']"/> + <element name="dashboard" type="button" selector="#menu-magento-backend-dashboard"/> + <element name="sales" type="button" selector="#menu-magento-sales-sales"/> + <element name="marketing" type="button" selector="#menu-magento-backend-marketing"/> + <element name="system" type="button" selector="#menu-magento-backend-system"/> + <element name="findPartners" type="button" selector="#menu-magento-marketplace-partners"/> <!-- Navigate menu selectors --> <element name="menuItem" type="button" selector="li[data-ui-id='menu-{{dataUiId}}']" parameterized="true" timeout="30"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml index 8ac7af096da0a..ef69764a87833 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml @@ -94,8 +94,7 @@ </actionGroup> <!--Navigate to Product attribute page--> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="navigateToNewProductAttributePage"/> <fillField userInput="test_label" selector="{{AttributePropertiesSection.DefaultLabel}}" stepKey="fillDefaultLabel"/> <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="Text Swatch" stepKey="selectInputType"/> <click selector="{{AttributePropertiesSection.addSwatch}}" stepKey="clickAddSwatch"/> 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/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml similarity index 92% rename from app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml rename to app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml index 972947656cd3d..44577771fe4f8 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml @@ -34,7 +34,6 @@ <!-- Reset admin order filter --> <comment userInput="Reset admin order filter" stepKey="resetAdminOrderFilter"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingOrderGrid"/> <magentoCLI command="config:set admin/dashboard/enable_charts 0" stepKey="setDisableChartsAsDefault"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> @@ -64,8 +63,7 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingCheckoutPageWithShippingMethod"/> <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask1"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <comment userInput="Select Check/Money payment" stepKey="checkoutSelectCheckMoneyPayment"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> @@ -85,16 +83,14 @@ <comment userInput="Create invoice" stepKey="createInvoice"/> <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> - <waitForPageLoad stepKey="waitForInvoicePageToLoad"/> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceButton"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoiceInPageTitle" after="clickInvoiceButton"/> <see selector="{{AdminInvoiceTotalSection.total('Subtotal')}}" userInput="$150.00" stepKey="seeCorrectGrandTotal"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessInvoiceMessage"/> <!--Create Shipment for the order--> <comment userInput="Create Shipment for the order" stepKey="createShipmentForOrder"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage2"/> - <waitForPageLoad time="30" stepKey="waitForOrderListPageLoading"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrdersPage2"/> <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="openOrderPageForShip"/> <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> <waitForPageLoad stepKey="waitForShipmentPagePage"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml index be734205e1f5b..c5b4e8c34bfec 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml @@ -21,11 +21,15 @@ </annotations> <before> <magentoCLI command="config:set {{ChangedCookieDomainForMainWebsiteConfigData.path}} --scope={{ChangedCookieDomainForMainWebsiteConfigData.scope}} --scope-code={{ChangedCookieDomainForMainWebsiteConfigData.scope_code}} {{ChangedCookieDomainForMainWebsiteConfigData.value}}" stepKey="changeDomainForMainWebsiteBeforeTestRun"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBeforeTestRun"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{EmptyCookieDomainForMainWebsiteConfigData.path}} --scope={{EmptyCookieDomainForMainWebsiteConfigData.scope}} --scope-code={{EmptyCookieDomainForMainWebsiteConfigData.scope_code}} {{EmptyCookieDomainForMainWebsiteConfigData.value}}" stepKey="changeDomainForMainWebsiteAfterTestComplete"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestComplete"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTestComplete"> + <argument name="tags" value="config"/> + </actionGroup> </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AssertAdminDashboardPageIsVisibleActionGroup" stepKey="seeDashboardPage"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml index d2c628ed13701..af0a5751a7488 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml @@ -21,15 +21,18 @@ </annotations> <before> <magentoCLI command="config:set {{MinifyJavaScriptFilesEnableConfigData.path}} {{MinifyJavaScriptFilesEnableConfigData.value}}" stepKey="enableJsMinification"/> - <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> + <magentoCLI command="setup:static-content:deploy -f" stepKey="deployStaticContent"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> <magentoCLI command="config:set {{MinifyJavaScriptFilesDisableConfigData.path}} {{MinifyJavaScriptFilesDisableConfigData.value}}" stepKey="disableJsMinification"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <see userInput="Dashboard" selector="{{AdminHeaderSection.pageTitle}}" stepKey="seeDashboardTitle"/> <waitForPageLoad stepKey="waitForPageLoadOnDashboard"/> + <see userInput="Dashboard" selector="{{AdminHeaderSection.pageTitle}}" stepKey="seeDashboardTitle"/> <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="loggedInSuccessfully"/> <actionGroup ref="AssertAdminPageIsNot404ActionGroup" stepKey="dontSee404Page"/> </test> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml index 812158948d85f..aa246cb5f9d22 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml @@ -20,12 +20,16 @@ </annotations> <before> <magentoCLI command="config:set admin/security/use_form_key 1" stepKey="enableUrlSecretKeys"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches1"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches1"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> <magentoCLI command="config:set admin/security/use_form_key 0" stepKey="disableUrlSecretKeys"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches2"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches2"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPersistentShoppingCartSettingsTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPersistentShoppingCartSettingsTest.xml index 387e81cb71546..bb69aa218e77a 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminPersistentShoppingCartSettingsTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPersistentShoppingCartSettingsTest.xml @@ -21,11 +21,15 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <magentoCLI stepKey="enablePersistentShoppingCart" command="config:set persistent/options/enabled 1"/> - <magentoCLI stepKey="cacheClean" command="cache:clean config"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <magentoCLI stepKey="disablePersistentShoppingCart" command="config:set persistent/options/enabled 0"/> - <magentoCLI stepKey="cacheClean" command="cache:clean config"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/PriceTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/PriceTest.php new file mode 100644 index 0000000000000..6efc6fcab5b8a --- /dev/null +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/PriceTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Test\Unit\Block\Widget\Grid\Column\Filter; + +use Magento\Backend\Block\Context; +use Magento\Backend\Block\Widget\Grid\Column; +use Magento\Backend\Block\Widget\Grid\Column\Filter\Price; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\DB\Helper; +use Magento\Directory\Model\Currency; +use Magento\Directory\Model\Currency\DefaultLocator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PriceTest extends TestCase +{ + /** @var RequestInterface|MockObject */ + private $requestMock; + + /** @var Context|MockObject */ + private $context; + + /** @var Helper|MockObject */ + private $helper; + + /** @var Currency|MockObject */ + private $currency; + + /** @var DefaultLocator|MockObject */ + private $currencyLocator; + + /** @var Column|MockObject */ + private $columnMock; + + /** @var Price */ + private $blockPrice; + + protected function setUp(): void + { + $this->requestMock = $this->getMockForAbstractClass(RequestInterface::class); + + $this->context = $this->createMock(Context::class); + $this->context->expects($this->any())->method('getRequest')->willReturn($this->requestMock); + + $this->helper = $this->createMock(Helper::class); + + $this->currency = $this->getMockBuilder(Currency::class) + ->disableOriginalConstructor() + ->setMethods(['getAnyRate']) + ->getMock(); + + $this->currencyLocator = $this->createMock(DefaultLocator::class); + + $this->columnMock = $this->getMockBuilder(Column::class) + ->disableOriginalConstructor() + ->setMethods(['getCurrencyCode']) + ->getMock(); + + $helper = new ObjectManager($this); + + $this->blockPrice = $helper->getObject(Price::class, [ + 'context' => $this->context, + 'resourceHelper' => $this->helper, + 'currencyModel' => $this->currency, + 'currencyLocator' => $this->currencyLocator + ]); + $this->blockPrice->setColumn($this->columnMock); + } + + public function testGetCondition() + { + $this->currencyLocator->expects( + $this->any() + )->method( + 'getDefaultCurrency' + )->with( + $this->requestMock + )->willReturn( + 'defaultCurrency' + ); + + $this->currency->expects($this->at(0)) + ->method('getAnyRate') + ->with('defaultCurrency') + ->willReturn(1.0); + + $testValue = [ + 'value' => [ + 'from' => '1234a', + ] + ]; + + $this->blockPrice->addData($testValue); + $this->assertEquals(['from' => 1234], $this->blockPrice->getCondition()); + } +} diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ExtendedTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ExtendedTest.php index deb9c300f41b8..b841ad271ac43 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ExtendedTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ExtendedTest.php @@ -41,8 +41,8 @@ public function testPrepareLoadedCollection() $layout->expects($this->any())->method('getBlock')->willReturn($columnSet); $collection = $this->createMock(Collection::class); - $collection->expects($this->atLeastOnce())->method('isLoaded')->willReturn(true); - $collection->expects($this->atLeastOnce())->method('clear'); + $collection->expects($this->never())->method('isLoaded'); + $collection->expects($this->never())->method('clear'); $collection->expects($this->atLeastOnce())->method('load'); /** @var Extended $block */ diff --git a/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php b/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php index 3f4f3669ab75b..d3c177fa907ab 100644 --- a/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php +++ b/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php @@ -17,6 +17,7 @@ * Provide an ability to show confirmation message on click on the "Delete" button * * @api + * @since 101.0.0 */ class DeleteButton implements ButtonProviderInterface { @@ -84,6 +85,7 @@ public function __construct( /** * {@inheritdoc} + * @since 101.0.0 */ public function getButtonData() { diff --git a/app/code/Magento/Backend/Ui/Component/Control/SaveSplitButton.php b/app/code/Magento/Backend/Ui/Component/Control/SaveSplitButton.php index 75d6bad06e239..f85264e532057 100644 --- a/app/code/Magento/Backend/Ui/Component/Control/SaveSplitButton.php +++ b/app/code/Magento/Backend/Ui/Component/Control/SaveSplitButton.php @@ -13,6 +13,7 @@ * Provide an ability to show drop-down list with options clicking on the "Save" button * * @api + * @since 101.0.0 */ class SaveSplitButton implements ButtonProviderInterface { @@ -31,6 +32,7 @@ public function __construct(string $targetName) /** * {@inheritdoc} + * @since 101.0.0 */ public function getButtonData() { diff --git a/app/code/Magento/Backend/Ui/Component/Listing/Column/EditAction.php b/app/code/Magento/Backend/Ui/Component/Listing/Column/EditAction.php index fb0aa6987f4d9..1769bd7b3bb64 100644 --- a/app/code/Magento/Backend/Ui/Component/Listing/Column/EditAction.php +++ b/app/code/Magento/Backend/Ui/Component/Listing/Column/EditAction.php @@ -14,6 +14,7 @@ * Represents Edit link in grid for entity by its identifier field * * @api + * @since 101.0.0 */ class EditAction extends Column { @@ -43,6 +44,7 @@ public function __construct( /** * @param array $dataSource * @return array + * @since 101.0.0 */ public function prepareDataSource(array $dataSource) { diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 45ed50fd49b7e..57cc36da95cfe 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -146,7 +146,7 @@ <label>Allow Symlinks</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment> - <![CDATA[<strong style="color:red">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk.]]> + <![CDATA[<strong class="colorRed">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk.]]> </comment> </field> <field id="minify_html" translate="label comment" type="select" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> @@ -459,7 +459,9 @@ <label>Add Store Code to Urls</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <backend_model>Magento\Config\Model\Config\Backend\Store</backend_model> - <comment><![CDATA[<strong style="color:red">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).]]></comment> + <comment> + <![CDATA[<strong class="colorRed">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).]]> + </comment> </field> <field id="redirect_to_base" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Auto-redirect to Base URL</label> diff --git a/app/code/Magento/Backend/i18n/en_US.csv b/app/code/Magento/Backend/i18n/en_US.csv index 59bbe7f69985a..74633141c89fe 100644 --- a/app/code/Magento/Backend/i18n/en_US.csv +++ b/app/code/Magento/Backend/i18n/en_US.csv @@ -332,7 +332,7 @@ Debug,Debug "Add Block Names to Hints","Add Block Names to Hints" "Template Settings","Template Settings" "Allow Symlinks","Allow Symlinks" -"<strong style=""color:red"">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk.","<strong style=""color:red"">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk." +"<strong class=""colorRed">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk.","<strong class=""colorRed"">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk." "Minify Html","Minify Html" "Minification is not applied in developer mode.","Minification is not applied in developer mode." "Translate Inline","Translate Inline" @@ -403,6 +403,11 @@ Security,Security Web,Web "Url Options","Url Options" "Add Store Code to Urls","Add Store Code to Urls" +" + <strong class=""colorRed">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.). + "," + <strong class=""colorRed">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.). + " "<strong style=""color:red"">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).","<strong style=""color:red"">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.)." "Enable Frontend Resize","Enable Frontend Resize" "Resize performed via javascript before file upload.","Resize performed via javascript before file upload." diff --git a/app/code/Magento/Backend/view/adminhtml/layout/default.xml b/app/code/Magento/Backend/view/adminhtml/layout/default.xml index a7faab0bc4673..0d629e31d6d91 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/default.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/default.xml @@ -70,7 +70,6 @@ <argument name="bugreport_url" xsi:type="string">https://github.com/magento/magento2/issues</argument> </arguments> </block> - </container> </container> </referenceContainer> diff --git a/app/code/Magento/Backend/view/adminhtml/requirejs-config.js b/app/code/Magento/Backend/view/adminhtml/requirejs-config.js index e886f28cd158b..ae0e84e2d27f8 100644 --- a/app/code/Magento/Backend/view/adminhtml/requirejs-config.js +++ b/app/code/Magento/Backend/view/adminhtml/requirejs-config.js @@ -6,8 +6,7 @@ var config = { map: { '*': { - 'mediaUploader': 'Magento_Backend/js/media-uploader', - 'mage/translate': 'Magento_Backend/js/translate' + 'mediaUploader': 'Magento_Backend/js/media-uploader' } } }; diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/chart.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/chart.phtml index 65c0d292ee187..56131fa622321 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/chart.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/chart.phtml @@ -7,18 +7,23 @@ use Magento\Backend\ViewModel\ChartsPeriod; use Magento\Framework\Escaper; use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * @var Template $block * @var Escaper $escaper * @var ChartsPeriod $viewModel + * @var SecureHtmlRenderer $secureRenderer */ $viewModel = $block->getViewModel(); ?> <div class="dashboard-diagram"> <div class="dashboard-diagram-graph"> - <canvas id="chart_<?= $escaper->escapeHtmlAttr($block->getData('html_id')) ?>_period" - style="display: none;"></canvas> + <canvas id="chart_<?= $escaper->escapeHtmlAttr($block->getData('html_id')) ?>_period"/> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + '#chart_' . $escaper->escapeJs($block->getData('html_id')) . '_period' + ) ?> <div class="dashboard-diagram-nodata"> <span><?= $escaper->escapeHtml(__('No Data Found')) ?></span> </div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml index 7c05335642ba7..a2eb24476726e 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml @@ -3,89 +3,113 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $numColumns = count($block->getColumns()); ?> -<?php if ($block->getCollection()) : ?> -<div class="dashboard-item-content"> - <?php if ($block->getCollection()->getSize() > 0) : ?> - <table class="admin__table-primary dashboard-data" id="<?= $block->escapeHtmlAttr($block->getId()) ?>_table"> - <?php - /* This part is commented to remove all <col> tags from the code. */ - /* foreach ($block->getColumns() as $_column): ?> - <col <?= $_column->getHtmlProperty() ?> /> - <?php endforeach; */ ?> - <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()) : ?> - <thead> - <?php if ($block->getHeadersVisibility()) : ?> - <tr> - <?php foreach ($block->getColumns() as $_column) : ?> - <?= $_column->getHeaderHtml() ?> - <?php endforeach; ?> - </tr> +<?php if ($block->getCollection()): ?> + <div class="dashboard-item-content"> + <?php if ($block->getCollection()->getSize() > 0): ?> + <table class="admin__table-primary dashboard-data" + id="<?= $block->escapeHtmlAttr($block->getId()) ?>_table"> + <?php + /* This part is commented to remove all <col> tags from the code. */ + /* foreach ($block->getColumns() as $_column): ?> + <col <?= $_column->getHtmlProperty() ?> /> + <?php endforeach; */ ?> + <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()): ?> + <thead> + <?php if ($block->getHeadersVisibility()): ?> + <tr> + <?php foreach ($block->getColumns() as $_column): ?> + <?= $_column->getHeaderHtml() ?> + <?php endforeach; ?> + </tr> + <?php endif; ?> + </thead> <?php endif; ?> - </thead> - <?php endif; ?> - <?php if (!$block->getIsCollapsed()) : ?> - <tbody> - <?php foreach ($block->getCollection() as $_index => $_item) : ?> - <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>"> - <?php $i = 0; foreach ($block->getColumns() as $_column) : ?> - <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> <?= /* @noEscape */ ++$i == $numColumns ? 'last' : '' ?>"><?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?></td> + <?php if (!$block->getIsCollapsed()): ?> + <tbody> + <?php foreach ($block->getCollection() as $_index => $_item): ?> + <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>"> + <?php $i = 0; foreach ($block->getColumns() as $_column): ?> + <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()); + ?> <?= /* @noEscape */ ++$i == $numColumns ? 'last' : ''; +?>"><?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? + $_html : ' ') ?></td> + <?php endforeach; ?> + </tr> <?php endforeach; ?> - </tr> - <?php endforeach; ?> - </tbody> - <?php endif; ?> - </table> - <?php else : ?> - <div class="<?= $block->escapeHtmlAttr($block->getEmptyTextClass()) ?>"><?= $block->escapeHtml($block->getEmptyText()) ?></div> - <?php endif; ?> -</div> - <?php if ($block->canDisplayContainer()) : ?> -<script> -var deps = []; - - <?php if ($block->getDependencyJsObject()) : ?> -deps.push('uiRegistry'); + </tbody> + <?php endif; ?> + </table> + <?php else: ?> + <div class="<?= $block->escapeHtmlAttr($block->getEmptyTextClass()); + ?>"><?= $block->escapeHtml($block->getEmptyText()) ?></div> <?php endif; ?> + </div> + <?php if ($block->canDisplayContainer()): ?> + <?php $scriptString = 'var deps = [];' . PHP_EOL; + if ($block->getDependencyJsObject()) { + $scriptString .= 'deps.push(\'uiRegistry\');' . PHP_EOL; + } - <?php if (strpos($block->getRowClickCallback(), 'order.') !== false) : ?> -deps.push('Magento_Sales/order/create/form'); - <?php endif; ?> + if (strpos($block->getRowClickCallback(), 'order.') !== false) { + $scriptString .= 'deps.push(\'Magento_Sales/order/create/form\');' . PHP_EOL; + } -deps.push('mage/adminhtml/grid'); + $scriptString .= 'deps.push(\'mage/adminhtml/grid\');' . PHP_EOL; -require(deps, function(<?= ($block->getDependencyJsObject() ? 'registry' : '') ?>){ - <?php //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed ?> + $scriptString .= 'require(deps, function('. ($block->getDependencyJsObject() ? 'registry' : '') .'){' . + PHP_EOL . + '//TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed' . PHP_EOL; - <?php if ($block->getDependencyJsObject()) : ?> - registry.get('<?= $block->escapeJs($block->getDependencyJsObject()) ?>', function (<?= $block->escapeJs($block->getDependencyJsObject()) ?>) { - <?php endif; ?> + if ($block->getDependencyJsObject()) { + $scriptString .= 'registry.get(\'' . $block->escapeJs($block->getDependencyJsObject()) . + '\', function ('. $block->escapeJs($block->getDependencyJsObject()) . ') {' . PHP_EOL; + } - <?= $block->escapeJs($block->getJsObjectName()) ?> = new varienGrid('<?= $block->escapeJs($block->getId()) ?>', '<?= $block->escapeJs($block->getGridUrl()) ?>', '<?= $block->escapeJs($block->getVarNamePage()) ?>', '<?= $block->escapeJs($block->getVarNameSort()) ?>', '<?= $block->escapeJs($block->getVarNameDir()) ?>', '<?= $block->escapeJs($block->getVarNameFilter()) ?>'); - <?= $block->escapeJs($block->getJsObjectName()) ?>.useAjax = '<?= $block->escapeJs($block->getUseAjax()) ?>'; - <?php if ($block->getRowClickCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.rowClickCallback = <?= /* @noEscape */ $block->getRowClickCallback() ?>; - <?php endif; ?> - <?php if ($block->getCheckboxCheckCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; - <?php endif; ?> - <?php if ($block->getRowInitCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; - <?= $block->escapeJs($block->getJsObjectName()) ?>.rows.each(function(row){<?= /* @noEscape */ $block->getRowInitCallback() ?>(<?= $block->escapeJs($block->getJsObjectName()) ?>, row)}); - <?php endif; ?> - <?php if ($block->getMassactionBlock()->isAvailable()) : ?> - <?= /* @noEscape */ $block->getMassactionBlock()->getJavaScript() ?> - <?php endif ?> + $scriptString .= $block->escapeJs($block->getJsObjectName()) . ' = new varienGrid(\'' . + $block->escapeJs($block->getId()) . '\', \'' . $block->escapeJs($block->getGridUrl()) . '\', \'' . + $block->escapeJs($block->getVarNamePage()) .'\', \'' . + $block->escapeJs($block->getVarNameSort()) . '\', \'' . + $block->escapeJs($block->getVarNameDir()) . '\', \'' . + $block->escapeJs($block->getVarNameFilter()) .'\');' . PHP_EOL; - <?php if ($block->getDependencyJsObject()) : ?> - }); - <?php endif; ?> + $scriptString .= $block->escapeJs($block->getJsObjectName()) .'.useAjax = \'' . + $block->escapeJs($block->getUseAjax()) . '\';' . PHP_EOL; + if ($block->getRowClickCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.rowClickCallback = ' . + /* @noEscape */ $block->getRowClickCallback() . ';' . PHP_EOL; + } + + if ($block->getCheckboxCheckCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.checkboxCheckCallback = ' . + /* @noEscape */ $block->getCheckboxCheckCallback() . ';' . PHP_EOL; + } + + if ($block->getRowInitCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.initRowCallback = ' . + /* @noEscape */ $block->getRowInitCallback() . ';' . PHP_EOL; + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.rows.each(function(row){' . + /* @noEscape */ $block->getRowInitCallback() . '(' . $block->escapeJs($block->getJsObjectName()) . + ', row)});' . PHP_EOL; + } -}); -</script> -<?php endif; ?> + if ($block->getMassactionBlock()->isAvailable()) { + $scriptString .= /* @noEscape */ $block->getMassactionBlock()->getJavaScript(); + } + + if ($block->getDependencyJsObject()) { + $scriptString .= '});' . PHP_EOL; + } + + $scriptString .= '});' . PHP_EOL; + + echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); + ?> + <?php endif; ?> <?php endif ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml index 87e5399ddda44..7cc9b781f579e 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml @@ -3,35 +3,50 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <p class="switcher"><label for="store_switcher"><?= $block->escapeHtml(__('View Statistics For:')) ?></label> <?= $block->getHintHtml() ?> -<select name="store_switcher" id="store_switcher" class="left-col-block" onchange="return switchStore(this);"> +<select name="store_switcher" id="store_switcher" class="left-col-block"> <option value=""><?= $block->escapeHtml(__('All Websites')) ?></option> - <?php foreach ($block->getWebsiteCollection() as $_website) : ?> + <?php foreach ($block->getWebsiteCollection() as $_website): ?> <?php $showWebsite = false; ?> - <?php foreach ($block->getGroupCollection($_website) as $_group) : ?> + <?php foreach ($block->getGroupCollection($_website) as $_group): ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStoreCollection($_group) as $_store) : ?> - <?php if ($showWebsite == false) : ?> + <?php foreach ($block->getStoreCollection($_group) as $_store): ?> + <?php if ($showWebsite == false): ?> <?php $showWebsite = true; ?> - <option website="true" value="<?= $block->escapeHtmlAttr($_website->getId()) ?>"<?php if ($block->getRequest()->getParam('website') == $_website->getId()) : ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_website->getName()) ?></option> + <option website="true" + value="<?= $block->escapeHtmlAttr($_website->getId()) ?>"<?php + if ($block->getRequest()->getParam('website') == $_website->getId()): + ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_website->getName()) ?> + </option> <?php endif; ?> - <?php if ($showGroup == false) : ?> + <?php if ($showGroup == false): ?> <?php $showGroup = true; ?> <!--optgroup label="   <?= $block->escapeHtmlAttr($_group->getName()) ?>"--> - <option group="true" value="<?= $block->escapeHtmlAttr($_group->getId()) ?>"<?php if ($block->getRequest()->getParam('group') == $_group->getId()) : ?> selected="selected"<?php endif; ?>>   <?= $block->escapeHtml($_group->getName()) ?></option> + <option group="true" value="<?= $block->escapeHtmlAttr($_group->getId()) ?>"<?php + if ($block->getRequest()->getParam('group') == $_group->getId()): ?> selected="selected"<?php + endif; ?>>   <?= $block->escapeHtml($_group->getName()) ?></option> <?php endif; ?> - <option value="<?= $block->escapeHtmlAttr($_store->getId()) ?>"<?php if ($block->getStoreId() == $_store->getId()) : ?> selected="selected"<?php endif; ?>>      <?= $block->escapeHtml($_store->getName()) ?></option> + <option value="<?= $block->escapeHtmlAttr($_store->getId()) ?>"<?php + if ($block->getStoreId() == $_store->getId()): ?> selected="selected"<?php + endif; ?>>      <?= $block->escapeHtml($_store->getName()) ?></option> <?php endforeach; ?> - <?php if ($showGroup) : ?> + <?php if ($showGroup): ?> <!--</optgroup>--> <?php endif; ?> <?php endforeach; ?> <?php endforeach; ?> </select> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'return switchStore(this);', + 'select#store_switcher' +) ?> </p> -<script> +<?php $scriptString = <<<script require([ 'prototype' ], function () { @@ -54,7 +69,9 @@ var select = $('order_amounts_period'); } var periodParam = select.value ? 'period/' + select.value + '/' : ''; - setLocation('<?= $block->escapeJs($block->getSwitchUrl()) ?>' + storeParam + periodParam); + setLocation('{$block->escapeJs($block->getSwitchUrl())}' + storeParam + periodParam); } }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml index 94df9ef9eb872..1ea5e225c8402 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml @@ -11,9 +11,10 @@ * * @see \Magento\Framework\View\Element\Html\Calendar */ -?> -<script> +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$scriptString = ' require([ "jquery", "jquery/ui" @@ -21,20 +22,20 @@ require([ $.extend(true, $, { calendarConfig: { - dayNames: <?= /* @noEscape */ $days['wide'] ?>, - dayNamesMin: <?= /* @noEscape */ $days['abbreviated'] ?>, - monthNames: <?= /* @noEscape */ $months['wide'] ?>, - monthNamesShort: <?= /* @noEscape */ $months['abbreviated'] ?>, - infoTitle: "<?= $block->escapeJs(__('About the calendar')) ?>", - firstDay: <?= /* @noEscape */ $firstDay ?>, - closeText: "<?= $block->escapeJs(__('Close')) ?>", - currentText: "<?= $block->escapeJs(__('Go Today')) ?>", - prevText: "<?= $block->escapeJs(__('Previous')) ?>", - nextText: "<?= $block->escapeJs(__('Next')) ?>", - weekHeader: "<?= $block->escapeJs(__('WK')) ?>", - timeText: "<?= $block->escapeJs(__('Time')) ?>", - hourText: "<?= $block->escapeJs(__('Hour')) ?>", - minuteText: "<?= $block->escapeJs(__('Minute')) ?>", + dayNames: ' . /* @noEscape */ $days['wide'] . ', + dayNamesMin: ' . /* @noEscape */ $days['abbreviated'] . ', + monthNames: ' . /* @noEscape */ $months['wide'] . ', + monthNamesShort: ' . /* @noEscape */ $months['abbreviated'] . ', + infoTitle: "' . $block->escapeJs(__('About the calendar')) . '", + firstDay: ' . /* @noEscape */ $firstDay . ', + closeText: "' . $block->escapeJs(__('Close')) . '", + currentText: "' . $block->escapeJs(__('Go Today')) . '", + prevText: "' . $block->escapeJs(__('Previous')) . '", + nextText: "' . $block->escapeJs(__('Next')) . '", + weekHeader: "' . $block->escapeJs(__('WK')) . '", + timeText: "' . $block->escapeJs(__('Time')) . '", + hourText: "' . $block->escapeJs(__('Hour')) . '", + minuteText: "' . $block->escapeJs(__('Minute')) . '", dateFormat: $.datepicker.RFC_2822, showOn: "button", showAnim: "", @@ -45,17 +46,18 @@ require([ showButtonPanel: true, showOtherMonths: true, showWeek: false, - timeFormat: '', + timeFormat: \'\', showTime: false, showHour: false, showMinute: false, - serverTimezoneSeconds: <?= (int) $block->getStoreTimestamp() ?>, - serverTimezoneOffset: <?= (int) $block->getTimezoneOffsetSeconds() ?>, - yearRange: '<?= $block->escapeJs($block->getYearRange()) ?>' + serverTimezoneSeconds: ' . (int) $block->getStoreTimestamp() . ', + serverTimezoneOffset: ' . (int) $block->getTimezoneOffsetSeconds() . ', + yearRange: \'' . $block->escapeJs($block->getYearRange()) . '\' } }); -enUS = <?= /* @noEscape */ $enUS ?>; // en_US locale reference +enUS = ' . /* @noEscape */ $enUS . '; // en_US locale reference + +});'; -}); -</script> +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml index 68453d9ff8ff2..6fa41e1079950 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<script> - var BASE_URL = '<?= /* @noEscape */ $block->getUrl('*') ?>'; - var FORM_KEY = '<?= /* @noEscape */ $block->getFormKey() ?>'; + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$scriptString = ' + var BASE_URL = \'' . /* @noEscape */ $block->getUrl('*') . '\'; + var FORM_KEY = \'' . /* @noEscape */ $block->getFormKey() . '\'; var require = { - "baseUrl": "<?= /* @noEscape */ $block->getViewFileUrl('/') ?>" - }; -</script> + \'baseUrl\': \'' . /* @noEscape */ $block->getViewFileUrl('/') . '\' + };'; + +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml index df9323a7276df..c6fcaff9cd877 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml @@ -5,24 +5,39 @@ */ /* @var $block \Magento\Backend\Block\Store\Switcher */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($websites = $block->getWebsites()) : ?> - +<?php if ($websites = $block->getWebsites()): ?> <div class="store-switcher store-view"> <span class="store-switcher-label"><?= $block->escapeHtml(__('Scope:')) ?></span> <div class="actions dropdown closable"> <input type="hidden" name="store_switcher" id="store_switcher" data-role="store-view-id" data-param="<?= $block->escapeHtmlAttr($block->getStoreVarName()) ?>" value="<?= $block->escapeHtml($block->getStoreId()) ?>" - onchange="switchScope(this);"<?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'switchScope(this);', + '#store_switcher' + ) ?> <input type="hidden" name="store_group_switcher" id="store_group_switcher" data-role="store-group-id" data-param="<?= $block->escapeHtmlAttr($block->getStoreGroupVarName()) ?>" value="<?= $block->escapeHtml($block->getStoreGroupId()) ?>" - onchange="switchScope(this);"<?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'switchScope(this);', + '#store_group_switcher' + ) ?> <input type="hidden" name="website_switcher" id="website_switcher" data-role="website-id" data-param="<?= $block->escapeHtmlAttr($block->getWebsiteVarName()) ?>" value="<?= $block->escapeHtml($block->getWebsiteId()) ?>" - onchange="switchScope(this);"<?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'switchScope(this);', + '#website_switcher' + ) ?> <button type="button" class="admin__action-dropdown" @@ -33,61 +48,75 @@ <?= $block->escapeHtml($block->getCurrentSelectionName()) ?> </button> <ul class="dropdown-menu" data-role="stores-list"> - <?php if ($block->hasDefaultOption()) : ?> - <li class="store-switcher-all <?php if (!($block->getDefaultSelectionName() != $block->getCurrentSelectionName())) : ?>disabled<?php endif; ?> <?php if (!$block->hasScopeSelected()) : ?>current<?php endif; ?>"> - <?php if ($block->getDefaultSelectionName() != $block->getCurrentSelectionName()) : ?> + <?php if ($block->hasDefaultOption()): ?> + <li class="store-switcher-all <?php + if (!($block->getDefaultSelectionName() != $block->getCurrentSelectionName())): ?>disabled<?php endif; + ?> <?php if (!$block->hasScopeSelected()): ?>current<?php endif; ?>"> + <?php if ($block->getDefaultSelectionName() != $block->getCurrentSelectionName()): ?> <a data-role="store-view-id" data-value="" href="#"> <?= $block->escapeHtml($block->getDefaultSelectionName()) ?> </a> - <?php else : ?> + <?php else: ?> <span><?= $block->escapeHtml($block->getDefaultSelectionName()) ?></span> <?php endif; ?> </li> <?php endif; ?> - <?php foreach ($websites as $website) : ?> + <?php foreach ($websites as $website): ?> <?php $showWebsite = false; ?> - <?php foreach ($website->getGroups() as $group) : ?> + <?php foreach ($website->getGroups() as $group): ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStores($group) as $store) : ?> - <?php if ($showWebsite == false) : ?> + <?php foreach ($block->getStores($group) as $store): ?> + <?php if ($showWebsite == false): ?> <?php $showWebsite = true; ?> - <li class="store-switcher-website <?php if (!($block->isWebsiteSwitchEnabled() && ! $block->isWebsiteSelected($website))) : ?>disabled<?php endif; ?> <?php if ($block->isWebsiteSelected($website)) : ?>current<?php endif; ?>"> - <?php if ($block->isWebsiteSwitchEnabled() && ! $block->isWebsiteSelected($website)) : ?> - <a data-role="website-id" data-value="<?= $block->escapeHtml($website->getId()) ?>" href="#"> + <li class="store-switcher-website <?php if (!($block->isWebsiteSwitchEnabled() && + ! $block->isWebsiteSelected($website))): ?>disabled<?php endif; ?> <?php +if ($block->isWebsiteSelected($website)): ?>current<?php endif; ?>"> + <?php if ($block->isWebsiteSwitchEnabled() && ! $block->isWebsiteSelected($website)): ?> + <a data-role="website-id" data-value="<?= $block->escapeHtmlAttr($website->getId()); + ?>" href="#"> <?= $block->escapeHtml($website->getName()) ?> </a> - <?php else : ?> + <?php else: ?> <span><?= $block->escapeHtml($website->getName()) ?></span> <?php endif; ?> </li> <?php endif; ?> - <?php if ($showGroup == false) : ?> + <?php if ($showGroup == false): ?> <?php $showGroup = true; ?> - <li class="store-switcher-store <?php if (!($block->isStoreGroupSwitchEnabled() && ! $block->isStoreGroupSelected($group))) : ?>disabled<?php endif; ?> <?php if ($block->isStoreGroupSelected($group)) : ?>current<?php endif; ?>"> - <?php if ($block->isStoreGroupSwitchEnabled() && ! $block->isStoreGroupSelected($group)) : ?> - <a data-role="store-group-id" data-value="<?= $block->escapeHtml($group->getId()) ?>" href="#"> + <li class="store-switcher-store <?php if (!($block->isStoreGroupSwitchEnabled() && + ! $block->isStoreGroupSelected($group))): ?>disabled<?php endif; ?> <?php +if ($block->isStoreGroupSelected($group)): ?>current<?php endif; ?>"> + <?php if ($block->isStoreGroupSwitchEnabled() && + ! $block->isStoreGroupSelected($group)): ?> + <a data-role="store-group-id" + data-value="<?= $block->escapeHtmlAttr($group->getId()) ?>" href="#"> <?= $block->escapeHtml($group->getName()) ?> </a> - <?php else : ?> + <?php else: ?> <span><?= $block->escapeHtml($group->getName()) ?></span> <?php endif; ?> </li> <?php endif; ?> - <li class="store-switcher-store-view <?php if (!($block->isStoreSwitchEnabled() && !$block->isStoreSelected($store))) : ?>disabled<?php endif; ?> <?php if ($block->isStoreSelected($store)) :?>current<?php endif; ?>"> - <?php if ($block->isStoreSwitchEnabled() && ! $block->isStoreSelected($store)) : ?> - <a data-role="store-view-id" data-value="<?= $block->escapeHtml($store->getId()) ?>" href="#"> + <li class="store-switcher-store-view <?php if (!($block->isStoreSwitchEnabled() && + !$block->isStoreSelected($store))): ?>disabled<?php endif; ?> <?php +if ($block->isStoreSelected($store)):?>current<?php endif; ?>"> + <?php if ($block->isStoreSwitchEnabled() && ! $block->isStoreSelected($store)): ?> + <a data-role="store-view-id" + data-value="<?= $block->escapeHtmlAttr($store->getId()) ?>" href="#"> <?= $block->escapeHtml($store->getName()) ?> </a> - <?php else : ?> + <?php else: ?> <span><?= $block->escapeHtml($store->getName()) ?></span> <?php endif; ?> </li> <?php endforeach; ?> <?php endforeach; ?> <?php endforeach; ?> - <?php if ($block->getShowManageStoresLink() && $block->getAuthorization()->isAllowed('Magento_Backend::store')) : ?> + <?php if ($block->getShowManageStoresLink() && + $block->getAuthorization()->isAllowed('Magento_Backend::store')): ?> <li class="dropdown-toolbar"> - <a href="<?= /* @noEscape */ $block->getUrl('*/system_store') ?>"><?= $block->escapeHtml(__('Stores Configuration')) ?></a> + <a href="<?= /* @noEscape */ $block->getUrl('*/system_store'); + ?>"><?= $block->escapeHtml(__('Stores Configuration')) ?></a> </li> <?php endif; ?> </ul> @@ -95,15 +124,17 @@ <?= $block->getHintHtml() ?> </div> -<script> + <?php + $useConfirm = (int)$block->getUseConfirm(); + $scriptString = <<<script require([ 'jquery', 'Magento_Ui/js/modal/confirm' ], function(jQuery, confirm){ (function($) { - var $storesList = $('[data-role=stores-list]'); - $storesList.on('click', '[data-value]', function(event) { + var storesList = $('[data-role=stores-list]'); + storesList.on('click', '[data-value]', function(event) { var val = $(event.target).data('value'); var role = $(event.target).data('role'); var switcher = $('[data-role='+role+']'); @@ -134,35 +165,42 @@ require([ var switcherParams = { scopeId: scopeId, scopeParams: scopeParams, - useConfirm: <?= (int)$block->getUseConfirm() ?> + useConfirm: {$useConfirm} }; scopeSwitcherHandler(switcherParams); } else { - - <?php if ($block->getUseConfirm()) : ?> - +script; + if ($block->getUseConfirm()) { + $scriptString .= ' confirm({ - content: "<?= $block->escapeJs(__('Please confirm scope switching. All data that hasn\'t been saved will be lost.')) ?>", + content: "' . $block->escapeJs(__( + 'Please confirm scope switching. All data that hasn\'t been saved will be lost.' + )) . '", actions: { confirm: function() { reload(); }, cancel: function() { - obj.value = '<?= $block->escapeHtml($block->getStoreId()) ?>'; + obj.value = \'' . $block->escapeJs($block->getStoreId()) . '\'; } } }); - - <?php else : ?> - reload(); - <?php endif; ?> +'; + } else { + $scriptString .= 'reload();'; + } + $scriptString .= ' } function reload() { - <?php if (!$block->isUsingIframe()) : ?> - var url = '<?= $block->escapeJs($block->getSwitchUrl()) ?>' + scopeParams; - setLocation(url); - <?php else : ?> + '; + if (!$block->isUsingIframe()) { + $scriptString .= ' + var url = \'' . $block->escapeJs($block->getSwitchUrl()) . '\' + scopeParams; + setLocation(url); +'; + } else { + $scriptString .= <<<script jQuery('#preview_selected_store').val(scopeId); jQuery('#preview_form').submit(); @@ -175,7 +213,9 @@ require([ }); jQuery('#store-change-button').click(); - <?php endif; ?> +script; + } + $scriptString .= <<<script } } @@ -183,5 +223,7 @@ require([ window.switchScope = switchScope; }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml index 959a27279e5c2..eb64cc602eaf9 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml @@ -21,19 +21,25 @@ $fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' . $block->getUiId('form-field', $element->getId()); ?> -<?php if (!$element->getNoDisplay()) : ?> - <?php if ($element->getType() == 'hidden') : ?> +<?php if (!$element->getNoDisplay()): ?> + <?php if ($element->getType() == 'hidden'): ?> <?= $element->getElementHtml() ?> - <?php else : ?> - <div<?= /* @noEscape */ $fieldAttributes ?>> - <?php if ($elementBeforeLabel) : ?> + <?php else: ?> + <div <?= /* @noEscape */ $fieldAttributes ?>> + <?php if ($elementBeforeLabel): ?> <?= $element->getElementHtml() ?> <?= $element->getLabelHtml('', $element->getScopeLabel()) ?> <?= /* @noEscape */ $note ?> - <?php else : ?> + <?php else: ?> <?= $element->getLabelHtml('', $element->getScopeLabel()) ?> <div class="admin__field-control control"> - <?= /* @noEscape */ ($addOn) ? '<div class="addon">' . $element->getElementHtml() . '</div>' : $element->getElementHtml() ?> + <?php if ($addOn): ?> + <div class="addon"> + <?php endif; ?> + <?= /* @noEscape */ $element->getElementHtml() ?> + <?php if ($addOn): ?> + </div> + <?php endif; ?> <?= $block->getHintHtml() ?> <?= /* @noEscape */ $note ?> </div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml index c392ebf3883d2..4b80d2863ad93 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml @@ -5,34 +5,51 @@ */ /** @var \Magento\Backend\Block\Cache\Permissions|null $permissions */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $permissions = $block->getData('permissions'); ?> -<?php if ($permissions && $permissions->hasAccessToAdditionalActions()) : ?> +<?php if ($permissions && $permissions->hasAccessToAdditionalActions()): ?> <div class="additional-cache-management"> <h2> <span><?= $block->escapeHtml(__('Additional Cache Management')); ?></span> </h2> - <?php if ($permissions->hasAccessToFlushCatalogImages()) : ?> + <?php if ($permissions->hasAccessToFlushCatalogImages()): ?> <p> - <button onclick="setLocation('<?= $block->escapeJs($block->getCleanImagesUrl()); ?>')" type="button"> + <button id="flushCatalogImages" type="button"> <?= $block->escapeHtml(__('Flush Catalog Images Cache')); ?> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'setLocation(\'' . $block->escapeJs($block->getCleanImagesUrl()) . '\')', + 'button#flushCatalogImages' + ) ?> <span><?= $block->escapeHtml(__('Pregenerated product images files')); ?></span> </p> <?php endif; ?> - <?php if ($permissions->hasAccessToFlushJsCss()) : ?> + <?php if ($permissions->hasAccessToFlushJsCss()): ?> <p> - <button onclick="setLocation('<?= $block->escapeJs($block->getCleanMediaUrl()); ?>')" type="button"> + <button id="flushJsCss" type="button"> <?= $block->escapeHtml(__('Flush JavaScript/CSS Cache')); ?> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'setLocation(\'' . $block->escapeJs($block->getCleanMediaUrl()) . '\')', + 'button#flushJsCss' + ) ?> <span><?= $block->escapeHtml(__('Themes JavaScript and CSS files combined to one file')) ?></span> </p> <?php endif; ?> - <?php if (!$block->isInProductionMode() && $permissions->hasAccessToFlushStaticFiles()) : ?> + <?php if (!$block->isInProductionMode() && $permissions->hasAccessToFlushStaticFiles()): ?> <p> - <button onclick="setLocation('<?= $block->escapeJs($block->getCleanStaticFilesUrl()); ?>')" type="button"> + <button id="flushStaticFiles" type="button"> <?= $block->escapeHtml(__('Flush Static Files Cache')); ?> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'setLocation(\'' . $block->escapeJs($block->getCleanStaticFilesUrl()) . '\')', + 'button#flushStaticFiles' + ) ?> <span><?= $block->escapeHtml(__('Preprocessed view files and static files')); ?></span> </p> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml index d1c51f0755a72..753cc7ceee356 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php /** @@ -14,16 +16,19 @@ */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"><?= $block->getSaveButtonHtml() ?></div> -<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="config-edit-form" enctype="multipart/form-data"> +<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="config-edit-form" + enctype="multipart/form-data"> <?= $block->getBlockHtml('formkey') ?> - <script> + <?php $scriptString = <<<script window.setCacheAction = function(id, button) { document.getElementById(id).value = button.id; configForm.submit(); } - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <input type="hidden" id="catalog_action" name="catalog_action" value="" /> <input type="hidden" id="jscss_action" name="jscss_action" value="" /> @@ -36,7 +41,7 @@ <fieldset id="catalog"> <table class="form-list"> <tbody> - <?php foreach ($block->getCatalogData() as $_item) : ?> + <?php foreach ($block->getCatalogData() as $_item): ?> <?php /* disable reindex buttons. functionality moved to index management*/?> <?php if ($_item['buttons'][0]['name'] != 'clear_images_cache') { @@ -46,15 +51,31 @@ <tr> <td class="label"><label><?= $block->escapeHtml($_item['label']) ?></label></td> <td class="value"> - <?php foreach ($_item['buttons'] as $_button) : ?> + <?php foreach ($_item['buttons'] as $_button): ?> <?php $clickAction = "setCacheAction('catalog_action',this)"; ?> - <?php if (isset($_button['warning']) && $_button['warning']) : ?> + <?php if (isset($_button['warning']) && $_button['warning']): ?> <?php //phpcs:disable ?> - <?php $clickAction = "if (confirm('" . addslashes($_button['warning']) . "')) {{$clickAction}}"; ?> + <?php $clickAction = "if (confirm('" . addslashes($_button['warning']) . + "')) {{$clickAction}}"; ?> <?php //phpcs:enable ?> <?php endif; ?> - <button <?php if (!isset($_button['disabled']) || !$_button['disabled']) :?>onclick="<?= /* @noEscape */ $clickAction ?>"<?php endif; ?> id="<?= $block->escapeHtmlAttr($_button['name']) ?>" type="button" class="scalable <?php if (isset($_button['disabled']) && $_button['disabled']) :?>disabled<?php endif; ?>" style=""><span><span><span><?= $block->escapeHtml($_button['action']) ?></span></span></span></button> - <?php if (isset($_button['comment'])) : ?> <br /> <small><?= $block->escapeHtml($_button['comment']) ?></small> <?php endif; ?> + <button + id="<?= $block->escapeHtmlAttr($_button['name']) ?>" + type="button" + class="scalable + <?php if (isset($_button['disabled']) && $_button['disabled']):?>disabled<?php endif;?>" + ><span><span><span><?= $block->escapeHtml($_button['action']) ?></span></span></span> + </button> + <?php if (!isset($_button['disabled']) || !$_button['disabled']):?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + /* @noEscape */ $clickAction, + '#' . $block->escapeJs($_button['name']) + ) ?> + <?php endif; ?> + <?php if (isset($_button['comment'])): ?> <br /> + <small><?= $block->escapeHtml($_button['comment']) ?></small> + <?php endif; ?> <?php endforeach; ?> </td> <td><small> </small></td> @@ -76,7 +97,16 @@ <tr> <td class="label"><label><?= $block->escapeHtml(__('JavaScript/CSS Cache')) ?></label></td> <td class="value"> - <button onclick="setCacheAction('jscss_action', this)" id='jscss_action' type="button" class="scalable"><span><span><span><?= $block->escapeHtml(__('Clear')) ?></span></span></span></button> + <button id='jscss_action' + type="button" + class="scalable"> + <span><span><span><?= $block->escapeHtml(__('Clear')) ?></span></span></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "setCacheAction('jscss_action', this)", + '#jscss_action' + ) ?> </td> </tr> </tbody> @@ -84,8 +114,11 @@ </fieldset> </div> </form> -<script> +<?php $scriptString = <<<script require(["jquery","mage/mage"],function($){ $('#config-edit-form').mage('form').mage('validation'); }); -</script> +script; +?> + +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml index c9cd765de35be..2d68012b2c5dc 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="design-edit-form"> <?= $block->getBlockHtml('formkey') ?> </form> -<script> + +<?php $scriptString = <<<script require([ "jquery", "mage/mage" @@ -16,4 +19,6 @@ require([ $('#design-edit-form').mage('form').mage('validation'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/shipping/applicable_country.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/shipping/applicable_country.phtml index 2a43baa4e24c8..0846fd2020b36 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/shipping/applicable_country.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/shipping/applicable_country.phtml @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script> + +<?php $scriptString = <<<script require([ 'prototype' ], function () { @@ -115,4 +120,6 @@ CountryModel.prototype = { } countryApply = new CountryModel(); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml index fecf5365544e0..dfee490379dd1 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php /** @@ -10,17 +12,20 @@ */ $items = $block->getItems(); ?> -<?php if (!empty($items)) : ?> +<?php if (!empty($items)): ?> <dl id="tab_content_<?= $block->getHtmlId() ?>" name="tab_content_<?= $block->getHtmlId() ?>" class="accordion"> - <?php foreach ($items as $_item) : ?> + <?php foreach ($items as $_item): ?> <?= $block->getChildHtml($_item->getId()) ?> <?php endforeach ?> </dl> - <script> + <?php $scriptString = <<<script require([ 'mage/adminhtml/accordion' ], function(){ - tab_content_<?= $block->getHtmlId() ?>AccordionJs = new varienAccordion('tab_content_<?= $block->getHtmlId() ?>', '<?= $block->escapeJs($block->getShowOnlyOne()) ?>'); + tab_content_{$block->getHtmlId()}AccordionJs = new varienAccordion('tab_content_{$block->getHtmlId()}', + '{$block->escapeJs($block->getShowOnlyOne())}'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml index 0123de098a9e0..6ebbd4118e7a0 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml @@ -12,19 +12,19 @@ <button <?= $block->getButtonAttributesHtml() ?>> <span><?= $block->escapeHtml($block->getLabel()) ?></span> </button> - <?php if ($block->hasSplit()) : ?> + <?php if ($block->hasSplit()): ?> <button <?= $block->getToggleAttributesHtml() ?>> <span>Select</span> </button> - <?php if (!$block->getDisabled()) : ?> + <?php if (!$block->getDisabled()): ?> <ul class="dropdown-menu" <?= /* @noEscape */ $block->getUiId("dropdown-menu") ?>> - <?php foreach ($block->getOptions() as $key => $option) : ?> + <?php foreach ($block->getOptions() as $key => $option): ?> <li> <span <?= $block->getOptionAttributesHtml($key, $option) ?>> <?= $block->escapeHtml($option['label']) ?> </span> - <?php if (isset($option['hint'])) : ?> + <?php if (isset($option['hint'])): ?> <div class="tooltip" <?= /* @noEscape */ $block->getUiId('item', $key, 'tooltip') ?>> <a href="<?= $block->escapeHtml($option['hint']['href']) ?>" class="help"> <?= $block->escapeHtml($option['hint']['label']) ?> @@ -37,6 +37,7 @@ <?php endif; ?> <?php endif; ?> </div> +<?= /* @noEscape */$block->getAfterHtml() ?> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml index aa289dbf1eb0f..08ec331e37b7d 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml @@ -5,12 +5,15 @@ */ /** @var $block \Magento\Backend\Block\Widget\Form\Container */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= /* @noEscape */ $block->getFormInitScripts() ?> -<?php if ($block->getButtonsHtml('header')) : ?> - <div class="page-form-actions" <?= /* @noEscape */ $block->getUiId('content-header') ?>><?= $block->getButtonsHtml('header') ?></div> +<?php if ($block->getButtonsHtml('header')): ?> + <div class="page-form-actions" <?= /* @noEscape */ $block->getUiId('content-header') ?>> + <?= $block->getButtonsHtml('header') ?> + </div> <?php endif; ?> -<?php if ($block->getButtonsHtml('toolbar')) : ?> +<?php if ($block->getButtonsHtml('toolbar')): ?> <div class="page-main-actions"> <div class="page-actions"> <div class="page-actions-buttons"> @@ -20,12 +23,13 @@ </div> <?php endif; ?> <?= $block->getFormHtml() ?> -<?php if ($block->hasFooterButtons()) : ?> +<?php if ($block->hasFooterButtons()): ?> <div class="content-footer"> <p class="form-buttons"><?= $block->getButtonsHtml('footer') ?></p> </div> <?php endif; ?> -<script> +<?php $scriptString = <<<script + require([ 'jquery', 'mage/backend/form', @@ -34,7 +38,7 @@ require([ $('#edit_form').form() .validation({ - validationUrl: '<?= $block->escapeJs($block->getValidationUrl()) ?>', + validationUrl: '{$block->escapeJs($block->getValidationUrl())}', highlight: function(element) { var detailsElement = $(element).closest('details'); if (detailsElement.length && detailsElement.is('.details')) { @@ -48,5 +52,8 @@ require([ }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?= /* @noEscape */ $block->getFormScripts() ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml index ec53f7e5c74ce..299f53dc9e3ef 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml @@ -4,47 +4,87 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $type = $element->getType(); +$htmlId = $element->getHtmlId(); ?> -<?php if ($type === 'fieldset') : ?> +<?php if ($type === 'fieldset'): ?> <fieldset> <legend><?= $block->escapeHtml($element->getLegend()) ?></legend><br /> - <?php foreach ($element->getElements() as $_element) : ?> + <?php foreach ($element->getElements() as $_element): ?> <?= /* @noEscape */ $formBlock->drawElement($_element) ?> <?php endforeach; ?> </fieldset> -<?php elseif ($type === 'hidden') : ?> - <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= $element->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($element->getValue()) ?>"> - <?php elseif ($type === 'select') : ?> +<?php elseif ($type === 'hidden'): ?> + <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" + name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + value="<?= $block->escapeHtmlAttr($element->getValue()) ?>"> + <?php elseif ($type === 'select'): ?> <span class="form_row"> - <?php if ($element->getLabel()) : ?><label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label><?php endif; ?> - <select name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= $element->getHtmlId() ?>" class="select<?= $block->escapeHtmlAttr($element->getClass()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>"> - <?php foreach ($element->getValues() as $_value) : ?> - <option <?= /* @noEscape */ $_value->serialize() ?><?php if ($_value->getValue() == $element->getValue()) : ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_value->getLabel()) ?></option> + <?php if ($element->getLabel()): ?> + <label for="<?= /* @noEscape */ $htmlId ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> + <?php endif; ?> + <select name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + class="select<?= $block->escapeHtmlAttr($element->getClass()) ?>" + title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>"> + <?php foreach ($element->getValues() as $_value): ?> + <option <?= /* @noEscape */ $_value->serialize() ?> + <?php if ($_value->getValue() == $element->getValue()): ?> selected="selected"<?php endif; ?>> + <?= $block->escapeHtml($_value->getLabel()) ?> + </option> <?php endforeach; ?> </select> </span> -<?php elseif ($type === 'text' || $type === 'button' || $type === 'password') : ?> +<?php elseif ($type === 'text' || $type === 'button' || $type === 'password'): ?> <span class="form_row"> - <?php if ($element->getLabel()) : ?><label for="<?= $element->getHtmlId() ?>" <?= /* @noEscape */ $block->getUiId('label') ?>><?= $block->escapeHtml($element->getLabel()) ?>:</label><?php endif; ?> - <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= /* @noEscape */ $element->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($element->getValue()) ?>" class="input-text <?= $block->escapeHtmlAttr($element->getClass()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" <?= /* @noEscape */ ($element->getOnClick() ? 'onClick="' . $element->getOnClick() . '"' : '') ?>/> + <?php if ($element->getLabel()): ?> + <label for="<?= /* @noEscape */ $htmlId ?>" <?= /* @noEscape */ $block->getUiId('label') ?>> + <?= $block->escapeHtml($element->getLabel()) ?>: + </label> + <?php endif; ?> + <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" + name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + value="<?= $block->escapeHtmlAttr($element->getValue()) ?>" + class="input-text <?= $block->escapeHtmlAttr($element->getClass()) ?>" + title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" /> + <?php if ($listener = $element->getOnclick()): ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag('onclick', $listener, "#{$htmlId}") ?> + <?php endif; ?> </span> -<?php elseif ($type === 'radio') : ?> +<?php elseif ($type === 'radio'): ?> <span class="form_row"> - <?php if ($element->getLabel()) : ?><label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label><?php endif; ?> - <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= $element->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($element->getValue()) ?>" class="input-text <?= $block->escapeHtmlAttr($element->getClass()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>"/> + <?php if ($element->getLabel()): ?> + <label for="<?= /* @noEscape */ $htmlId ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> + <?php endif; ?> + <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" + name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + value="<?= $block->escapeHtmlAttr($element->getValue()) ?>" + class="input-text <?= $block->escapeHtmlAttr($element->getClass()) ?>" + title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>"/> </span> -<?php elseif ($type === 'radios') : ?> +<?php elseif ($type === 'radios'): ?> <span class="form_row"> - <label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> - <?php foreach ($element->getRadios() as $_radio) : ?> - <input type="radio" name="<?= $block->escapeHtmlAttr($_radio->getName()) ?>" id="<?= $_radio->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($_radio->getValue()) ?>" class="input-radio <?= $block->escapeHtmlAttr($_radio->getClass()) ?>" title="<?= $block->escapeHtmlAttr($_radio->getTitle()) ?>" <?= ($_radio->getValue() == $element->getChecked()) ? 'checked="true"' : '' ?> > <?= $block->escapeHtml($_radio->getLabel()) ?> + <label for="<?= /* @noEscape */ $htmlId ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> + <?php foreach ($element->getRadios() as $_radio): ?> + <input type="radio" + name="<?= $block->escapeHtmlAttr($_radio->getName()) ?>" + id="<?= $_radio->getHtmlId() ?>" + value="<?= $block->escapeHtmlAttr($_radio->getValue()) ?>" + class="input-radio <?= $block->escapeHtmlAttr($_radio->getClass()) ?>" + title="<?= $block->escapeHtmlAttr($_radio->getTitle()) ?>" + <?= ($_radio->getValue() == $element->getChecked()) ? 'checked="true"' : '' ?> > + <?= $block->escapeHtml($_radio->getLabel()) ?> <?php endforeach; ?> </span> -<?php elseif ($type === 'wysiwyg') : ?> +<?php elseif ($type === 'wysiwyg'): ?> <span class="form_row"> - <label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> - <script> + <label for="<?= /* @noEscape */ $htmlId ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> + <?php $scriptString = <<<script require([ "wysiwygAdapter" ], function(tinyMCE){ @@ -52,35 +92,59 @@ $type = $element->getType(); tinyMCE.init({ mode : "exact", theme : "advanced", - elements : "<?= $block->escapeJs($element->getName()) ?>", - plugins : "inlinepopups,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,insertdatetime,preview,zoom,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras", - theme_advanced_buttons1 : "newdocument,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect,fontselect,fontsizeselect", - theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor", - theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen", - theme_advanced_buttons4 : "insertlayer,moveforward,movebackward,absolute,|,styleprops,|,cite,abbr,acronym,del,ins,|,visualchars,nonbreaking", + elements : "{$block->escapeJs($element->getName())}", + plugins : "inlinepopups,style,layer,table,save,advhr,advimage,advlink,emotions,iespell," + + "insertdatetime,preview,zoom,media,searchreplace,print,contextmenu,paste,directionality," + + "fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras", + theme_advanced_buttons1 : "newdocument,|,bold,italic,underline,strikethrough,|" + + ",justifyleft,justifycenter,justifyright,justifyfull,|" + + ",styleselect,formatselect,fontselect,fontsizeselect", + theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|" + + ",outdent,indent,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|" + + ",insertdate,inserttime,preview,|,forecolor,backcolor", + theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|" + + ",charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen", + theme_advanced_buttons4 : "insertlayer,moveforward,movebackward,absolute,|,styleprops,|" + + ",cite,abbr,acronym,del,ins,|,visualchars,nonbreaking", theme_advanced_toolbar_location : "top", theme_advanced_toolbar_align : "left", theme_advanced_path_location : "bottom", - extended_valid_elements : "a[name|href|target|title|onclick],img[class|src|border=0|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name],hr[class|width|size|noshade],font[face|size|color|style],span[class|align|style]", + extended_valid_elements : "a[name|href|target|title|onclick],img[class|src|border=0|alt|title|hspace" + + "|vspace|width|height|align|onmouseover|onmouseout|name],hr[class|width|size" + + "|noshade],font[face|size|color|style],span[class|align|style]", theme_advanced_resize_horizontal : 'false', theme_advanced_resizing : 'true', apply_source_formatting : 'true', convert_urls : 'false', force_br_newlines : 'true', - doctype : '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' + doctype : '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"' + + ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' }); }); -</script> - <textarea name="<?= $block->escapeHtmlAttr($element->getName()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" id="<?= $element->getHtmlId() ?>" class="textarea <?= $block->escapeHtmlAttr($element->getClass()) ?>" cols="80" rows="20"><?= $block->escapeHtml($element->getValue()) ?></textarea> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <textarea name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + class="textarea <?= $block->escapeHtmlAttr($element->getClass()) ?>" + cols="80" rows="20"> + <?= $block->escapeHtml($element->getValue()) ?> + </textarea> </span> -<?php elseif ($type === 'textarea') : ?> +<?php elseif ($type === 'textarea'): ?> <span class="form_row"> - <label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> - <textarea name="<?= $block->escapeHtmlAttr($element->getName()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" id="<?= $element->getHtmlId() ?>" class="textarea <?= $block->escapeHtmlAttr($element->getClass()) ?>" cols="15" rows="2"><?= $block->escapeHtml($element->getValue()) ?></textarea> + <label for="<?= /* @noEscape */ $htmlId ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> + <textarea name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + class="textarea <?= $block->escapeHtmlAttr($element->getClass()) ?>" + cols="15" + rows="2"> + <?= $block->escapeHtml($element->getValue()) ?> + </textarea> </span> <?php endif; ?> -<?php if ($element->getScript()) : ?> -<script> - <?= /* @noEscape */ $element->getScript() ?> -</script> +<?php if ($element->getScript()): ?> + <?php /* @noEscape */ $secureRenderer->renderTag('script', [], $element->getScript(), false) ?> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml index 5c07b35e72a19..c2abd6069dd5d 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <tr> <td colspan="2"> @@ -25,35 +27,79 @@ <tbody class="gallery"> -<?php $i = 0; if ($block->getValues() !== null) : ?> - <?php foreach ($block->getValues() as $image) : $i++; ?> - <tr id="<?= $block->getElement()->getHtmlId() ?>_tr_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" class="gallery"> - <?php foreach ($block->getValues()->getAttributeBackend()->getImageTypes() as $type) : ?> - <td class="gallery" align="center" style="vertical-align:bottom;"> - <a href="<?= $block->escapeUrl($image->setType($type)->getSourceUrl()) ?>" target="_blank" onclick="imagePreview('<?= $block->getElement()->getHtmlId() ?>_image_<?= $block->escapeHtmlAttr($block->escapeJs($type)) ?>_<?= $block->escapeHtmlAttr($block->escapeJs($image->getValueId())) ?>');return false;"> - <img id="<?= $block->getElement()->getHtmlId() ?>_image_<?= $block->escapeHtmlAttr($type) ?>_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" src="<?= $block->escapeUrl($image->setType($type)->getSourceUrl()) ?>?<?= /* @noEscape */ time() ?>" alt="<?= $block->escapeHtmlAttr($image->getValue()) ?>" title="<?= $block->escapeHtmlAttr($image->getValue()) ?>" height="25" class="small-image-preview v-middle"/></a><br/> - <input type="file" name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) ?>_<?= $block->escapeHtmlAttr($type) ?>[<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" size="1"></td> +<?php $i = 0; if ($block->getValues() !== null): ?> + <?php foreach ($block->getValues() as $image): $i++; ?> + <?php $trId = $block->getElement()->getHtmlId() . '_tr_' . $block->escapeHtmlAttr($image->getValueId()); ?> + <tr id="<?= /* @noEscape */ $trId ?>" class="gallery"> + <?php foreach ($block->getValues()->getAttributeBackend()->getImageTypes() as $type): ?> + <?php $typeId = $block->getElement()->getHtmlId() . '_image_' . $block->escapeHtmlAttr($type); + $imgId = $typeId . '_' . $block->escapeHtmlAttr($image->getValueId()); ?> + <td class="gallery" align="center" id="<?= /* @noEscape */ $typeId ?>"> + <a href="<?= $block->escapeUrl($image->setType($type)->getSourceUrl()) ?>" + id = <?= /* @noEscape */ 'a_' . $imgId ?> + target="_blank" + <img id="<?= /* @noEscape */ $imgId ?>" + src="<?= $block->escapeUrl($image->setType($type)->getSourceUrl()) ?>?<?= /* @noEscape */ time() ?>" + alt="<?= $block->escapeHtmlAttr($image->getValue()) ?>" + title="<?= $block->escapeHtmlAttr($image->getValue()) ?>" + height="25" + class="small-image-preview v-middle"/> + </a><br/> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "imagePreview('<?= $imgId ?>');event.preventDefault()", + '#a_' . $imgId + ) ?> + <input type="file" + name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) + ?>_<?= $block->escapeHtmlAttr($type) ?>[<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" + size="1"> + </td> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('vertical-align:bottom;', 'td#' . $typeId) ?> <?php endforeach; ?> - <td class="gallery" align="center" style="vertical-align:bottom;"><input type="input" name="<?= $block->escapeHtmlAttr($block->getElement()->getParentName()) ?>[position][<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" value="<?= $block->escapeHtmlAttr($image->getPosition()) ?>" id="<?= $block->getElement()->getHtmlId() ?>_position_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" size="3"/></td> - <td class="gallery" align="center" style="vertical-align:bottom;"><?= $block->getDeleteButtonHtml($image->getValueId()) ?><input type="hidden" name="<?= $block->escapeHtmlAttr($block->getElement()->getParentName()) ?>[delete][<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" id="<?= $block->getElement()->getHtmlId() ?>_delete_<?= $block->escapeHtmlAttr($image->getValueId()) ?>"/></td> + <td class="gallery" align="center" id="<?= /* @noEscape */ $trId . '_td_input' ?>"> + <input type="input" + name="<?= $block->escapeHtmlAttr($block->getElement()->getParentName()) + ?>[position][<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" + value="<?= $block->escapeHtmlAttr($image->getPosition()) ?>" + id="<?= $block->getElement()->getHtmlId() + ?>_position_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" + size="3"/> + </td> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('vertical-align:bottom;', $trId . '_td_input') ?> + <td class="gallery" align="center" id="<?= /* @noEscape */ $trId . '_td_delete' ?>"> + <?= $block->getDeleteButtonHtml($image->getValueId()) ?> + <input type="hidden" + name="<?= $block->escapeHtmlAttr($block->getElement()->getParentName()) + ?>[delete][<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" + id="<?= $block->getElement()->getHtmlId() + ?>_delete_<?= $block->escapeHtmlAttr($image->getValueId()) ?>"/> + </td> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('vertical-align:bottom;', $trId . '_td_delete') ?> </tr> <?php endforeach; ?> <?php endif; ?> -<?php if ($i == 0) : ?> - <script> +<?php if ($i == 0): ?> + <?php $scriptString = <<<script document.getElementById("gallery_thead").style.visibility="hidden"; -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <?php endif; ?> </tbody></table> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); + +$scriptString = <<<script require([ 'prototype' ], function () { id = 0; -num_of_images = <?= /* @noEscape */ $i ?>; +num_of_images = {$i}; window.addNewImage = function() { @@ -62,19 +108,24 @@ window.addNewImage = function() id--; num_of_images++; - new_file_input = '<input type="file" name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) ?>_%j%[%id%]" size="1">'; +script; + + $elementName = $block->escapeHtmlAttr($block->getElement()->getName()); + $parentName = $block->escapeJs($block->getElement()->getParentName()); + $deleteButton = /* @noEscape */ $jsonHelper->jsonEncode($block->getDeleteButtonHtml("this")); + $elementNameDel = $block->escapeJs($block->getElement()->getName()); + $scriptString .= <<<script + new_file_input = '<input type="file" name="{$elementName}_%j%[%id%]" size="1">'; // Sort order input var new_row_input = document.createElement( 'input' ); new_row_input.type = 'text'; - new_row_input.name = '<?= $block->escapeJs($block->getElement()->getParentName()) ?>[position]['+id+']'; + new_row_input.name = '{$parentName}[position]['+id+']'; new_row_input.size = '3'; new_row_input.value = '0'; // Delete button - <?php //phpcs:disable ?> - new_row_button = <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getDeleteButtonHtml("this")) ?>; - <?php // phpcs:enable ?> + new_row_button = {$deleteButton}; table = document.getElementById( "gallery" ); @@ -113,13 +164,15 @@ window.deleteImage = function(image) document.getElementById("gallery_thead").style.visibility="hidden"; } if (image>0) { - document.getElementById('<?= $block->escapeJs($block->getElement()->getName()) ?>_delete_'+image).value=image; - document.getElementById('<?= $block->escapeJs($block->getElement()->getName()) ?>_tr_'+image).style.display='none'; + document.getElementById('{$elementNameDel}_delete_'+image).value=image; + document.getElementById('{$elementNameDel}_tr_'+image).style.display='none'; } else { image.parentNode.parentNode.parentNode.removeChild( image.parentNode.parentNode ); } } }); -</script> +script; +?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> </td> </tr> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml index 7f6f2bbd13fa5..6f9344d7e1d77 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + ?> <?php /** @@ -16,63 +17,73 @@ * */ /* @var $block \Magento\Backend\Block\Widget\Grid */ -$numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$numColumns = $block->getColumns() !== null ? count($block->getColumns()): 0; ?> -<?php if ($block->getCollection()) : ?> +<?php if ($block->getCollection()): ?> - <?php if ($block->canDisplayContainer()) : ?> + <?php if ($block->canDisplayContainer()): ?> <div id="<?= $block->escapeHtml($block->getId()) ?>" data-grid-id="<?= $block->escapeHtml($block->getId()) ?>"> - <?php else : ?> + <?php else: ?> <?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> <?php endif; ?> <div class="admin__data-grid-header admin__data-grid-toolbar"> - <?php $massActionAvailable = $block->getChildBlock('grid.massaction') && $block->getChildBlock('grid.massaction')->isAvailable() ?> - <?php if ($block->getPagerVisibility() || $block->getExportTypes() || $block->getChildBlock('grid.columnSet')->getFilterVisibility() || $massActionAvailable) : ?> + <?php $massActionAvailable = $block->getChildBlock('grid.massaction') && + $block->getChildBlock('grid.massaction')->isAvailable() ?> + <?php if ($block->getPagerVisibility() || $block->getExportTypes() || + $block->getChildBlock('grid.columnSet')->getFilterVisibility() || $massActionAvailable): ?> <div class="admin__data-grid-header-row"> - <?php if ($massActionAvailable) : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> + <?php if ($massActionAvailable): ?> + <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . + $block->getMainButtonsHtml() . '</div>' : '' ?> <?php endif; ?> - <?php if ($block->getChildBlock('grid.export')) : ?> + <?php if ($block->getChildBlock('grid.export')): ?> <?= $block->getChildHtml('grid.export') ?> <?php endif; ?> </div> <?php endif; ?> <div class="<?php if ($massActionAvailable) { echo '_massaction ';} ?>admin__data-grid-header-row"> - <?php if ($massActionAvailable) : ?> + <?php if ($massActionAvailable): ?> <?= $block->getChildHtml('grid.massaction') ?> - <?php else : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> + <?php else: ?> + <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . + $block->getMainButtonsHtml() . '</div>' : '' ?> <?php endif; ?> <?php $countRecords = $block->getCollection()->getSize(); ?> <div class="admin__control-support-text"> - <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" <?= /* @noEscape */ $block->getUiId('total-count') ?>> + <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" + <?= /* @noEscape */ $block->getUiId('total-count') ?>> <?= /* @noEscape */ $countRecords ?> </span> <?= $block->escapeHtml(__('records found')) ?> <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>_massaction-count" - class="mass-select-info _empty"><strong data-role="counter">0</strong> <span><?= $block->escapeHtml(__('selected')) ?></span></span> + class="mass-select-info _empty"><strong data-role="counter">0</strong> + <span><?= $block->escapeHtml(__('selected')) ?></span> + </span> </div> - <?php if ($block->getPagerVisibility()) : ?> + <?php if ($block->getPagerVisibility()): ?> <div class="admin__data-grid-pager-wrap"> <select name="<?= $block->escapeHtmlAttr($block->getVarNameLimit()) ?>" id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" - onchange="<?= /* @noEscape */ $block->getJsObjectName() ?>.loadByElement(this)" <?= /* @noEscape */ $block->getUiId('per-page') ?> + onchange="<?= /* @noEscape */ $block->getJsObjectName() ?>.loadByElement(this)" + <?= /* @noEscape */ $block->getUiId('per-page') ?> class="admin__control-select"> - <option value="20"<?php if ($block->getCollection()->getPageSize() == 20) : ?> + <option value="20"<?php if ($block->getCollection()->getPageSize() == 20): ?> selected="selected"<?php endif; ?>>20 </option> - <option value="30"<?php if ($block->getCollection()->getPageSize() == 30) : ?> + <option value="30"<?php if ($block->getCollection()->getPageSize() == 30): ?> selected="selected"<?php endif; ?>>30 </option> - <option value="50"<?php if ($block->getCollection()->getPageSize() == 50) : ?> + <option value="50"<?php if ($block->getCollection()->getPageSize() == 50): ?> selected="selected"<?php endif; ?>>50 </option> - <option value="100"<?php if ($block->getCollection()->getPageSize() == 100) : ?> + <option value="100"<?php if ($block->getCollection()->getPageSize() == 100): ?> selected="selected"<?php endif; ?>>100 </option> - <option value="200"<?php if ($block->getCollection()->getPageSize() == 200) : ?> + <option value="200"<?php if ($block->getCollection()->getPageSize() == 200): ?> selected="selected"<?php endif; ?>>200 </option> </select> @@ -82,14 +93,21 @@ $numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; <?php $_curPage = $block->getCollection()->getCurPage() ?> <?php $_lastPage = $block->getCollection()->getLastPageNumber() ?> - <?php if ($_curPage > 1) : ?> - <button class="action-previous" - type="button" - onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage - 1) ?>');return false;"> - <span><?= $block->escapeHtml(__('Previous page')) ?></span> + <?php if ($_curPage > 1): ?> + <button class="action-previous" type="button"> + <span><?= $block->escapeHtml(__('Previous page')) ?></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + /* @noEscape */ $block->getJsObjectName() . '.setPage(\'' . + /* @noEscape */ ($_curPage - 1) . '\');event.preventDefault();', + 'div#' . $block->escapeJs($block->getId()) . + ' .admin__data-grid-pager button.action-previous:not(.disabled)' + ) ?> + <?php else: ?> + <button type="button" class="action-previous disabled"> + <span><?= $block->escapeHtml(__('Previous page')) ?></span> </button> - <?php else : ?> - <button type="button" class="action-previous disabled"><span><?= $block->escapeHtml(__('Previous page')) ?></span></button> <?php endif; ?> <input type="text" @@ -97,20 +115,36 @@ $numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; name="<?= $block->escapeHtmlAttr($block->getVarNamePage()) ?>" value="<?= $block->escapeHtmlAttr($_curPage) ?>" class="admin__control-text" - onkeypress="<?= /* @noEscape */ $block->getJsObjectName() ?>.inputPage(event, '<?= /* @noEscape */ $_lastPage ?>')" <?= /* @noEscape */ $block->getUiId('current-page') ?> /> + <?= /* @noEscape */ $block->getUiId('current-page') ?> /> + + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onkeypress', + /* @noEscape */ $block->getJsObjectName() . '.inputPage(event, \'' . + /* @noEscape */ $_lastPage . '\')', + '#' . $block->escapeHtml($block->getHtmlId()) . '_page-current' + ) ?> <label class="admin__control-support-text" for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current"> - <?= /* @noEscape */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> + <?= /* @noEscape */ __('of %1', '<span>' . + $block->getCollection()->getLastPageNumber() . '</span>') ?> </label> - <?php if ($_curPage < $_lastPage) : ?> + <?php if ($_curPage < $_lastPage): ?> <button type="button" title="<?= $block->escapeHtmlAttr(__('Next page')) ?>" - class="action-next" - onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage + 1) ?>');return false;"> + class="action-next"> + <span><?= $block->escapeHtml(__('Next page')) ?></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + /* @noEscape */ $block->getJsObjectName() . '.setPage(\'' . + /* @noEscape */ ($_curPage + 1) . '\');event.preventDefault();', + 'div#' . $block->escapeJs($block->getId()) . + ' .admin__data-grid-pager button.action-next:not(.disabled)' + ) ?> + <?php else: ?> + <button type="button" class="action-next disabled"> <span><?= $block->escapeHtml(__('Next page')) ?></span> </button> - <?php else : ?> - <button type="button" class="action-next disabled"><span><?= $block->escapeHtml(__('Next page')) ?></span></button> <?php endif; ?> </div> </div> @@ -118,79 +152,104 @@ $numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; </div> </div> <div class="admin__data-grid-wrap admin__data-grid-wrap-static"> - <?php if ($block->getGridCssClass()) : ?> - <table class="<?= $block->escapeHtmlAttr($block->getGridCssClass()) ?> data-grid" id="<?= $block->escapeHtml($block->getId()) ?>_table"> + <?php if ($block->getGridCssClass()): ?> + <table class="<?= $block->escapeHtmlAttr($block->getGridCssClass()) ?> data-grid" + id="<?= $block->escapeHtml($block->getId()) ?>_table"> <!-- Rendering column set --> <?= $block->getChildHtml('grid.columnSet') ?> </table> - <?php else : ?> + <?php else: ?> <table class="data-grid" id="<?= $block->escapeHtml($block->getId()) ?>_table"> <!-- Rendering column set --> <?= $block->getChildHtml('grid.columnSet') ?> </table> - <?php if ($block->getChildBlock('grid.bottom.links')) : ?> + <?php if ($block->getChildBlock('grid.bottom.links')): ?> <?= $block->getChildHtml('grid.bottom.links') ?> <?php endif; ?> <?php endif ?> </div> - <?php if ($block->canDisplayContainer()) : ?> + <?php if ($block->canDisplayContainer()): ?> </div> -<script> - var deps = []; - - <?php if ($block->getDependencyJsObject()) : ?> - deps.push('uiRegistry'); - <?php endif; ?> - - <?php if (strpos($block->getRowClickCallback(), 'order.') !== false) : ?> - deps.push('Magento_Sales/order/create/form'); - deps.push('jquery'); - <?php endif; ?> - - deps.push('mage/adminhtml/grid'); - - require(deps, function(<?= ($block->getDependencyJsObject() ? 'registry' : '') ?>){ - <?php //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed ?> - - <?php if ($block->getDependencyJsObject()) : ?> - registry.get('<?= $block->escapeJs($block->getDependencyJsObject()) ?>', function (<?= $block->escapeJs($block->getDependencyJsObject()) ?>) { - <?php endif; ?> - - <?= $block->escapeJs($block->getJsObjectName()) ?> = new varienGrid('<?= $block->escapeHtml($block->getId()) ?>', '<?= $block->escapeJs($block->getGridUrl()) ?>', '<?= $block->escapeJs($block->getVarNamePage()) ?>', '<?= $block->escapeJs($block->getVarNameSort()) ?>', '<?= $block->escapeJs($block->getVarNameDir()) ?>', '<?= $block->escapeJs($block->getVarNameFilter()) ?>'); - <?= $block->escapeJs($block->getJsObjectName()) ?>.useAjax = <?= /* @noEscape */ $block->getUseAjax() ? 'true' : 'false' ?>; - <?php if ($block->getRowClickCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.rowClickCallback = <?= /* @noEscape */ $block->getRowClickCallback() ?>; - <?php endif; ?> - <?php if ($block->getCheckboxCheckCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; - <?php endif; ?> - <?php if ($block->getSortableUpdateCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.sortableUpdateCallback = <?= /* @noEscape */ $block->getSortableUpdateCallback() ?>; - <?php endif; ?> - <?php if ($block->getFilterKeyPressCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.filterKeyPressCallback = <?= /* @noEscape */ $block->getFilterKeyPressCallback() ?>; - <?php endif; ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.bindSortable(); - <?php if ($block->getRowInitCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; - <?= $block->escapeJs($block->getJsObjectName()) ?>.initGridRows(); - <?php endif; ?> - <?php if ($block->getChildBlock('grid.massaction') && $block->getChildBlock('grid.massaction')->isAvailable()) : ?> - <?= /* @noEscape */ $block->getChildBlock('grid.massaction')->getJavaScript() ?> - <?php endif ?> - <?= /* @noEscape */ $block->getAdditionalJavaScript() ?> + <?php + $scriptString = 'var deps = [];' . PHP_EOL; + + if ($block->getDependencyJsObject()) { + $scriptString .= 'deps.push(\'uiRegistry\');' . PHP_EOL; + } + + if (strpos($block->getRowClickCallback(), 'order.') !== false) { + $scriptString .= 'deps.push(\'Magento_Sales/order/create/form\');' . PHP_EOL; + $scriptString .= 'deps.push(\'jquery\');' . PHP_EOL; + } + + $scriptString .= 'deps.push(\'mage/adminhtml/grid\');' . PHP_EOL; + + $scriptString .= ' +require(deps, function('. ($block->getDependencyJsObject() ? 'registry' : '') .'){' . PHP_EOL; + //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed + if ($block->getDependencyJsObject()) { + $scriptString .= 'registry.get(\'' . $block->escapeJs($block->getDependencyJsObject()) . + '\', function ('. $block->escapeJs($block->getDependencyJsObject()) . ') {' . PHP_EOL; + } + + $scriptString .= $block->escapeJs($block->getJsObjectName()) . ' = new varienGrid(\'' . + $block->escapeJs($block->getId()) . '\', \'' . $block->escapeJs($block->getGridUrl()) . '\', \'' . + $block->escapeJs($block->getVarNamePage()) .'\', \'' . + $block->escapeJs($block->getVarNameSort()) . '\', \'' . + $block->escapeJs($block->getVarNameDir()) . '\', \'' . $block->escapeJs($block->getVarNameFilter()) .'\'); +' . PHP_EOL; + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.useAjax = ' . + (/* @noEscape */ $block->escapeJs($block->getUseAjax()) ? 'true' : 'false') . ';' . PHP_EOL; + if ($block->getRowClickCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.rowClickCallback = ' . + /* @noEscape */ $block->getRowClickCallback() . ';' . PHP_EOL; + } + + if ($block->getCheckboxCheckCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.checkboxCheckCallback = ' . + /* @noEscape */ $block->getCheckboxCheckCallback() . ';' . PHP_EOL; + } + + if ($block->getSortableUpdateCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.sortableUpdateCallback = ' . + /* @noEscape */ $block->getSortableUpdateCallback() . ';' . PHP_EOL; + } + + if ($block->getFilterKeyPressCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.filterKeyPressCallback = ' . + /* @noEscape */ $block->getFilterKeyPressCallback() . ';' . PHP_EOL; + } + + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.bindSortable();' . PHP_EOL; + + if ($block->getRowInitCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.initRowCallback = ' . + /* @noEscape */ $block->getRowInitCallback() . ';' . PHP_EOL; + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '..initGridRows();' . PHP_EOL; + } + + if ($block->getChildBlock('grid.massaction') && + $block->getChildBlock('grid.massaction')->isAvailable()) { + $scriptString .= /* @noEscape */ $block->getChildBlock('grid.massaction')->getJavaScript() . PHP_EOL; + } + + $scriptString .= /* @noEscape */ $block->getAdditionalJavaScript() . PHP_EOL; + + if ($block->getDependencyJsObject()) { + $scriptString .= '});' . PHP_EOL; + } + + $scriptString .= '});' . PHP_EOL; + + echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); + ?> - <?php if ($block->getDependencyJsObject()) : ?> - }); - <?php endif; ?> - }); -</script> <?php endif; ?> - <?php if ($block->getChildBlock('grid.js')) : ?> + <?php if ($block->getChildBlock('grid.js')): ?> <?= $block->getChildHtml('grid.js') ?> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml index 527ddc436207f..d4aa14250837f 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml @@ -18,31 +18,39 @@ $numColumns = count($block->getColumns()); /** * @var \Magento\Backend\Block\Widget\Grid\Extended $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->getCollection()) : ?> - <?php if ($block->canDisplayContainer()) : ?> +<?php if ($block->getCollection()): ?> + <?php if ($block->canDisplayContainer()): ?> <div id="<?= $block->escapeHtml($block->getId()) ?>" data-grid-id="<?= $block->escapeHtml($block->getId()) ?>"> - <?php else : ?> + <?php else: ?> <?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> <?php endif; ?> <?php $massActionAvailable = $block->getMassactionBlock() && $block->getMassactionBlock()->isAvailable() ?> - <?php if ($block->getPagerVisibility() || $block->getExportTypes() || $block->getFilterVisibility() || $massActionAvailable) : ?> + <?php if ($block->getPagerVisibility() || $block->getExportTypes() || $block->getFilterVisibility() || + $massActionAvailable): ?> <div class="admin__data-grid-header admin__data-grid-toolbar"> <div class="admin__data-grid-header-row"> - <?php if ($massActionAvailable) : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> + <?php if ($massActionAvailable): ?> + <?= $block->getMainButtonsHtml() ? + '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> <?php endif; ?> - <?php if ($block->getExportTypes()) : ?> + <?php if ($block->getExportTypes()): ?> <div class="admin__data-grid-export"> <label class="admin__control-support-text" - for="<?= $block->escapeHtml($block->getId()) ?>_export"><?= $block->escapeHtml(__('Export to:')) ?></label> - <select name="<?= $block->escapeHtml($block->getId()) ?>_export" id="<?= $block->escapeHtml($block->getId()) ?>_export" + for="<?= $block->escapeHtml($block->getId()) ?>_export"> + <?= $block->escapeHtml(__('Export to:')) ?> + </label> + <select name="<?= $block->escapeHtml($block->getId()) ?>_export" + id="<?= $block->escapeHtml($block->getId()) ?>_export" class="admin__control-select"> - <?php foreach ($block->getExportTypes() as $_type) : ?> - <option value="<?= $block->escapeHtmlAttr($_type->getUrl()) ?>"><?= $block->escapeHtml($_type->getLabel()) ?></option> + <?php foreach ($block->getExportTypes() as $_type): ?> + <option value="<?= $block->escapeHtmlAttr($_type->getUrl()) ?>"> + <?= $block->escapeHtml($_type->getLabel()) ?> + </option> <?php endforeach; ?> </select> <?= $block->getExportButtonHtml() ?> @@ -51,76 +59,105 @@ $numColumns = count($block->getColumns()); </div> <div class="admin__data-grid-header-row <?= $massActionAvailable ? '_massaction' : '' ?>"> - <?php if ($massActionAvailable) : ?> + <?php if ($massActionAvailable): ?> <?= $block->getMassactionBlockHtml() ?> - <?php else : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> + <?php else: ?> + <?= $block->getMainButtonsHtml() ? + '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> <?php endif; ?> <?php $countRecords = $block->getCollection()->getSize(); ?> <div class="admin__control-support-text"> - <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" <?= /* @noEscape */ $block->getUiId('total-count') ?>> + <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" + <?= /* @noEscape */ $block->getUiId('total-count') ?>> <?= /* @noEscape */ $countRecords ?> </span> <?= $block->escapeHtml(__('records found')) ?> <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>_massaction-count" - class="mass-select-info _empty"><strong data-role="counter">0</strong> <span><?= $block->escapeHtml(__('selected')) ?></span></span> + class="mass-select-info _empty"> + <strong data-role="counter">0</strong> + <span><?= $block->escapeHtml(__('selected')) ?></span> + </span> </div> - <?php if ($block->getPagerVisibility()) : ?> + <?php if ($block->getPagerVisibility()): ?> <div class="admin__data-grid-pager-wrap"> <select name="<?= $block->escapeHtmlAttr($block->getVarNameLimit()) ?>" id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" onchange="<?= /* @noEscape */ $block->getJsObjectName() ?>.loadByElement(this)" class="admin__control-select"> - <option value="20"<?php if ($block->getCollection()->getPageSize() == 20) : ?> + <option value="20"<?php if ($block->getCollection()->getPageSize() == 20): ?> selected="selected"<?php endif; ?>>20 </option> - <option value="30"<?php if ($block->getCollection()->getPageSize() == 30) : ?> + <option value="30"<?php if ($block->getCollection()->getPageSize() == 30): ?> selected="selected"<?php endif; ?>>30 </option> - <option value="50"<?php if ($block->getCollection()->getPageSize() == 50) : ?> + <option value="50"<?php if ($block->getCollection()->getPageSize() == 50): ?> selected="selected"<?php endif; ?>>50 </option> - <option value="100"<?php if ($block->getCollection()->getPageSize() == 100) : ?> + <option value="100"<?php if ($block->getCollection()->getPageSize() == 100): ?> selected="selected"<?php endif; ?>>100 </option> - <option value="200"<?php if ($block->getCollection()->getPageSize() == 200) : ?> + <option value="200"<?php if ($block->getCollection()->getPageSize() == 200): ?> selected="selected"<?php endif; ?>>200 </option> </select> - <label for="<?= $block->escapeHtml($block->getHtmlId()) ?><?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" + <label for="<?= $block->escapeHtml($block->getHtmlId()) + ?><?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" class="admin__control-support-text"><?= $block->escapeHtml(__('per page')) ?></label> <div class="admin__data-grid-pager"> <?php $_curPage = $block->getCollection()->getCurPage() ?> <?php $_lastPage = $block->getCollection()->getLastPageNumber() ?> - <?php if ($_curPage > 1) : ?> - <button class="action-previous" - type="button" - onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage - 1) ?>');return false;"> + <?php if ($_curPage > 1): ?> + <button class="action-previous" type="button"> + <span><?= $block->escapeHtml(__('Previous page')) ?></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + /* @noEscape */ $block->getJsObjectName() . '.setPage(\'' . + /* @noEscape */ ($_curPage - 1) . '\');event.preventDefault();', + 'div#' . $block->escapeJs($block->getId()) . + ' .admin__data-grid-pager button.action-previous:not(.disabled)' + ) ?> + <?php else: ?> + <button type="button" class="action-previous disabled"> <span><?= $block->escapeHtml(__('Previous page')) ?></span> </button> - <?php else : ?> - <button type="button" class="action-previous disabled"><span><?= $block->escapeHtml(__('Previous page')) ?></span></button> <?php endif; ?> <input type="text" id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current" name="<?= $block->escapeHtmlAttr($block->getVarNamePage()) ?>" value="<?= $block->escapeHtmlAttr($_curPage) ?>" class="admin__control-text" - onkeypress="<?= /* @noEscape */ $block->getJsObjectName() ?>.inputPage(event, '<?= /* @noEscape */ $_lastPage ?>')" <?= /* @noEscape */ $block->getUiId('current-page') ?> /> - <label class="admin__control-support-text" for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current"> - <?= /* @noEscape */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> + <?= /* @noEscape */ $block->getUiId('current-page') ?> /> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onkeypress', + /* @noEscape */ $block->getJsObjectName() . '.inputPage(event, \'' . + /* @noEscape */ $_lastPage . '\')', + '#' . $block->escapeHtml($block->getHtmlId()) . '_page-current' + ) ?> + <label class="admin__control-support-text" + for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current"> + <?= /* @noEscape */ __('of %1', '<span>' . + $block->getCollection()->getLastPageNumber() . '</span>') ?> </label> - <?php if ($_curPage < $_lastPage) : ?> + <?php if ($_curPage < $_lastPage): ?> <button type="button" title="<?= $block->escapeHtmlAttr(__('Next page')) ?>" - class="action-next" - onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage + 1) ?>');return false;"> + class="action-next"> + <span><?= $block->escapeHtml(__('Next page')) ?></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + /* @noEscape */ $block->getJsObjectName() . '.setPage(\'' . + /* @noEscape */ ($_curPage + 1) . '\');event.preventDefault();', + 'div#' . $block->escapeJs($block->getId()) . + ' .admin__data-grid-pager button.action-next:not(.disabled)' + ) ?> + <?php else: ?> + <button type="button" class="action-next disabled"> <span><?= $block->escapeHtml(__('Next page')) ?></span> </button> - <?php else : ?> - <button type="button" class="action-next disabled"><span><?= $block->escapeHtml(__('Next page')) ?></span></button> <?php endif; ?> </div> </div> @@ -137,25 +174,26 @@ $numColumns = count($block->getColumns()); <col <?= $_column->getHtmlProperty() ?> /> <?php endforeach; */ ?> - <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()) : ?> + <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()): ?> <thead> - <?php if ($block->getHeadersVisibility()) : ?> + <?php if ($block->getHeadersVisibility()): ?> <tr> - <?php foreach ($block->getColumns() as $_column) : ?> - <?php if ($_column->getHeaderHtml() == ' ') : ?> + <?php foreach ($block->getColumns() as $_column): ?> + <?php if ($_column->getHeaderHtml() == ' '): ?> <th class="data-grid-th" data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" <?= $_column->getHeaderHtmlProperty() ?>> </th> - <?php else : ?> + <?php else: ?> <?= $_column->getHeaderHtml() ?> <?php endif; ?> <?php endforeach; ?> </tr> <?php endif; ?> - <?php if ($block->getFilterVisibility()) : ?> + <?php if ($block->getFilterVisibility()): ?> <tr class="data-grid-filters" data-role="filter-form"> <?php $i = 0; - foreach ($block->getColumns() as $_column) : ?> - <td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" <?= $_column->getHeaderHtmlProperty() ?>> + foreach ($block->getColumns() as $_column): ?> + <td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" + <?= $_column->getHeaderHtmlProperty() ?>> <?= $_column->getFilterHtml() ?> </td> <?php endforeach; ?> @@ -163,12 +201,14 @@ $numColumns = count($block->getColumns()); <?php endif ?> </thead> <?php endif; ?> - <?php if ($block->getCountTotals()) : ?> + <?php if ($block->getCountTotals()): ?> <tfoot> <tr class="totals"> - <?php foreach ($block->getColumns() as $_column) : ?> + <?php foreach ($block->getColumns() as $_column): ?> <th class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?>"> - <?= /* @noEscape */ ($_column->hasTotalsLabel()) ? $block->escapeHtml($_column->getTotalsLabel()) : $_column->getRowField($_column->getGrid()->getTotals()) ?> + <?= /* @noEscape */ ($_column->hasTotalsLabel()) ? + $block->escapeHtml($_column->getTotalsLabel()) : + $_column->getRowField($_column->getGrid()->getTotals()) ?> </th> <?php endforeach; ?> </tr> @@ -176,21 +216,26 @@ $numColumns = count($block->getColumns()); <?php endif; ?> <tbody> - <?php if (($block->getCollection()->getSize() > 0) && (!$block->getIsCollapsed())) : ?> - <?php foreach ($block->getCollection() as $_index => $_item) : ?> - <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>"<?php if ($_class = $block->getRowClass($_item)) : ?> - class="<?= $block->escapeHtmlAttr($_class) ?>"<?php endif; ?> ><?php + <?php if (($block->getCollection()->getSize() > 0) && (!$block->getIsCollapsed())): ?> + <?php foreach ($block->getCollection() as $_index => $_item): ?> + <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>" + <?php if ($_class = $block->getRowClass($_item)): ?> + class="<?= $block->escapeHtmlAttr($_class) ?>" + <?php endif; ?>> + <?php $i = 0; - foreach ($block->getColumns() as $_column) : - if ($block->shouldRenderCell($_item, $_column)) : + foreach ($block->getColumns() as $_column): + if ($block->shouldRenderCell($_item, $_column)): $_rowspan = $block->getRowspan($_item, $_column); ?> <td <?= /* @noEscape */ ($_rowspan ? 'rowspan="' . $_rowspan . '" ' : '') ?> class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> - <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> - <?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?> + <?= /* @noEscape */ $_column->getId() == 'massaction' ? + 'data-grid-checkbox-cell': '' ?>"> + <?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? + $_html : ' ') ?> </td><?php - if ($block->shouldRenderEmptyCell($_item, $_column)) : + if ($block->shouldRenderEmptyCell($_item, $_column)): ?> <td colspan="<?= $block->escapeHtmlAttr($block->getEmptyCellColspan($_item)) ?>" class="last"><?= $block->escapeHtml($block->getEmptyCellLabel()) ?></td><?php @@ -198,98 +243,164 @@ $numColumns = count($block->getColumns()); endif; endforeach; ?> </tr> - <?php if ($_multipleRows = $block->getMultipleRows($_item)) : ?> - <?php foreach ($_multipleRows as $_i) : ?> + <?php if ($_multipleRows = $block->getMultipleRows($_item)): ?> + <?php foreach ($_multipleRows as $_i): ?> <tr> <?php $i = 0; - foreach ($block->getMultipleRowColumns($_i) as $_column) : ?> + foreach ($block->getMultipleRowColumns($_i) as $_column): ?> <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> - <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> - <?= /* @noEscape */ (($_html = $_column->getRowField($_i)) != '' ? $_html : ' ') ?> + <?= /* @noEscape */ $_column->getId() == 'massaction' ? + 'data-grid-checkbox-cell': '' ?>"> + <?= /* @noEscape */ (($_html = $_column->getRowField($_i)) != '' ? + $_html : ' ') ?> </td> <?php endforeach; ?> </tr> <?php endforeach; ?> <?php endif; ?> - <?php if ($block->shouldRenderSubTotal($_item)) : ?> + <?php if ($block->shouldRenderSubTotal($_item)): ?> <tr class="subtotals"> <?php $i = 0; - foreach ($block->getSubTotalColumns() as $_column) : ?> + foreach ($block->getSubTotalColumns() as $_column): ?> <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> - <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> - <?= /* @noEscape */ $_column->hasSubtotalsLabel() ? $block->escapeHtml($_column->getSubtotalsLabel()) : $_column->getRowField($block->getSubTotalItem($_item)) ?> + <?= /* @noEscape */ $_column->getId() == 'massaction' ? + 'data-grid-checkbox-cell': '' ?>"> + <?= /* @noEscape */ $_column->hasSubtotalsLabel() ? + $block->escapeHtml($_column->getSubtotalsLabel()) : + $_column->getRowField($block->getSubTotalItem($_item)) ?> </td> <?php endforeach; ?> </tr> <?php endif; ?> <?php endforeach; ?> - <?php elseif ($block->getEmptyText()) : ?> + <?php elseif ($block->getEmptyText()): ?> <tr class="data-grid-tr-no-data"> <td class="<?= $block->escapeHtmlAttr($block->getEmptyTextClass()) ?>" - colspan="<?= $block->escapeHtmlAttr($numColumns) ?>"><?= $block->escapeHtml($block->getEmptyText()) ?></td> + colspan="<?= $block->escapeHtmlAttr($numColumns) ?>"><?= + $block->escapeHtml($block->getEmptyText()) ?></td> </tr> <?php endif; ?> </tbody> </table> </div> - <?php if ($block->canDisplayContainer()) : ?> </div> -<script> + <?php + /** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ + $jsonHelper = $block->getData('jsonHelper'); + if ($block->canDisplayContainer()): + $scriptString = <<<script + var deps = []; +script; + if ($block->getDependencyJsObject()): + $scriptString .= <<<script - <?php if ($block->getDependencyJsObject()) : ?> deps.push('uiRegistry'); - <?php endif; ?> +script; + endif; + + if (strpos($block->getRowClickCallback(), 'order.') !== false): + $scriptString .= <<<script - <?php if (strpos($block->getRowClickCallback(), 'order.') !== false) : ?> deps.push('Magento_Sales/order/create/form') - <?php endif; ?> +script; + endif; + $scriptString .= <<<script deps.push('mage/adminhtml/grid'); +script; + if (is_array($block->getRequireJsDependencies())): + foreach ($block->getRequireJsDependencies() as $dependency): + $scriptString .= <<<script - <?php if (is_array($block->getRequireJsDependencies())) : ?> - <?php foreach ($block->getRequireJsDependencies() as $dependency) : ?> - deps.push('<?= $block->escapeJs($dependency) ?>'); - <?php endforeach; ?> - <?php endif; ?> + deps.push('{$block->escapeJs($dependency)}'); +script; + endforeach; + endif; + $dependencyJsObject = ($block->getDependencyJsObject() ? 'registry' : ''); + $scriptString .= <<<script - require(deps, function(<?= ($block->getDependencyJsObject() ? 'registry' : '') ?>){ - <?php //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed ?> + require(deps, function({$dependencyJsObject}){ + +script; + //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed + $scriptString .= <<<script //<![CDATA[ - <?php if ($block->getDependencyJsObject()) : ?> - registry.get('<?= $block->escapeJs($block->getDependencyJsObject()) ?>', function (<?= $block->escapeJs($block->getDependencyJsObject()) ?>) { - <?php endif; ?> - <?php // phpcs:disable ?> - <?= $block->escapeJs($block->getJsObjectName()) ?> = new varienGrid(<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getId()) ?>, '<?= $block->escapeJs($block->getGridUrl()) ?>', '<?= $block->escapeJs($block->getVarNamePage()) ?>', '<?= $block->escapeJs($block->getVarNameSort()) ?>', '<?= $block->escapeJs($block->getVarNameDir()) ?>', '<?= $block->escapeJs($block->getVarNameFilter()) ?>'); - <?php //phpcs:enable ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.useAjax = '<?= $block->escapeJs($block->getUseAjax()) ?>'; - <?php if ($block->getRowClickCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.rowClickCallback = <?= /* @noEscape */ $block->getRowClickCallback() ?>; - <?php endif; ?> - <?php if ($block->getCheckboxCheckCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; - <?php endif; ?> - <?php if ($block->getFilterKeyPressCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.filterKeyPressCallback = <?= /* @noEscape */ $block->getFilterKeyPressCallback() ?>; - <?php endif; ?> - <?php if ($block->getRowInitCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; - <?= $block->escapeJs($block->getJsObjectName()) ?>.initGridRows(); - <?php endif; ?> - <?php if ($block->getMassactionBlock() && $block->getMassactionBlock()->isAvailable()) : ?> - <?= /* @noEscape */ $block->getMassactionBlock()->getJavaScript() ?> - <?php endif ?> - <?= /* @noEscape */ $block->getAdditionalJavaScript() ?> - - <?php if ($block->getDependencyJsObject()) : ?> + +script; + if ($block->getDependencyJsObject()): + $scriptString .= <<<script + + registry.get('{$block->escapeJs($block->getDependencyJsObject())}', + function ({$block->escapeJs($block->getDependencyJsObject())}) { +script; + endif; + $encodedId = /* @noEscape */ $jsonHelper->jsonEncode($block->getId()); + $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())} = new varienGrid({$encodedId}, + '{$block->escapeJs($block->getGridUrl())}', + '{$block->escapeJs($block->getVarNamePage())}', + '{$block->escapeJs($block->getVarNameSort())}', + '{$block->escapeJs($block->getVarNameDir())}', + '{$block->escapeJs($block->getVarNameFilter())}' + ); + + {$block->escapeJs($block->getJsObjectName())}.useAjax = '{$block->escapeJs($block->getUseAjax())}'; + +script; + if ($block->getRowClickCallback()): + $rowClickCallback = /* @noEscape */ $block->getRowClickCallback(); + $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())}.rowClickCallback = {$rowClickCallback}; +script; + endif; + if ($block->getCheckboxCheckCallback()): + $checkboxCheckCallback = /* @noEscape */ $block->getCheckboxCheckCallback(); + $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())}.checkboxCheckCallback = {$checkboxCheckCallback}; +script; + endif; + if ($block->getFilterKeyPressCallback()): + $filterKeyPressCallback = /* @noEscape */ $block->getFilterKeyPressCallback(); + $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())}.filterKeyPressCallback = {$filterKeyPressCallback}; +script; + endif; + if ($block->getRowInitCallback()): + $rowInitCallback = /* @noEscape */ $block->getRowInitCallback(); + $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())}.initRowCallback = {$rowInitCallback}; + {$block->escapeJs($block->getJsObjectName())}.initGridRows(); + +script; + endif; + if ($block->getMassactionBlock() && $block->getMassactionBlock()->isAvailable()): + $scriptString .= /* @noEscape */ $block->getMassactionBlock()->getJavaScript() . PHP_EOL; + endif; + $scriptString .= /* @noEscape */ $block->getAdditionalJavaScript() . PHP_EOL; + + if ($block->getDependencyJsObject()): + $scriptString .= <<<script + }); - <?php endif; ?> + +script; + endif; + $scriptString .= <<<script + //]]> }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> <?php endif ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml index 9a21cd4ef71a1..179557c2984e5 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml @@ -3,11 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= /* @noEscape */ $block->getSomething() ?> <div id="<?= $block->getHtmlId() ?>" class="admin__grid-massaction"> - <?php if ($block->getHideFormElement() !== true) : ?> + <?php if ($block->getHideFormElement() !== true): ?> <form action="" id="<?= $block->getHtmlId() ?>-form" method="post"> <?php endif ?> <div class="admin__grid-massaction-form"> @@ -16,22 +18,26 @@ id="<?= $block->getHtmlId() ?>-select" class="required-entry local-validation admin__control-select" <?= /* @noEscape */ $block->getUiId('select') ?>> - <option class="admin__control-select-placeholder" value="" selected><?= $block->escapeHtml(__('Actions')) ?></option> - <?php foreach ($block->getItems() as $_item) : ?> - <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= $block->escapeHtml($_item->getLabel()) ?></option> + <option class="admin__control-select-placeholder" value="" selected> + <?= $block->escapeHtml(__('Actions')) ?></option> + <?php foreach ($block->getItems() as $_item): ?> + <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" + <?= ($_item->getSelected() ? ' selected="selected"' : '') ?>> + <?= $block->escapeHtml($_item->getLabel()) ?> + </option> <?php endforeach; ?> </select> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-hiddens"></span> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-additional"></span> <?= $block->getApplyButtonHtml() ?> </div> - <?php if ($block->getHideFormElement() !== true) :?> + <?php if ($block->getHideFormElement() !== true):?> </form> <?php endif ?> <div class="no-display"> - <?php foreach ($block->getItems() as $_item) : ?> + <?php foreach ($block->getItems() as $_item): ?> <div id="<?= $block->getHtmlId() ?>-item-<?= /* @noEscape */ $_item->getId() ?>-block"> - <?php if ('' != $_item->getBlockName()) :?> + <?php if ('' != $_item->getBlockName()):?> <?= $block->getChildHtml($_item->getBlockName()) ?> <?php endif;?> </div> @@ -46,7 +52,7 @@ data-menu="grid-mass-select"> <optgroup label="<?= $block->escapeHtmlAttr(__('Mass Actions')) ?>"> <option disabled selected></option> - <?php if ($block->getUseSelectAll()) :?> + <?php if ($block->getUseSelectAll()):?> <option value="selectAll"> <?= $block->escapeHtml(__('Select All')) ?> </option> @@ -65,35 +71,43 @@ <label for="<?= $block->getHtmlId() ?>-mass-select"></label> </div> -<script> +<?php $scriptString = <<<script require(['jquery', 'domReady!'], function($){ 'use strict'; - $('#<?= $block->getHtmlId() ?>-mass-select') +script; +$scriptString .= '$(\'#' . $block->getHtmlId() . '-mass-select\')'; +$scriptString .= <<<script .removeClass('_disabled') .prop('disabled', false) .change(function () { var massAction = $('option:selected', this).val(); this.blur(); switch (massAction) { - <?php if ($block->getUseSelectAll()) : ?> - case 'selectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectAll(); - break; - case 'unselectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectAll(); +script; +if ($block->getUseSelectAll()): + $scriptString .= ' + case \'selectAll\': + return ' . $block->escapeJs($block->getJsObjectName()) . '.selectAll(); break; - <?php endif; ?> - case 'selectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectVisible(); + case \'unselectAll\': + return ' . $block->escapeJs($block->getJsObjectName()) . '.unselectAll(); + break;'; +endif; + $scriptString .= ' + case \'selectVisible\': + return ' . $block->escapeJs($block->getJsObjectName()) . '.selectVisible(); break; - case 'unselectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectVisible(); + case \'unselectVisible\': + return ' . $block->escapeJs($block->getJsObjectName()) . '.unselectVisible(); break; } }); - }); - <?php if (!$block->getParentBlock()->canDisplayContainer()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.setGridIds('<?= $block->escapeJs($block->getGridIdsJson()) ?>'); - <?php endif; ?> -</script> + });'; + +if (!$block->getParentBlock()->canDisplayContainer()): + $scriptString .= $block->escapeJs($block->getJsObjectName()) . + '.setGridIds(\'' . $block->escapeJs($block->getGridIdsJson()) .'\');'; +endif; +?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> </div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml index c0f30fc282f38..495cb572fe125 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="<?= $block->getHtmlId() ?>" class="admin__grid-massaction"> - <?php if ($block->getHideFormElement() !== true) : ?> + <?php if ($block->getHideFormElement() !== true): ?> <form action="" id="<?= $block->getHtmlId() ?>-form" method="post"> <?php endif ?> <div class="admin__grid-massaction-form"> @@ -14,20 +16,25 @@ <select id="<?= $block->getHtmlId() ?>-select" class="required-entry local-validation admin__control-select"> - <option class="admin__control-select-placeholder" value="" selected><?= $block->escapeHtml(__('Actions')) ?></option> - <?php foreach ($block->getItems() as $_item) : ?> - <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= $block->escapeHtml($_item->getLabel()) ?></option> + <option class="admin__control-select-placeholder" value="" selected> + <?= $block->escapeHtml(__('Actions')) ?> + </option> + <?php foreach ($block->getItems() as $_item): ?> + <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" + <?= ($_item->getSelected() ? ' selected="selected"' : '') ?>> + <?= $block->escapeHtml($_item->getLabel()) ?> + </option> <?php endforeach; ?> </select> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-hiddens"></span> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-additional"></span> <?= $block->getApplyButtonHtml() ?> </div> - <?php if ($block->getHideFormElement() !== true) : ?> + <?php if ($block->getHideFormElement() !== true): ?> </form> <?php endif ?> <div class="no-display"> - <?php foreach ($block->getItems() as $_item) : ?> + <?php foreach ($block->getItems() as $_item): ?> <div id="<?= $block->getHtmlId() ?>-item-<?= /* @noEscape */ $_item->getId() ?>-block"> <?= $_item->getAdditionalActionBlockHtml() ?> </div> @@ -40,7 +47,7 @@ data-menu="grid-mass-select"> <optgroup label="<?= $block->escapeHtml(__('Mass Actions')) ?>"> <option disabled selected></option> - <?php if ($block->getUseSelectAll()) : ?> + <?php if ($block->getUseSelectAll()): ?> <option value="selectAll"> <?= $block->escapeHtml(__('Select All')) ?> </option> @@ -58,33 +65,41 @@ </select> <label for="<?= $block->getHtmlId() ?>-mass-select"></label> </div> -<script> + <?php $scriptString = <<<script require(['jquery'], function($){ 'use strict'; - $('#<?= $block->getHtmlId() ?>-mass-select').change(function () { + $('#{$block->getHtmlId()}-mass-select').change(function () { var massAction = $('option:selected', this).val(); switch (massAction) { - <?php if ($block->getUseSelectAll()) : ?> +script; + if ($block->getUseSelectAll()): + $scriptString .= <<<script case 'selectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectAll(); + return {$block->escapeJs($block->getJsObjectName())}.selectAll(); break; case 'unselectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectAll(); + return {$block->escapeJs($block->getJsObjectName())}.unselectAll(); break; - <?php endif; ?> +script; +endif; + $scriptString .= <<<script case 'selectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectVisible(); + return {$block->escapeJs($block->getJsObjectName())}.selectVisible(); break; case 'unselectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectVisible(); + return {$block->escapeJs($block->getJsObjectName())}.unselectVisible(); break; } this.blur(); }); }); - - <?php if (!$block->getParentBlock()->canDisplayContainer()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.setGridIds('<?= /* @noEscape */ $block->getGridIdsJson() ?>'); - <?php endif; ?> -</script> +script; + if (!$block->getParentBlock()->canDisplayContainer()): + $gridIdsJson = /* @noEscape */ $block->getGridIdsJson(); + $scriptString .= <<<script + {$block->escapeJs($block->getJsObjectName())}.setGridIds('{$gridIdsJson}'); +script; + endif; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml index 2208a00929592..9ddf3ea5df3c8 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml @@ -7,6 +7,7 @@ <?php /** * @var $block \Magento\Backend\Block\Widget\Grid\Serializer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php @@ -14,8 +15,8 @@ $_id = 'id_' . md5(microtime()); ?> <?php $formId = $block->getFormId()?> -<?php if (!empty($formId)) : ?> -<script> +<?php if (!empty($formId)): ?> + <?php $scriptString = <<<script require([ 'prototype', 'mage/adminhtml/grid' @@ -23,24 +24,33 @@ $_id = 'id_' . md5(microtime()); Event.observe(window, "load", function(){ var serializeInput = document.createElement('input'); serializeInput.type = 'hidden'; - serializeInput.name = '<?= $block->escapeJs($block->getInputElementName()) ?>'; - serializeInput.id = '<?= /* @noEscape */ $_id ?>'; + serializeInput.name = '{$block->escapeJs($block->getInputElementName())}'; + serializeInput.id = '{$_id}'; try { - document.getElementById('<?= $block->escapeJs($formId) ?>').appendChild(serializeInput); - new serializerController('<?= /* @noEscape */ $_id ?>', <?= /* @noEscape */ $block->getDataAsJSON() ?>, <?= /* @noEscape */ $block->getColumnInputNames(true) ?>, <?= $block->escapeJs($block->getGridBlock()->getJsObjectName()) ?>, '<?= $block->escapeJs($block->getReloadParamName()) ?>'); + document.getElementById('{$block->escapeJs($formId)}').appendChild(serializeInput); + new serializerController('{$_id}', {$block->getDataAsJSON()}, {$block->getColumnInputNames(true)}, + {$block->escapeJs($block->getGridBlock()->getJsObjectName())}, + '{$block->escapeJs($block->getReloadParamName())}'); } catch(e) { //Error add serializer } }); }); -</script> -<?php else :?> -<input type="hidden" name="<?= $block->escapeHtmlAttr($block->getInputElementName()) ?>" value="" id="<?= /* @noEscape */ $_id ?>" /> -<script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<?php else:?> +<input type="hidden" name="<?= $block->escapeHtmlAttr($block->getInputElementName()) ?>" value="" + id="<?= /* @noEscape */ $_id ?>" /> + <?php $scriptString = <<<script require([ 'mage/adminhtml/grid' ], function(){ - new serializerController('<?= /* @noEscape */ $_id ?>', <?= /* @noEscape */ $block->getDataAsJSON() ?>, <?= /* @noEscape */ $block->getColumnInputNames(true) ?>, <?= $block->escapeJs($block->getGridBlock()->getJsObjectName()) ?>, '<?= $block->escapeJs($block->getReloadParamName()) ?>'); + new serializerController('{$_id}', {$block->getDataAsJSON()}, {$block->getColumnInputNames(true)}, + {$block->escapeJs($block->getGridBlock()->getJsObjectName())}, + '{$block->escapeJs($block->getReloadParamName())}'); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml index 5246aac088a5b..51183f733434e 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml @@ -5,29 +5,38 @@ */ /** @var $block \Magento\Backend\Block\Widget\Tabs */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if (!empty($tabs)) : ?> +<?php if (!empty($tabs)): ?> <div class="admin__page-nav" data-role="container" id="<?= $block->escapeHtmlAttr($block->getId()) ?>"> - <?php if ($block->getTitle()) : ?> + <?php if ($block->getTitle()): ?> <div class="admin__page-nav-title" data-role="title" <?= /* @noEscape */ $block->getUiId('title') ?>> <strong><?= $block->escapeHtml($block->getTitle()) ?></strong> <span data-role="title-messages" class="admin__page-nav-title-messages"></span> </div> <?php endif ?> - <ul <?= /* @noEscape */ $block->getUiId('tab', $block->getId()) ?> class="<?= /* @noEscape */ $block->getIsHoriz() ? 'tabs-horiz' : 'tabs admin__page-nav-items' ?>"> - <?php foreach ($tabs as $_tab) : ?> + <ul <?= /* @noEscape */ $block->getUiId('tab', $block->getId()) ?> + class="<?= /* @noEscape */ $block->getIsHoriz() ? 'tabs-horiz' : 'tabs admin__page-nav-items' ?>"> + <?php foreach ($tabs as $_tab): ?> <?php - if (!$block->canShowTab($_tab)) : + if (!$block->canShowTab($_tab)): continue; endif; ?> - <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> - <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> - <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> + <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . + (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> + <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? + 'link' : '' ?> + <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : + $block->getTabUrl($_tab) ?> - <li class="admin__page-nav-item" <?php if ($block->getTabIsHidden($_tab)) : ?> style="display:none"<?php endif; ?><?= /* @noEscape */ $block->getUiId('tab', 'item', $_tab->getId()) ?>> - <a href="<?= $block->escapeUrl($_tabHref) ?>" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" name="<?= $block->escapeHtmlAttr($block->getTabId($_tab, false)) ?>" title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" + <li class="admin__page-nav-item no-display" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" + <?= /* @noEscape */ $block->getUiId('tab', 'item', $_tab->getId()) ?>> + <a href="<?= $block->escapeUrl($_tabHref) ?>" + id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" + name="<?= $block->escapeHtmlAttr($block->getTabId($_tab, false)) ?>" + title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" class="admin__page-nav-link <?= $block->escapeHtmlAttr($_tabClass) ?>" data-tab-type="<?= $block->escapeHtmlAttr($_tabType) ?>" <?= /* @noEscape */ $block->getUiId('tab', 'link', $_tab->getId()) ?>> @@ -38,13 +47,17 @@ <span class="admin__page-nav-item-message _changed"> <span class="admin__page-nav-item-message-icon"></span> <span class="admin__page-nav-item-message-tooltip"> - <?= $block->escapeHtml(__('Changes have been made to this section that have not been saved.')) ?> + <?= $block->escapeHtml(__( + 'Changes have been made to this section that have not been saved.' + )) ?> </span> </span> <span class="admin__page-nav-item-message _error"> <span class="admin__page-nav-item-message-icon"></span> <span class="admin__page-nav-item-message-tooltip"> - <?= $block->escapeHtml(__('This tab contains invalid data. Please resolve this before saving.')) ?> + <?= $block->escapeHtml(__( + 'This tab contains invalid data. Please resolve this before saving.' + )) ?> </span> </span> <span class="admin__page-nav-item-message-loader"> @@ -55,23 +68,49 @@ </span> </span> </a> - <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content" style="display:none;"<?= /* @noEscape */ $block->getUiId('tab', 'content', $_tab->getId()) ?>><?= /* @noEscape */ $block->getTabContent($_tab) ?></div> + <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content" + <?= /* @noEscape */ $block->getUiId('tab', 'content', $_tab->getId()) ?>> + <?= /* @noEscape */ $block->getTabContent($_tab) ?> + </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#' . $block->escapeJs($block->getTabId($_tab)) . '_content' + ); ?> </li> + <?php $scriptString = <<<script + require(['jquery'], function($){ + 'use strict'; +script; + if ($block->getTabIsHidden($_tab)): + $scriptString .= <<<script + $('li.admin__page-nav-item#{$block->escapeJs($block->getTabId($_tab))}').css('display', 'none'); +script; + endif; + + $scriptString .= <<<script + $('li.admin__page-nav-item#{$block->escapeJs($block->getTabId($_tab))}').removeClass('no-display'); + }) +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endforeach; ?> </ul> </div> - -<script> -require(['jquery',"mage/backend/tabs"], function($){ + <?php $scriptString = <<<script +require(['jquery','mage/backend/tabs'], function($){ $(function() { - $('#<?= /* @noEscape */ $block->getId() ?>').tabs({ - active: '<?= /* @noEscape */ $block->getActiveTabId() ?>', - destination: '#<?= /* @noEscape */ $block->getDestElementId() ?>', - shadowTabs: <?= /* @noEscape */ $block->getAllShadowTabs() ?>, - tabsBlockPrefix: '<?= /* @noEscape */ $block->getId() ?>_', +script; + $scriptString .= '$(\'#' . /* @noEscape */ $block->getId() . '\').tabs({' . PHP_EOL . + 'active: \'' . /* @noEscape */ $block->getActiveTabId() . '\',' . PHP_EOL . + 'destination: \'#' . /* @noEscape */ $block->getDestElementId() . '\',' . PHP_EOL . + 'shadowTabs: ' . /* @noEscape */ $block->getAllShadowTabs() . ',' . PHP_EOL . + 'tabsBlockPrefix: \'' . /* @noEscape */ $block->getId() . '_\',' . PHP_EOL; + $scriptString .= <<<script tabIdArgument: 'active_tab' }); }); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml index 747dc577d2348..9a3c941fdc9ed 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml @@ -3,40 +3,70 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Framework\Escaper; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + +/** + * @var SecureHtmlRenderer $secureRenderer + * @var Escaper $escaper + */ ?> -<!-- <?php if ($block->getTitle()) : ?> - <h3><?= $block->escapeHtml($block->getTitle()) ?></h3> -<?php endif ?> --> -<?php if (!empty($tabs)) : ?> -<div id="<?= $block->escapeHtmlAttr($block->getId()) ?>"> +<?php if (!empty($tabs)): ?> + <?php $blockId = $block->getId() ?> +<div id="<?= $escaper->escapeHtmlAttr($blockId) ?>" class="hidden"> <ul class="tabs-horiz"> - <?php foreach ($tabs as $_tab) : ?> - <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> + <?php foreach ($tabs as $_tab): ?> + <?php $tabId = $block->getTabId($_tab) ?> + <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . + (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> - <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> + <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? + '#' . $tabId . '_content' : + $block->getTabUrl($_tab) ?> <li> - <a href="<?= $block->escapeHtmlAttr($_tabHref) ?>" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" class="<?= $block->escapeHtmlAttr($_tabClass) ?>" data-tab-type="<?= $block->escapeHtmlAttr($_tabType) ?>"> + <a href="<?= $escaper->escapeUrl($_tabHref) ?>" + id="<?= $escaper->escapeHtmlAttr($tabId) ?>" + title="<?= $escaper->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" + class="<?= $escaper->escapeHtmlAttr($_tabClass) ?>" + data-tab-type="<?= $escaper->escapeHtmlAttr($_tabType) ?>"> <span> - <span class="changed" title="<?= $block->escapeHtmlAttr(__('The information in this tab has been changed.')) ?>"></span> - <span class="error" title="<?= $block->escapeHtmlAttr(__('This tab contains invalid data. Please resolve this before saving.')) ?>"></span> - <span class="loader" title="<?= $block->escapeHtmlAttr(__('Loading...')) ?>"></span> - <?= $block->escapeHtml($block->getTabLabel($_tab)) ?> + <span class="changed" + title="<?= $escaper->escapeHtmlAttr(__( + 'The information in this tab has been changed.' + )) ?>"></span> + <span class="error" + title="<?= $escaper->escapeHtmlAttr(__( + 'This tab contains invalid data. Please resolve this before saving.' + )) ?>"></span> + <span class="loader" + title="<?= $escaper->escapeHtmlAttr(__('Loading...')) ?>"></span> + <?= $escaper->escapeHtml($block->getTabLabel($_tab)) ?> </span> </a> - <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content" style="display:none"><?= /* @noEscape */ $block->getTabContent($_tab) ?></div> + <div id="<?= $escaper->escapeHtmlAttr($tabId) ?>_content"> + <?= /* @noEscape */ $block->getTabContent($_tab) ?> + </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + '#' . $escaper->escapeJs($tabId) . '_content' + ); ?> </li> <?php endforeach; ?> </ul> </div> -<script> + <?php $scriptString = <<<script require(["jquery","mage/backend/tabs"], function($){ $(function() { - $('#<?= /* @noEscape */ $block->getId() ?>').tabs({ - active: '<?= /* @noEscape */ $block->getActiveTabId() ?>', - destination: '#<?= /* @noEscape */ $block->getDestElementId() ?>', - shadowTabs: <?= /* @noEscape */ $block->getAllShadowTabs() ?> + $('#{$escaper->escapeJs($blockId)}').tabs({ + active: '{$escaper->escapeJs($block->getActiveTabId())}', + destination: '#{$escaper->escapeJs($block->getDestElementId())}', + shadowTabs: {$block->getAllShadowTabs()} }); + $('#{$escaper->escapeJs($blockId)}').removeClass('hidden'); }); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index.php b/app/code/Magento/Backup/Controller/Adminhtml/Index.php index b62963947d7bf..64052254f5233 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index.php @@ -87,6 +87,7 @@ public function __construct( /** * @inheritDoc + * @since 100.2.6 */ public function dispatch(\Magento\Framework\App\RequestInterface $request) { diff --git a/app/code/Magento/Backup/Helper/Data.php b/app/code/Magento/Backup/Helper/Data.php index c6df6a7366852..a29aa01e64d46 100644 --- a/app/code/Magento/Backup/Helper/Data.php +++ b/app/code/Magento/Backup/Helper/Data.php @@ -293,6 +293,7 @@ public function extractDataFromFilename($filename) * Is backup functionality enabled. * * @return bool + * @since 100.2.6 */ public function isEnabled(): bool { diff --git a/app/code/Magento/Backup/Model/Db.php b/app/code/Magento/Backup/Model/Db.php index 084b35448a823..0d117a7dff818 100644 --- a/app/code/Magento/Backup/Model/Db.php +++ b/app/code/Magento/Backup/Model/Db.php @@ -16,7 +16,7 @@ * * @api * @since 100.0.2 - * @deprecated Backup module is to be removed. + * @deprecated 100.2.6 Backup module is to be removed. */ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface { diff --git a/app/code/Magento/Backup/Model/ResourceModel/Db.php b/app/code/Magento/Backup/Model/ResourceModel/Db.php index 6e7d6f9863f33..c38a7b3005e21 100644 --- a/app/code/Magento/Backup/Model/ResourceModel/Db.php +++ b/app/code/Magento/Backup/Model/ResourceModel/Db.php @@ -120,6 +120,7 @@ public function getTableForeignKeysSql($tableName = null) * @param string|null $tableName * @param bool $addDropIfExists * @return string + * @since 100.2.3 */ public function getTableTriggersSql($tableName = null, $addDropIfExists = true) { diff --git a/app/code/Magento/Backup/Model/ResourceModel/Helper.php b/app/code/Magento/Backup/Model/ResourceModel/Helper.php index dace56fd60688..b5a5faae1fde4 100644 --- a/app/code/Magento/Backup/Model/ResourceModel/Helper.php +++ b/app/code/Magento/Backup/Model/ResourceModel/Helper.php @@ -345,6 +345,7 @@ public function restoreTransactionIsolationLevel() * @param boolean $addDropIfExists * @param boolean $stripDefiner * @return string + * @since 100.2.3 */ public function getTableTriggersSql($tableName, $addDropIfExists = false, $stripDefiner = true) { diff --git a/app/code/Magento/Backup/Test/Mftf/ActionGroup/AdminAssertBackupLinkAbsentInMenuActionGroup.xml b/app/code/Magento/Backup/Test/Mftf/ActionGroup/AdminAssertBackupLinkAbsentInMenuActionGroup.xml new file mode 100644 index 0000000000000..094c6292684f6 --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/ActionGroup/AdminAssertBackupLinkAbsentInMenuActionGroup.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="AdminAssertBackupLinkAbsentInMenuActionGroup"> + <annotations> + <description>Verify 'Backup' link is absent in admin menu.</description> + </annotations> + + <click selector="{{AdminMenuSection.menuItem('magento-backend-system')}}" stepKey="clickSystem"/> + <dontSeeElement selector="{{AdminMenuSection.menuItem('magento-backup-system-tools-backup')}}" stepKey="dontSeeBackup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backup/Test/Mftf/Test/AdminSystemBackupMenuTest.xml b/app/code/Magento/Backup/Test/Mftf/Test/AdminSystemBackupMenuTest.xml new file mode 100644 index 0000000000000..af86163fbfe64 --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/Test/AdminSystemBackupMenuTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemBackupMenuTest"> + <annotations> + <features value="Backup"/> + <stories value="Backup menu not visible if config disabled"/> + <title value="Backup menu not visible if backup config disabled"/> + <description value="Disable backup config and check backup menu isn't visible"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-36292"/> + <group value="backup"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <actionGroup ref="AdminAssertBackupLinkAbsentInMenuActionGroup" stepKey="verifyBackupLinkAbsentInMenu"/> + </test> +</tests> diff --git a/app/code/Magento/Backup/etc/adminhtml/menu.xml b/app/code/Magento/Backup/etc/adminhtml/menu.xml index 27991e57b8485..d6967a6a7932e 100644 --- a/app/code/Magento/Backup/etc/adminhtml/menu.xml +++ b/app/code/Magento/Backup/etc/adminhtml/menu.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> - <add id="Magento_Backup::system_tools_backup" title="Backups" translate="title" module="Magento_Backup" sortOrder="30" parent="Magento_Backend::system_tools" action="backup/index" resource="Magento_Backup::backup"/> + <add id="Magento_Backup::system_tools_backup" title="Backups" translate="title" module="Magento_Backup" sortOrder="30" parent="Magento_Backend::system_tools" action="backup/index" resource="Magento_Backup::backup" dependsOnConfig="system/backup/functionality_enabled"/> </menu> </config> diff --git a/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml b/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml index 81aa49efd11e8..33313e71d8c6a 100644 --- a/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml +++ b/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml @@ -3,14 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <!-- TODO: refactor form styles and js --> <script type="text/x-magento-template" id="rollback-warning-template"> -<p><?= $block->escapeHtml(__('You will lose any data created since the backup was made, including admin users, customers and orders.')) ?></p> +<p><?= $block->escapeHtml(__( + 'You will lose any data created since the backup was made, including admin users, customers and orders.' +)) ?></p> <p><?= $block->escapeHtml(__('Are you sure you want to continue?')) ?></p> </script> <script type="text/x-magento-template" id="backup-options-template"> - <div class="backup-messages" style="display: none;"> + <div class="backup-messages no-display"> <div class="messages"></div> </div> <div class="messages"> @@ -21,33 +25,56 @@ <form action="" method="post" id="backup-form" class="form-inline"> <fieldset class="admin__fieldset form-list question"> <div class="admin__field field _required"> - <label for="backup_name" class="admin__field-label"><span><?= $block->escapeHtml(__('Backup Name')) ?></span></label> + <label for="backup_name" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Backup Name')) ?></span> + </label> <div class="admin__field-control"> <input type="text" name="backup_name" id="backup_name" - class="admin__control-text required-entry validate-alphanum-with-spaces validate-length maximum-length-50" + class="admin__control-text required-entry validate-alphanum-with-spaces validate-length + maximum-length-50" maxlength="50" /> <div class="admin__field-note"> - <?= $block->escapeHtml(__('Please use only letters (a-z or A-Z), numbers (0-9) or spaces in this field.')) ?> + <?= $block->escapeHtml(__( + 'Please use only letters (a-z or A-Z), numbers (0-9) or spaces in this field.' + )) ?> </div> </div> </div> <div class="admin__field field maintenance-checkbox-container"> - <label for="backup_maintenance_mode" class="admin__field-label"><span><?= $block->escapeHtml(__('Maintenance mode')) ?></span></label> + <label for="backup_maintenance_mode" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Maintenance mode')) ?></span> + </label> <div class="admin__field-control"> <div class="admin__field-option"> - <input class="admin__control-checkbox" type="checkbox" name="maintenance_mode" value="1" id="backup_maintenance_mode"/> - <label class="admin__field-label" for="backup_maintenance_mode"><?= $block->escapeHtml(__('Please put your store into maintenance mode during backup.')) ?></label> + <input class="admin__control-checkbox" + type="checkbox" + name="maintenance_mode" + value="1" + id="backup_maintenance_mode"/> + <label class="admin__field-label" + for="backup_maintenance_mode"><?= $block->escapeHtml(__( + 'Please put your store into maintenance mode during backup.' + )) ?></label> </div> </div> </div> - <div class="admin__field field maintenance-checkbox-container" id="exclude-media-checkbox-container" style="display: none;"> - <label for="exclude_media" class="admin__field-label"><span><?= $block->escapeHtml(__('Exclude')) ?></span></label> + <div class="admin__field field maintenance-checkbox-container no-display" + id="exclude-media-checkbox-container"> + <label for="exclude_media" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Exclude')) ?></span> + </label> <div class="admin__field-control"> <div class="admin__field-option"> - <input class="admin__control-checkbox" type="checkbox" name="exclude_media" value="1" id="exclude_media"/> - <label class="admin__field-label" for="exclude_media"><?= $block->escapeHtml(__('Exclude media folder from backup')) ?></label> + <input class="admin__control-checkbox" + type="checkbox" + name="exclude_media" + value="1" + id="exclude_media"/> + <label class="admin__field-label" + for="exclude_media"><?= $block->escapeHtml(__('Exclude media folder from backup')) ?> + </label> </div> </div> </div> @@ -56,7 +83,7 @@ </script> <script type="text/x-magento-template" id="rollback-request-password-template"> - <div class="backup-messages" style="display: none;"> + <div class="backup-messages no-display"> <div class="messages"></div> </div> <div class="messages"> @@ -69,44 +96,63 @@ <form action="" method="post" id="rollback-form" class="form-inline"> <fieldset class="admin__fieldset password-box-container"> <div class="admin__field field _required"> - <label for="password" class="admin__field-label"><span><?= $block->escapeHtml(__('User Password')) ?></span></label> - <div class="admin__field-control"><input type="password" name="password" id="password" class="admin__control-text required-entry" autocomplete="new-password"></div> + <label for="password" class="admin__field-label"> + <span><?= $block->escapeHtml(__('User Password')) ?></span> + </label> + <div class="admin__field-control"> + <input type="password" name="password" id="password" class="admin__control-text required-entry" + autocomplete="new-password"> + </div> </div> <div class="admin__field field maintenance-checkbox-container"> - <label for="rollback_maintenance_mode" class="admin__field-label"><span><?= $block->escapeHtml(__('Maintenance mode')) ?></span></label> + <label for="rollback_maintenance_mode" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Maintenance mode')) ?></span> + </label> <div class="admin__field-control"> <div class="admin__field-option"> - <input class="admin__control-checkbox" type="checkbox" name="maintenance_mode" value="1" id="rollback_maintenance_mode"/> - <label class="admin__field-label" for="rollback_maintenance_mode"><?= $block->escapeHtml(__('Please put your store into maintenance mode during rollback processing.')) ?></label> - </div> + <input class="admin__control-checkbox" type="checkbox" name="maintenance_mode" value="1" + id="rollback_maintenance_mode"/> + <label class="admin__field-label" for="rollback_maintenance_mode"> + <?= $block->escapeHtml(__( + 'Please put your store into maintenance mode during rollback processing.' + )) ?></label> + </div> </div> </div> - <div class="admin__field field maintenance-checkbox-container" id="use-ftp-checkbox-row" style="display: none;"> + <div class="admin__field field maintenance-checkbox-container" id="use-ftp-checkbox-row"> <label for="use_ftp" class="admin__field-label"> <span><?= $block->escapeHtml(__('FTP')) ?></span> </label> <div class="admin__field-control"> <div class="admin__field-option"> - <input class="admin__control-checkbox" type="checkbox" name="use_ftp" value="1" id="use_ftp" onclick="backup.toggleFtpCredentialsForm(event)"/> - <label class="admin__field-label" for="use_ftp"><?= $block->escapeHtml(__('Use FTP Connection')) ?></label> + <input class="admin__control-checkbox" type="checkbox" name="use_ftp" value="1" id="use_ftp"/> + <label class="admin__field-label" for="use_ftp"> + <?= $block->escapeHtml(__('Use FTP Connection')) ?> + </label> </div> </div> </div> </fieldset> - <div class="entry-edit" id="ftp-credentials-container" style="display: none;"> + <div class="entry-edit no-display" id="ftp-credentials-container"> <fieldset class="admin__fieldset"> - <legend class="admin__legend legend"><span><?= $block->escapeHtml(__('FTP credentials')) ?></span></legend><br /> + <legend class="admin__legend legend"> + <span><?= $block->escapeHtml(__('FTP credentials')) ?></span> + </legend><br /> <div class="admin__field field _required"> - <label class="admin__field-label" for="ftp_host"><span><?= $block->escapeHtml(__('FTP Host')) ?></span></label> + <label class="admin__field-label" for="ftp_host"> + <span><?= $block->escapeHtml(__('FTP Host')) ?></span> + </label> <div class="admin__field-control"> <input type="text" class="admin__control-text" name="ftp_host" id="ftp_host"> </div> </div> <div class="admin__field field _required"> - <label class="admin__field-label" for="ftp_user"><span><?= $block->escapeHtml(__('FTP Login')) ?></span></label> + <label class="admin__field-label" for="ftp_user"> + <span><?= $block->escapeHtml(__('FTP Login')) ?></span> + </label> <div class="admin__field-control"> <input type="text" class="admin__control-text" name="ftp_user" id="ftp_user"> </div> @@ -116,7 +162,8 @@ <span><?= $block->escapeHtml(__('FTP Password')) ?></span> </label> <div class="admin__field-control"> - <input type="password" class="admin__control-text" name="ftp_pass" id="ftp_pass" autocomplete="new-password"> + <input type="password" class="admin__control-text" name="ftp_pass" id="ftp_pass" + autocomplete="new-password"> </div> </div> <div class="admin__field field"> @@ -136,17 +183,25 @@ $backupUrl = $block->getUrl('*/*/create'); ?> -<script> +<?php $scriptString = <<<script + require([ - "prototype", - "mage/adminhtml/backup" + 'prototype', + 'mage/adminhtml/backup' ], function(){ //<![CDATA[ backup = new AdminBackup(); - backup.rollbackUrl = '<?= $block->escapeUrl($rollbackUrl) ?>'; - backup.backupUrl = '<?= $block->escapeUrl($backupUrl) ?>'; + backup.rollbackUrl = '{$block->escapeJs($rollbackUrl)}'; + backup.backupUrl = '{$block->escapeJs($backupUrl)}'; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<?=/* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'backup.toggleFtpCredentialsForm(event)', + '#use_ftp' +) ?> diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php index 46db8a9907341..05a2a51c51213 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php @@ -5,6 +5,9 @@ */ namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Bundle option checkbox type renderer * @@ -19,22 +22,71 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op */ protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/checkbox.phtml'; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param \Magento\Catalog\Helper\Data $catalogData + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\Math\Random $mathRandom + * @param \Magento\Checkout\Helper\Cart $cartHelper + * @param \Magento\Tax\Helper\Data $taxData + * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper + * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + \Magento\Catalog\Helper\Data $catalogData, + \Magento\Framework\Registry $registry, + \Magento\Framework\Stdlib\StringUtils $string, + \Magento\Framework\Math\Random $mathRandom, + \Magento\Checkout\Helper\Cart $cartHelper, + \Magento\Tax\Helper\Data $taxData, + \Magento\Framework\Pricing\Helper\Data $pricingHelper, + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct( + $context, + $jsonEncoder, + $catalogData, + $registry, + $string, + $mathRandom, + $cartHelper, + $taxData, + $pricingHelper, + $data + ); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { - return '<script> - document.getElementById(\'' . + $scriptString = 'document.getElementById(\'' . $elementId . '\').advaiceContainer = \'' . $containerId . - '\'; - </script>'; + '\';'; + + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } /** * @inheritdoc + * @since 100.3.1 */ public function getSelectionPrice($selection) { diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php index 629f08dc75106..af3642995a6c4 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php @@ -5,6 +5,9 @@ */ namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Bundle option multi select type renderer * @@ -19,22 +22,71 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio */ protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/multi.phtml'; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param \Magento\Catalog\Helper\Data $catalogData + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\Math\Random $mathRandom + * @param \Magento\Checkout\Helper\Cart $cartHelper + * @param \Magento\Tax\Helper\Data $taxData + * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper + * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + \Magento\Catalog\Helper\Data $catalogData, + \Magento\Framework\Registry $registry, + \Magento\Framework\Stdlib\StringUtils $string, + \Magento\Framework\Math\Random $mathRandom, + \Magento\Checkout\Helper\Cart $cartHelper, + \Magento\Tax\Helper\Data $taxData, + \Magento\Framework\Pricing\Helper\Data $pricingHelper, + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct( + $context, + $jsonEncoder, + $catalogData, + $registry, + $string, + $mathRandom, + $cartHelper, + $taxData, + $pricingHelper, + $data + ); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { - return '<script> - document.getElementById(\'' . + $scriptString = 'document.getElementById(\'' . $elementId . '\').advaiceContainer = \'' . $containerId . - '\'; - </script>'; + '\';'; + + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } /** * @inheritdoc + * @since 100.3.1 */ public function getSelectionPrice($selection) { diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php index 1519b3a67ac97..a9b8f7880cac3 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php @@ -5,6 +5,9 @@ */ namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Bundle option radiobox type renderer * @@ -20,18 +23,64 @@ class Radio extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/radio.phtml'; /** - * @param string $elementId - * @param string $containerId - * @return string + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param \Magento\Catalog\Helper\Data $catalogData + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\Math\Random $mathRandom + * @param \Magento\Checkout\Helper\Cart $cartHelper + * @param \Magento\Tax\Helper\Data $taxData + * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper + * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + \Magento\Catalog\Helper\Data $catalogData, + \Magento\Framework\Registry $registry, + \Magento\Framework\Stdlib\StringUtils $string, + \Magento\Framework\Math\Random $mathRandom, + \Magento\Checkout\Helper\Cart $cartHelper, + \Magento\Tax\Helper\Data $taxData, + \Magento\Framework\Pricing\Helper\Data $pricingHelper, + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct( + $context, + $jsonEncoder, + $catalogData, + $registry, + $string, + $mathRandom, + $cartHelper, + $taxData, + $pricingHelper, + $data + ); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + + /** + * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { - return '<script> - document.getElementById(\'' . + $scriptString = 'document.getElementById(\'' . $elementId . '\').advaiceContainer = \'' . $containerId . - '\'; - </script>'; + '\';'; + + return $this->secureRenderer->renderTag('script', [], $scriptString, false); } } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php index 502dfa32044a3..948d0c4a84c92 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php @@ -5,6 +5,9 @@ */ namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Bundle option dropdown type renderer * @@ -20,18 +23,63 @@ class Select extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Opti protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/select.phtml'; /** - * @param string $elementId - * @param string $containerId - * @return string + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param \Magento\Catalog\Helper\Data $catalogData + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\Math\Random $mathRandom + * @param \Magento\Checkout\Helper\Cart $cartHelper + * @param \Magento\Tax\Helper\Data $taxData + * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper + * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + \Magento\Catalog\Helper\Data $catalogData, + \Magento\Framework\Registry $registry, + \Magento\Framework\Stdlib\StringUtils $string, + \Magento\Framework\Math\Random $mathRandom, + \Magento\Checkout\Helper\Cart $cartHelper, + \Magento\Tax\Helper\Data $taxData, + \Magento\Framework\Pricing\Helper\Data $pricingHelper, + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct( + $context, + $jsonEncoder, + $catalogData, + $registry, + $string, + $mathRandom, + $cartHelper, + $taxData, + $pricingHelper, + $data + ); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + + /** + * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { - return '<script> - document.getElementById(\'' . + $scriptString = 'document.getElementById(\'' . $elementId . '\').advaiceContainer = \'' . $containerId . - '\'; - </script>'; + '\';'; + + return $this->secureRenderer->renderTag('script', [], $scriptString, false); } } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php index 0fe8c38cc4992..cc28fab403fa4 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php @@ -6,12 +6,43 @@ namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Bundle product attributes tab * @SuppressWarnings(PHPMD.DepthOfInheritance) */ class Attributes extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Attributes { + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Data\FormFactory $formFactory + * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Data\FormFactory $formFactory, + array $data = [], + SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct( + $context, + $registry, + $formFactory, + $data + ); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Prepare attributes form of bundle product * @@ -69,9 +100,7 @@ protected function _prepareForm() $tax = $this->getForm()->getElement('tax_class_id'); if ($tax) { - $tax->setAfterElementHtml( - '<script>' . - " + $scriptString = " require(['prototype'], function(){ function changeTaxClassId() { if ($('price_type').value == '" . @@ -96,9 +125,9 @@ function changeTaxClassId() { changeTaxClassId(); } }); - " . - '</script>' - ); + "; + + $tax->setAfterElementHtml($this->secureRenderer->renderTag('script', [], $scriptString, false)); } $weight = $this->getForm()->getElement('weight'); diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php index 491f6c3fb1096..befa5794bfb69 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php @@ -9,6 +9,8 @@ use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Store\Model\Store; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; /** * Block for rendering option of bundle product @@ -59,17 +61,20 @@ class Option extends \Magento\Backend\Block\Widget * @param \Magento\Bundle\Model\Source\Option\Type $optionTypes * @param \Magento\Framework\Registry $registry * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Config\Model\Config\Source\Yesno $yesno, \Magento\Bundle\Model\Source\Option\Type $optionTypes, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->_coreRegistry = $registry; $this->_optionTypes = $optionTypes; $this->_yesno = $yesno; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php index b4134e7e3a97e..29c85fcf455f9 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php @@ -7,6 +7,8 @@ use Magento\Catalog\Model\Product\Type\AbstractType; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Helper\Data as CatalogHelper; /** * Adminhtml sales order item renderer @@ -30,6 +32,7 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRend * @param \Magento\Framework\Registry $registry * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param CatalogHelper|null $catalogHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -37,11 +40,11 @@ public function __construct( \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, \Magento\Framework\Registry $registry, array $data = [], - Json $serializer = null + Json $serializer = null, + ?CatalogHelper $catalogHelper = null ) { - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(Json::class); - + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); + $data['catalogHelper'] = $catalogHelper ?? ObjectManager::getInstance()->get(CatalogHelper::class); parent::__construct($context, $stockRegistry, $stockConfiguration, $registry, $data); } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/View/Items/Renderer.php b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/View/Items/Renderer.php index 9fe8891254a5a..dee924ae3cf5e 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/View/Items/Renderer.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/View/Items/Renderer.php @@ -6,7 +6,9 @@ namespace Magento\Bundle\Block\Adminhtml\Sales\Order\View\Items; use Magento\Catalog\Model\Product\Type\AbstractType; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Catalog\Helper\Data as CatalogHelper; /** * Adminhtml sales order item renderer @@ -32,6 +34,7 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Order\View\Items\Renderer\ * @param \Magento\Checkout\Helper\Data $checkoutHelper * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param CatalogHelper|null $catalogHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -41,10 +44,11 @@ public function __construct( \Magento\GiftMessage\Helper\Message $messageHelper, \Magento\Checkout\Helper\Data $checkoutHelper, array $data = [], - Json $serializer = null + Json $serializer = null, + ?CatalogHelper $catalogHelper = null ) { - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(Json::class); + $this->serializer = $serializer ?? ObjectManager::getInstance()->get(Json::class); + $data['catalogHelper'] = $catalogHelper ?? ObjectManager::getInstance()->get(CatalogHelper::class); parent::__construct( $context, @@ -63,7 +67,7 @@ public function __construct( * @param string $value * @param int $length * @param string $etc - * @param string &$remainder + * @param string $remainder * @param bool $breakWords * @return string */ @@ -76,6 +80,8 @@ public function truncateString($value, $length = 80, $etc = '...', &$remainder = } /** + * Get is shipment separately. + * * @param null|object $item * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -109,6 +115,8 @@ public function isShipmentSeparately($item = null) } /** + * Get is child calculated. + * * @param null|object $item * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -144,6 +152,8 @@ public function isChildCalculated($item = null) } /** + * Return selection attributes. + * * @param mixed $item * @return mixed */ @@ -161,6 +171,8 @@ public function getSelectionAttributes($item) } /** + * Return order options. + * * @return array */ public function getOrderOptions() @@ -182,6 +194,8 @@ public function getOrderOptions() } /** + * Return value html. + * * @param object $item * @return string */ @@ -204,6 +218,8 @@ public function getValueHtml($item) } /** + * Return can show price. + * * @param object $item * @return bool */ 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/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index 2dc519dbf1540..fe120e9a179dd 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -6,6 +6,8 @@ namespace Magento\Bundle\Model\Product; +use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\ResourceModel\Option\Collection; use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections; use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier; use Magento\Catalog\Api\ProductRepositoryInterface; @@ -414,16 +416,13 @@ public function beforeSave($product) if ($product->getCanSaveBundleSelections()) { $product->canAffectOptions(true); $selections = $product->getBundleSelectionsData(); - if ($selections && !empty($selections)) { - $options = $product->getBundleOptionsData(); - if ($options) { - foreach ($options as $option) { - if (empty($option['delete']) || 1 != (int)$option['delete']) { - $product->setTypeHasOptions(true); - if (1 == (int)$option['required']) { - $product->setTypeHasRequiredOptions(true); - break; - } + if (!empty($selections) && $options = $product->getBundleOptionsData()) { + foreach ($options as $option) { + if (empty($option['delete']) || 1 != (int)$option['delete']) { + $product->setTypeHasOptions(true); + if (1 == (int)$option['required']) { + $product->setTypeHasRequiredOptions(true); + break; } } } @@ -464,7 +463,7 @@ public function getOptionsIds($product) public function getOptionsCollection($product) { if (!$product->hasData($this->_keyOptionsCollection)) { - /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */ + /** @var Collection $optionsCollection */ $optionsCollection = $this->_bundleOption->create() ->getResourceCollection(); $optionsCollection->setProductIdFilter($product->getEntityId()); @@ -530,10 +529,10 @@ public function getSelectionsCollection($optionIds, $product) * Example: the catalog inventory validation of decimal qty can change qty to int, * so need to change quote item qty option value too. * - * @param array $options - * @param \Magento\Framework\DataObject $option - * @param mixed $value - * @param \Magento\Catalog\Model\Product $product + * @param array $options + * @param \Magento\Framework\DataObject $option + * @param mixed $value + * @param \Magento\Catalog\Model\Product $product * @return $this */ public function updateQtyOption($options, \Magento\Framework\DataObject $option, $value, $product) @@ -682,6 +681,11 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p $options ); + $this->validateRadioAndSelectOptions( + $optionsCollection, + $options + ); + $selectionIds = array_values($this->arrayUtility->flatten($options)); // If product has not been configured yet then $selections array should be empty if (!empty($selectionIds)) { @@ -1184,9 +1188,11 @@ public function canConfigure($product) * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ + // @codingStandardsIgnoreStart public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $product) { } + // @codingStandardsIgnoreEnd /** * Return array of specific to type product entities @@ -1196,18 +1202,19 @@ public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $product) */ public function getIdentities(\Magento\Catalog\Model\Product $product) { - $identities = parent::getIdentities($product); + $identities = []; + $identities[] = parent::getIdentities($product); /** @var \Magento\Bundle\Model\Option $option */ foreach ($this->getOptions($product) as $option) { if ($option->getSelections()) { /** @var \Magento\Catalog\Model\Product $selection */ foreach ($option->getSelections() as $selection) { - $identities = array_merge($identities, $selection->getIdentities()); + $identities[] = $selection->getIdentities(); } } } - return $identities; + return array_merge([], ...$identities); } /** @@ -1272,6 +1279,53 @@ protected function checkIsAllRequiredOptions($product, $isStrictProcessMode, $op } } + /** + * Validate Options for Radio and Select input types + * + * @param Collection $optionsCollection + * @param int[] $options + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function validateRadioAndSelectOptions($optionsCollection, $options): void + { + $errorTypes = []; + + if (is_array($optionsCollection->getItems())) { + foreach ($optionsCollection->getItems() as $option) { + if ($this->isSelectedOptionValid($option, $options)) { + $errorTypes[] = $option->getType(); + } + } + } + + if (!empty($errorTypes)) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Option type (%types) should have only one element.', + ['types' => implode(", ", $errorTypes)] + ) + ); + } + } + + /** + * Check if selected option is valid + * + * @param Option $option + * @param array $options + * @return bool + */ + private function isSelectedOptionValid($option, $options): bool + { + return ( + ($option->getType() == 'radio' || $option->getType() == 'select') && + isset($options[$option->getOptionId()]) && + is_array($options[$option->getOptionId()]) && + count($options[$option->getOptionId()]) > 1 + ); + } + /** * Check if selection is salable * @@ -1333,16 +1387,18 @@ protected function checkIsResult($_result) */ protected function mergeSelectionsWithOptions($options, $selections) { + $selections = []; + foreach ($options as $option) { $optionSelections = $option->getSelections(); if ($option->getRequired() && is_array($optionSelections) && count($optionSelections) == 1) { - $selections = array_merge($selections, $optionSelections); + $selections[] = $optionSelections; } else { $selections = []; break; } } - return $selections; + return array_merge([], ...$selections); } } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index a2fff5739f2f9..96d68d7e74117 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -377,8 +377,7 @@ private function prepareBundlePriceByType($priceType, array $dimensions, $entity ] ); - $query = $select->insertFromSelect($this->getBundlePriceTable()); - $connection->query($query); + $this->tableMaintainer->insertFromSelect($select, $this->getBundlePriceTable(), []); } /** @@ -418,8 +417,7 @@ private function calculateBundleOptionPrice($priceTable, $dimensions) ] ); - $query = $select->insertFromSelect($this->getBundleOptionTable()); - $connection->query($query); + $this->tableMaintainer->insertFromSelect($select, $this->getBundleOptionTable(), []); $this->getConnection()->delete($priceTable->getTableName()); $this->applyBundlePrice($priceTable); @@ -575,8 +573,7 @@ private function calculateFixedBundleSelectionPrice() 'tier_price' => $tierExpr, ] ); - $query = $select->insertFromSelect($this->getBundleSelectionTable()); - $connection->query($query); + $this->tableMaintainer->insertFromSelect($select, $this->getBundleSelectionTable(), []); $this->applyFixedBundleSelectionPrice(); } @@ -627,8 +624,7 @@ private function calculateDynamicBundleSelectionPrice($dimensions) 'tier_price' => $tierExpr, ] ); - $query = $select->insertFromSelect($this->getBundleSelectionTable()); - $connection->query($query); + $this->tableMaintainer->insertFromSelect($select, $this->getBundleSelectionTable(), []); } /** @@ -697,8 +693,7 @@ private function prepareTierPriceIndex($dimensions, $entityIds) $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue()); } - $query = $select->insertFromSelect($this->getTable('catalog_product_index_tier_price')); - $connection->query($query); + $this->tableMaintainer->insertFromSelect($select, $this->getTable('catalog_product_index_tier_price'), []); } /** @@ -725,8 +720,7 @@ private function applyBundlePrice($priceTable): void ] ); - $query = $select->insertFromSelect($priceTable->getTableName()); - $this->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $priceTable->getTableName(), []); } /** @@ -785,7 +779,7 @@ private function getMainTable($dimensions) if ($this->fullReindexAction) { return $this->tableMaintainer->getMainReplicaTable($dimensions); } - return $this->tableMaintainer->getMainTable($dimensions); + return $this->tableMaintainer->getMainTableByDimensions($dimensions); } /** 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/Pricing/Price/ConfiguredPrice.php b/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php index 11f7e2f3d1f15..4b5ec32bf61aa 100644 --- a/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php +++ b/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php @@ -18,6 +18,7 @@ /** * Configured price model * @api + * @since 100.0.2 */ class ConfiguredPrice extends CatalogPrice\FinalPrice implements ConfiguredPriceInterface { diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminBundleProductSetAdvancedPricingActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminBundleProductSetAdvancedPricingActionGroup.xml new file mode 100644 index 0000000000000..16b07a49fabda --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminBundleProductSetAdvancedPricingActionGroup.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="AdminBundleProductSetAdvancedPricingActionGroup" extends="ProductSetAdvancedPricingActionGroup"> + <annotations> + <description>Sets the provided Advanced Pricing on the Admin Bundle Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="priceView" type="string" defaultValue="Price Range"/> + </arguments> + <remove keyForRemoval="selectProductCustomGroupValue"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.bundleAdvancedPriceView}}" userInput="{{priceView}}" stepKey="selectPriceView" before="clickDoneButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminClickAddProductToOptionActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminClickAddProductToOptionActionGroup.xml new file mode 100644 index 0000000000000..c692dfaae35aa --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminClickAddProductToOptionActionGroup.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="AdminClickAddProductToOptionActionGroup"> + <annotations> + <description>Click AddProductToOption button for bundle product.</description> + </annotations> + + <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> + <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> + <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml new file mode 100644 index 0000000000000..a37bb443224b4 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.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="AssertStorefrontBundleValidationMessageActionGroup"> + <annotations> + <description>Check error message in validation message box</description> + </annotations> + <arguments> + <argument name="message" type="string"/> + </arguments> + + <waitForPageLoad stepKey="waitForPageLoad"/> + <see selector="{{StorefrontBundledSection.validationMessageBox}}" userInput="{{message}}" stepKey="seeErrorHoldMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml new file mode 100644 index 0000000000000..35ac68b602a5e --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.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="AssertStorefrontBundleValidationMessagesCountActionGroup"> + <annotations> + <description>Check if there's a validation message box on page and asserts the validation messages number</description> + </annotations> + + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{StorefrontBundledSection.validationMessageBox}}" stepKey="seeErrorBox"/> + <seeNumberOfElements selector="{{StorefrontBundledSection.validationMessageBox}}" userInput="1" stepKey="seeOneErrorBox"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml new file mode 100644 index 0000000000000..f0afcffca816c --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.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="StorefrontAddToTheCartButtonActionGroup"> + <annotations> + <description>Clicks 'Add to Cart' on a Storefront Bundled Product page.</description> + </annotations> + + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="waitForAddToCartButton"/> + <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickOnAddToCartButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml new file mode 100644 index 0000000000000..b30b599f4a034 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.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="AdminProductFormAdvancedPricingSection"> + <element name="bundleAdvancedPriceView" type="select" selector="div[data-index='advanced-pricing'] select[name='product[price_view]']"/> + </section> +</sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml index 967cf5ac49ed5..6ad83ba1105f4 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml @@ -20,6 +20,7 @@ <element name="bundleOptionXInputType" type="select" selector="[name='bundle_options[bundle_options][{{x}}][type]']" parameterized="true"/> <element name="bundleOptionXRequired" type="checkbox" selector="[name='bundle_options[bundle_options][{{x}}][required]']" parameterized="true"/> <element name="bundleOptionXProductYQuantity" type="input" selector="[name='bundle_options[bundle_options][{{x}}][bundle_selections][{{y}}][selection_qty]']" parameterized="true"/> + <element name="bundleOptionXProductYPrice" type="input" selector="[name='bundle_options[bundle_options][{{x}}][bundle_selections][{{y}}][selection_price_value]']" parameterized="true"/> <element name="addProductsToOption" type="button" selector="[data-index='modal_set']" timeout="30"/> <element name="nthAddProductsToOption" type="button" selector="//tr[{{var}}]//button[@data-index='modal_set']" timeout="30" parameterized="true"/> <element name="bundlePriceType" type="select" selector="bundle_options[bundle_options][0][bundle_selections][0][selection_price_type]"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml index 7a188fd58e1af..739c2839e990d 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml @@ -14,8 +14,8 @@ <element name="bundleOptionSelection" type="checkbox" selector="//div[@class='nested options-list']/div[{{optionNumber}}]/label[@class='label']" parameterized="true"/> <!--Description--> <!--CE exclusively--> - <element name="longDescriptionText" type="text" selector="//*[@id='description']/div/div" timeout="30"/> - <element name="shortDescriptionText" type="text" selector="//div[@class='product attribute overview']" timeout="30"/> + <element name="longDescriptionText" type="text" selector="#description>div>div" timeout="30"/> + <element name="shortDescriptionText" type="text" selector="div.product.attribute.overview" timeout="30"/> <!--NameOfProductOnProductPage--> <element name="bundleProductName" type="text" selector="//*[@id='maincontent']//span[@itemprop='name']"/> <!--PageNotFoundErrorMessage--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml index c47cf6095c777..1dea8958c3552 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml @@ -17,7 +17,7 @@ <element name="updateCart" type="button" selector="#product-updatecart-button" timeout="30"/> <element name="configuredPrice" type="block" selector=".price-configured_price .price"/> <element name="fixedPricing" type="text" selector="//div[@class='price-box price-final_price']//span[@id]//..//span[contains(text(),'{{var1}}')]" parameterized="true"/> - <element name="customizeProduct" type="button" selector="//*[@id='bundle-slide']"/> + <element name="customizeProduct" type="button" selector="#bundle-slide"/> <element name="customizableBundleItemOption" type="text" selector="//div[@class='field choice'][1]//input[@type='checkbox']"/> <element name="customizableBundleItemOption2" type="text" selector="//div[@class='field choice'][2]//input[@type='checkbox']"/> <element name="nthOptionDiv" type="block" selector="#product-options-wrapper div.field.option:nth-of-type({{var}})" parameterized="true"/> @@ -38,5 +38,6 @@ <element name="currencyTrigger" type="select" selector="#switcher-currency-trigger" timeout="30"/> <element name="currency" type="select" selector="//a[text()='{{arg}}']" parameterized="true"/> <element name="multiSelectOption" type="select" selector="//div[@class='field option required']//select"/> + <element name="validationMessageBox" type="block" selector="#validation-message-box"/> </section> </sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index fae1ec331b667..23b541273a861 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -13,6 +13,8 @@ <element name="priceTo" type="text" selector=".product-info-price .price-to"/> <element name="minPrice" type="text" selector="span[data-price-type='minPrice']"/> <element name="maxPrice" type="text" selector="span[data-price-type='minPrice']"/> + <element name="asLowAsFinalPrice" type="text" selector="div.price-box.price-final_price p.minimal-price > span.price-final_price span.price"/> + <element name="fixedFinalPrice" type="text" selector="div.price-box.price-final_price > span.price-final_price span.price"/> <element name="productBundleOptionsCheckbox" type="checkbox" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{childName}}')]/../input" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml index 4a78aeb752ca7..26119c5267d86 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml @@ -49,9 +49,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct0$$"/> </actionGroup> @@ -70,7 +68,7 @@ </actionGroup> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Checking on admin side--> @@ -88,8 +86,7 @@ <!--Add another bundle option with 2 items--> <!--Go to bundle product creation page--> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> - <waitForPageLoad stepKey="WaitForPageToLoad"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage"/> <conditionalClick selector="{{AdminProductFiltersSection.filtersClear}}" dependentSelector="{{AdminProductFiltersSection.filtersClear}}" visible="true" stepKey="ClickOnButtonToRemoveFiltersIfPresent"/> <waitForPageLoad stepKey="WaitForClear"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterBundleProductOptionsDownToName"> @@ -101,9 +98,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('1')}}" stepKey="waitForBundleOptionsToAppear"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('1')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillNewestOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('1')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectNewInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToNewBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToNewOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterNewBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToNewOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterNewBundleProductOptions"> <argument name="product" value="$$simpleProduct2$$"/> </actionGroup> @@ -117,7 +112,7 @@ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '3')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillNewProductDefaultQty2"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAgain"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAgain"/> <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <!--Checking on admin side--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml index 55038b0c68c44..0fa4a8ed93732 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml @@ -25,14 +25,13 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Create a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> - <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageBundle"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> @@ -49,9 +48,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml index 30922839a191d..f73941c375a41 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml @@ -43,7 +43,9 @@ </createData> <!-- Reindex --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Login as admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml index 06a05e7a29cd9..ca8a35ee7a363 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!-- Create a new attribute set --> <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> @@ -49,13 +49,12 @@ <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku}}" stepKey="fillProductSku"/> <!--save the product/published by default--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Testing that price appears correctly in admin catalog--> <!--Set filter to product name--> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> - <waitForPageLoad stepKey="WaitForPageToLoad"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterBundleProductOptionsDownToName"> <argument name="product" value="BundleProduct"/> </actionGroup> @@ -70,13 +69,12 @@ <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet2"/> <!--save the product/published by default--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton2"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown2"/> <!--Testing that price appears correctly in admin catalog--> <!--Set filter to product name--> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage2"/> - <waitForPageLoad stepKey="WaitForPageToLoad2"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage2"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterBundleProductOptionsDownToName2"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml index b9fb1c72e079f..79d85c6ced957 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml @@ -21,7 +21,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Create attribute set--> <actionGroup ref="CreateDefaultAttributeSetActionGroup" stepKey="createDefaultAttributeSet"> @@ -96,8 +96,7 @@ </actionGroup> <!--Filter catalog--> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> - <waitForPageLoad stepKey="WaitForPageToLoad"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterBundleProductOptionsDownToName"> <argument name="product" value="BundleProduct"/> </actionGroup> @@ -143,7 +142,7 @@ <selectOption selector="{{AdminProductFormBundleSection.countryOfManufactureDropDown}}" userInput="France" stepKey="countryOfManufactureDropDown"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Verify form was filled out correctly after edit--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml index 82da228e040dc..5a7c1beeea706 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml @@ -111,7 +111,7 @@ <!-- Save product form --> <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveWithTwoOptions"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveWithTwoOptions"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!-- Delete created bundle product --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml index 51c30ef86242c..83db83949f059 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml @@ -25,7 +25,7 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> @@ -38,9 +38,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -57,25 +55,18 @@ <actionGroup ref="AncillaryPrepBundleProductActionGroup" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> - <!--Go to catalog deletion page--> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogPage"/> - <waitForPageLoad stepKey="Loading"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogPage"/> <!--Apply Name Filter--> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterBundleProductOptionsDownToName"> <argument name="product" value="BundleProduct"/> </actionGroup> - <click selector="{{AdminProductFiltersSection.allCheckbox}}" stepKey="SelectAllOnly1"/> - <waitForPageLoad stepKey="loading2"/> <!--Delete--> - <click selector="{{AdminProductFiltersSection.actions}}" stepKey="ClickOnActionsChangingView"/> - <click selector="{{AdminProductFiltersSection.delete}}" stepKey="ClickDelete"/> - <click selector="//button[@class='action-primary action-accept']" stepKey="ConfirmDelete"/> - <waitForPageLoad stepKey="loading3"/> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="selectAndDeleteProducts"/> <!--Locating delete message--> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="deleteMessage"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml new file mode 100644 index 0000000000000..8b50fffec091f --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteBundleDynamicPriceProductTest"> + <annotations> + <features value="Bundle"/> + <stories value="Delete products"/> + <title value="Delete Bundle Dynamic Product"/> + <description value="Admin should be able to delete a bundle dynamic product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-26056"/> + <group value="mtf_migrated"/> + <group value="bundle"/> + </annotations> + <before> + <!-- Create category and simple product --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + + <!-- Create bundle product --> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createDynamicBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createNewBundleLink"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <!-- TODO: Remove this action when MC-37719 will be fixed --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value="cataloginventory_stock"/> + </actionGroup> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteBundleProductBySku"> + <argument name="sku" value="$createDynamicBundleProduct.sku$"/> + </actionGroup> + <!-- Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($createDynamicBundleProduct.custom_attributes[url_key]$)}}" stepKey="openBundleProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoopsMessage"/> + <!-- Search for the product by sku --> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchBySku"> + <argument name="query" value="$createDynamicBundleProduct.sku$"/> + </actionGroup> + <!-- Should not see bundle product --> + <dontSee userInput="$createDynamicBundleProduct.sku$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="openCategoryPage"/> + <!-- Should not see any products in category --> + <dontSee userInput="$createDynamicBundleProduct.name$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml index 5b603ef2f0a44..7973860e4d5c5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml @@ -7,17 +7,17 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminDeleteBundleDynamicProductTest"> + <test name="AdminDeleteBundleDynamicProductTest" deprecated="Use AdminDeleteBundleDynamicPriceProductTest instead"> <annotations> <features value="Bundle"/> <stories value="Delete products"/> - <title value="Delete Bundle Dynamic Product"/> - <description value="Admin should be able to delete a bundle dynamic product"/> + <title value="Deprecated. Delete Bundle Dynamic Product"/> + <description value="Deprecated. Admin should be able to delete a bundle dynamic product"/> <severity value="CRITICAL"/> <testCaseId value="MC-11016"/> <group value="mtf_migrated"/> <skip> - <issueId value="MC-16393"/> + <issueId value="DEPRECATED">Use AdminDeleteBundleDynamicPriceProductTest instead</issueId> </skip> </annotations> <before> @@ -40,10 +40,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createDynamicBundleProduct.name$$)}}" stepKey="amOnBundleProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createDynamicBundleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createDynamicBundleProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createDynamicBundleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml index 46244603f2868..edde81f338437 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml @@ -37,10 +37,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createFixedBundleProduct.name$$)}}" stepKey="amOnBundleProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createFixedBundleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createFixedBundleProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createFixedBundleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml index 4ba5d0f66e096..ac0c3e7b5b791 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml @@ -31,14 +31,13 @@ <argument name="product" value="BundleProduct"/> </actionGroup> <!--Logging out--> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct0" stepKey="deleteSimpleProduct0"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> </after> <!-- Create a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> - <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageBundle"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> @@ -52,7 +51,7 @@ </actionGroup> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <actionGroup ref="AddRelatedProductBySkuActionGroup" stepKey="addRelatedProduct1"> @@ -63,7 +62,7 @@ <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.removeRelatedProduct($$simpleProduct0.sku$$)}}" stepKey="removeRelatedProduct"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAfterEdit"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAfterEdit"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <!--See related product in admin--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml index f8914656cc32b..7ef529f0e6976 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml @@ -25,7 +25,7 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> @@ -38,9 +38,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -57,7 +55,7 @@ <actionGroup ref="AncillaryPrepBundleProductActionGroup" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Apply Bundle Product Filter--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml similarity index 86% rename from app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml rename to app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml index 2c1fcb6d7de42..3b0f9afd3d4f6 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml @@ -29,7 +29,7 @@ <after> <!--Clear Filters--> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="ClearFiltersAfter"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> @@ -46,9 +46,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -67,7 +65,7 @@ </actionGroup> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Creating Second bundle product--> @@ -81,9 +79,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions2"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle2"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType2"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle2"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption2"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts2"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptionsx2"> <argument name="product" value="$$simpleProduct3$$"/> </actionGroup> @@ -107,7 +103,7 @@ <fillField userInput="{{BundleProduct.urlKey2}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension2"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton2"/> <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown2"/> <!--Mass delete bundle products--> @@ -118,12 +114,7 @@ <actionGroup ref="BundleProductFilter" stepKey="FilterForOnlyBundleProducts"/> <!--Delete--> - <click selector="{{AdminProductFiltersSection.allCheckbox}}" stepKey="SelectAllOnly1"/> - <waitForPageLoad stepKey="loading"/> - <click selector="{{AdminProductFiltersSection.actions}}" stepKey="ClickOnActionsChangingView"/> - <click selector="{{AdminProductFiltersSection.delete}}" stepKey="ClickDelete"/> - <click selector="//button[@class='action-primary action-accept']" stepKey="ConfirmDelete"/> - <waitForPageLoad stepKey="loading3"/> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="selectAndDeleteProducts"/> <!--Locating delete message--> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="deleteMessage"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml index 17235c531de8f..1b3de481b2a08 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml @@ -45,9 +45,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -66,7 +64,7 @@ </actionGroup> <!--save the product/published by default--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!-- go to page--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml index ab1d4bb5ce68a..17c31b8a5ae53 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml @@ -29,14 +29,13 @@ <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteBundleProduct"> <argument name="sku" value="{{BundleProduct.sku}}"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Create a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> - <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageBundle"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> @@ -53,9 +52,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml index b8eef5c1b406f..d9ab2962964b2 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml @@ -47,9 +47,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -73,13 +71,12 @@ <fillField userInput="{{BundleProduct.fixedPrice}}" selector="{{AdminProductFormBundleSection.priceField}}" stepKey="fillPrice"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Testing that price appears correctly in admin catalog--> <!--Set filter to product name--> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> - <waitForPageLoad stepKey="WaitForPageToLoad"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage"/> <conditionalClick selector="{{AdminProductFiltersSection.filtersClear}}" dependentSelector="{{AdminProductFiltersSection.filtersClear}}" visible="true" stepKey="ClickOnButtonToRemoveFiltersIfPresent"/> <waitForPageLoad stepKey="WaitForClear"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterBundleProductOptionsDownToName"> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml new file mode 100644 index 0000000000000..c56e09562d49a --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.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="BundleProductWithDynamicTierPriceInCartTest" extends="BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest"> + <annotations> + <stories value="Add bundle product to cart on storefront"/> + <title value="Customer should get the right subtotal in cart when the bundle product with dynamic tier price added to the cart"/> + <description value="Customer should be able to add bundle product with dynamic tier price to the cart and get the right price"/> + <severity value="CRITICAL"/> + </annotations> + + <before> + <createData entity="VirtualProduct" stepKey="createProductForBundleItem1"> + <field key="price">50.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createProductForBundleItem2"> + <field key="price">100.00</field> + </createData> + </before> + + <remove keyForRemoval="clickDynamicPriceSwitcher"/> + <remove keyForRemoval="fillBundlePrice"/> + <remove keyForRemoval="disableDynamicSku"/> + <remove keyForRemoval="fillBundleOption1Price"/> + <remove keyForRemoval="selectPercentPrice"/> + <remove keyForRemoval="fillBundleOption2Price"/> + <assertEquals message="ExpectedPrice" stepKey="assertBundleProductPrice"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">$75.00</expectedResult> + </assertEquals> + <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> + <argument name="subTotal" value="$75.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml new file mode 100644 index 0000000000000..1b33bb08b1b03 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="BundleProductWithOptionTierPriceInCartTest" extends="BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest"> + <annotations> + <stories value="Add bundle product to cart on storefront"/> + <title value="Customer should get the right subtotal in cart when the bundle product with tier price for sub-item added to the cart"/> + <description value="Customer should be able to add bundle product with tier price for sub-item price to the cart and get the right price"/> + <severity value="CRITICAL"/> + </annotations> + + <before> + <createData entity="VirtualProduct" stepKey="createProductForBundleItem1"> + <field key="price">50.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createProductForBundleItem2"> + <field key="price">100.00</field> + </createData> + <createData entity="TierProductPrice50PercentDiscount" stepKey="addTierPrice"> + <requiredEntity createDataKey="createProductForBundleItem2"/> + </createData> + </before> + + <remove keyForRemoval="clickDynamicPriceSwitcher"/> + <remove keyForRemoval="fillBundlePrice"/> + <remove keyForRemoval="disableDynamicSku"/> + <remove keyForRemoval="fillBundleOption1Price"/> + <remove keyForRemoval="selectPercentPrice"/> + <remove keyForRemoval="fillBundleOption2Price"/> + <remove keyForRemoval="addProductTierPrice"/> + <actionGroup ref="SaveProductFormActionGroup" after="addBundleOption2" stepKey="saveBundleProduct"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.fixedFinalPrice}}" stepKey="grabProductPrice"/> + <assertEquals message="ExpectedPrice" stepKey="assertBundleProductPrice"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">$100.00</expectedResult> + </assertEquals> + <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> + <argument name="subTotal" value="$100.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml index b5812817b5640..def24c86e1730 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml @@ -34,7 +34,7 @@ <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <amOnPage url="{{AdminProductCreatePage.url(BundleProduct.set, BundleProduct.type)}}" stepKey="goToBundleProductCreationPage"/> <waitForPageLoad stepKey="waitForBundleProductCreatePageToLoad"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest.xml new file mode 100644 index 0000000000000..59a6869747444 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest"> + <annotations> + <features value="Bundle"/> + <stories value="Add bundle product to cart on storefront"/> + <title value="Customer should get the right subtotal in cart when the bundle product with tier price and bundle items with fixed and percent price added to the cart"/> + <description value="Customer should be able to add bundle product with tier price and bundle items with fixed and percent price to the cart and get the right price"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-26543"/> + <group value="bundle"/> + <group value="catalog"/> + </annotations> + + <before> + <createData entity="SimpleProduct2" stepKey="createProductForBundleItem1"> + <field key="price">100.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createProductForBundleItem2"> + <field key="price">100.00</field> + </createData> + <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + + <after> + <deleteData createDataKey="createProductForBundleItem1" stepKey="deleteProductForBundleItem1"/> + <deleteData createDataKey="createProductForBundleItem2" stepKey="deleteProductForBundleItem2"/> + <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteBundle"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> + <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <amOnPage url="{{AdminProductCreatePage.url(BundleProduct.set, BundleProduct.type)}}" stepKey="goToBundleProductCreationPage"/> + <waitForPageLoad stepKey="waitForBundleProductCreatePageToLoad"/> + <click selector="{{AdminProductFormBundleSection.dynamicSkuToggle}}" stepKey="disableDynamicSku"/> + <click selector="{{AdminProductFormBundleSection.dynamicPrice}}" stepKey="clickDynamicPriceSwitcher"/> + <fillField selector="{{AdminProductFormBundleSection.priceField}}" userInput="100" stepKey="fillBundlePrice"/> + <actionGroup ref="FillMainBundleProductFormActionGroup" stepKey="fillMainFieldsForBundle"/> + <actionGroup ref="AddBundleOptionWithOneProductActionGroup" stepKey="addBundleOption1"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$createProductForBundleItem1.sku$"/> + <argument name="prodTwoSku" value=""/> + <argument name="optionTitle" value="Option1"/> + <argument name="inputType" value="checkbox"/> + </actionGroup> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYPrice('0', '0')}}" userInput="100" stepKey="fillBundleOption1Price"/> + <selectOption selector="{{AdminProductFormBundleSection.bundlePriceType}}" userInput="Percent" stepKey="selectPercentPrice"/> + <actionGroup ref="AddBundleOptionWithOneProductActionGroup" stepKey="addBundleOption2"> + <argument name="x" value="1"/> + <argument name="n" value="2"/> + <argument name="prodOneSku" value="$createProductForBundleItem2.sku$"/> + <argument name="prodTwoSku" value=""/> + <argument name="optionTitle" value="Option2"/> + <argument name="inputType" value="checkbox"/> + </actionGroup> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYPrice('1', '0')}}" userInput="100" stepKey="fillBundleOption2Price"/> + <scrollToTopOfPage stepKey="scrollToTopOfTheProductPage"/> + <actionGroup ref="AdminBundleProductSetAdvancedPricingActionGroup" stepKey="addProductTierPrice"> + <argument name="quantity" value="1"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="50"/> + <argument name="priceView" value="As Low as"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url(BundleProduct.urlKey)}}" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStorefront"/> + <!--Assert Bundle Product Price--> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.asLowAsFinalPrice}}" stepKey="grabProductPrice"/> + <assertEquals message="ExpectedPrice" stepKey="assertBundleProductPrice"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">$150.00</expectedResult> + </assertEquals> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickOnCustomizeAndAddToCartButton"/> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> + <argument name="subTotal" value="$150.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml index ada91d068efcf..b25139835de59 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml @@ -40,7 +40,7 @@ <click selector="{{CurrencySetupSection.currencyOptions}}" stepKey="closeOptions"/> <waitForPageLoad stepKey="waitForCloseOptions"/> <click stepKey="saveUnselectedConfigs" selector="{{AdminConfigSection.saveButton}}"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml index 90619eeeadae9..45328900bf156 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml @@ -44,9 +44,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -65,7 +63,7 @@ </actionGroup> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Testing enabled view--> @@ -76,15 +74,13 @@ <seeElement stepKey="LookingForNameOfProduct" selector="{{StorefrontBundledSection.bundleProductName}}"/> <!--Testing disabled view--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="GoToProductCatalog"/> - <waitForPageLoad stepKey="WaitForCatalogProductPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="GoToProductCatalog"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="FindProductEditPage"> <argument name="product" value="BundleProduct"/> </actionGroup> <click selector="{{AdminDataGridTableSection.rowViewAction('1')}}" stepKey="ClickProductInGrid"/> <click stepKey="ClickOnEnableDisableToggle" selector="{{AdminProductFormBundleSection.enableDisableToggle}}"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAgain"/> - <waitForPageLoad stepKey="PauseForSave"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAgain"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <amOnPage url="{{BundleProduct.urlKey}}.html" stepKey="GoToProductPageAgain"/> <waitForPageLoad stepKey="WaitForProductPageToLoadToShowElement"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml index fd94ca93b1600..f8f98384ee8da 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml @@ -29,7 +29,7 @@ <after> <!--Clear Filters--> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="ClearFiltersAfter"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> @@ -46,9 +46,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -64,7 +62,7 @@ <actionGroup ref="AncillaryPrepBundleProductActionGroup" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Creating Second bundle product--> @@ -78,9 +76,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions2"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle2"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType2"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle2"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption2"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts2"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptionsx2"> <argument name="product" value="$$simpleProduct3$$"/> </actionGroup> @@ -105,7 +101,7 @@ <fillField userInput="{{BundleProduct.urlKey2}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension2"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton2"/> <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown2"/> <!--Clear Filters--> @@ -130,16 +126,19 @@ <dontSeeElement stepKey="LookingForNameOfProductDisabled" selector="{{StorefrontBundledSection.bundleProductName}}"/> <!--Enabling bundle products--> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="GoToCatalogPageChangingView"/> - <waitForPageLoad stepKey="WaitForPageToLoadFullyChangingView"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToCatalogPageChangingView"/> <click selector="{{AdminProductFiltersSection.allCheckbox}}" stepKey="ClickOnSelectAllCheckBoxChangingView"/> <click selector="{{AdminProductFiltersSection.actions}}" stepKey="ClickOnActionsChangingView"/> <click selector="{{AdminProductFiltersSection.changeStatus}}" stepKey="ClickOnChangeStatusChangingView"/> <click selector="{{AdminProductFiltersSection.enable}}" stepKey="ClickOnEnable"/> <!--Clear Cache - reindex - resets products according to enabled/disabled view--> - <magentoCLI command="indexer:reindex" stepKey="reindex2"/> - <magentoCLI command="cache:flush" stepKey="flushCache2"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Confirm bundle products have been enabled--> <amOnPage url="{{BundleProduct.urlKey2}}.html" stepKey="GoToProductPageEnabled"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml index efef033f9d974..f4b81e9ba9577 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml @@ -22,10 +22,9 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> - <waitForPageLoad stepKey="WaitForPageToLoad"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage"/> <!--Selecting new bundle product--> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml index cc2aeb0602d36..e89ef1d3f87fa 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml @@ -35,8 +35,7 @@ <!-- Create a product to appear in the widget, fill in basic info first --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <click selector="{{AdminProductGridActionSection.addBundleProduct}}" stepKey="clickAddBundleProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> @@ -51,8 +50,7 @@ <waitForPageLoad stepKey="waitForOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="MFTF Test Bundle 1" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="checkbox" stepKey="selectInputType"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -64,7 +62,7 @@ <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="clickAddSelectedBundleProducts"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="1" stepKey="fillProductDefaultQty1"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="1" stepKey="fillProductDefaultQty2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here, via merge --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml index 8e0197697e691..5f9697f666e7b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml @@ -53,8 +53,7 @@ </after> <!-- Start creating a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml index b3c542af7bbc9..93fac3171e9fb 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml @@ -40,7 +40,9 @@ <requiredEntity createDataKey="bundleOption"/> <requiredEntity createDataKey="apiSimple"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml index 8e8df1f4f16f0..bd61f7aaf3b99 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml @@ -25,14 +25,13 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Create a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> - <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageBundle"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> @@ -48,9 +47,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -84,15 +81,13 @@ <grabTextFrom selector="{{CheckoutCartProductSection.nthBundleOptionName('1')}}" stepKey="grabTotalBefore"/> <!-- Find the product that we just created using the product grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> <waitForPageLoad stepKey="waitForProductFilterLoad"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- Change the product option title --> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="BundleOption2" stepKey="fillOptionTitle2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleAddToCartSuccessTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleAddToCartSuccessTest.xml index e6f8834336683..194a3972ed934 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleAddToCartSuccessTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleAddToCartSuccessTest.xml @@ -24,14 +24,13 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Start creating a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml index 966082739aa68..7883cc4faf00b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml @@ -25,14 +25,13 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Start creating a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml new file mode 100644 index 0000000000000..91cc58ee0119b --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontBundleCheckBoxOptionValidationTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle product validation before add to cart"/> + <title value="Customer should be able to see only one validation message for checkbox option group"/> + <description value="Customer should be able to see only one validation message for checkbox option group"/> + <testCaseId value="MC-35133"/> + <severity value="MINOR"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simpleProduct1" before="bundleProduct"/> + <createData entity="ApiProductWithDescription" stepKey="simpleProduct2" after="simpleProduct1"/> + <createData entity="ApiBundleProduct" stepKey="bundleProduct"/> + <createData entity="CheckboxOption" stepKey="checkboxBundleOption"> + <requiredEntity createDataKey="bundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simpleProduct1"/> + <field key="qty">2</field> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simpleProduct2"/> + <field key="qty">4</field> + </createData> + <magentoCron stepKey="runCronIndex" groups="index"/> + </before> + <after> + <deleteData createDataKey="bundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + </after> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductStorefront"> + <argument name="productUrl" value="$$bundleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="customizeBundleProduct"/> + <actionGroup ref="StorefrontAddToTheCartButtonActionGroup" stepKey="addToCartBundleProduct"/> + <actionGroup ref="AssertStorefrontBundleValidationMessagesCountActionGroup" stepKey="assertBundleValidationCount"/> + <actionGroup ref="AssertStorefrontBundleValidationMessageActionGroup" stepKey="assertBundleValidationMessage"> + <argument name="message" value="Please select one of the options."/> + </actionGroup> + </test> +</tests> 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/StorefrontBundleProductDetailsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml index 786040d16b7e2..63362071568b5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml @@ -52,9 +52,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -73,7 +71,7 @@ </actionGroup> <!--save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAgain"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAgain"/> <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <!--Checking details--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml index 871bf71d1f876..04753baec45f6 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml @@ -29,7 +29,7 @@ </before> <after> <!--Logging out--> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> @@ -57,8 +57,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -73,7 +72,7 @@ <actionGroup ref="AncillaryPrepBundleProductActionGroup" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <magentoCron stepKey="runCronReindex" groups="index"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml index de6718dfd9f31..5997cdc14ade8 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml @@ -39,7 +39,9 @@ <requiredEntity createDataKey="fixedBundleOption"/> <requiredEntity createDataKey="createSimpleProductTwo"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createDynamicBundle" stepKey="deleteDynamicBundleProduct"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml index 77c561f311280..97d466964fbd7 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml @@ -30,14 +30,13 @@ <actionGroup stepKey="deleteBundle" ref="DeleteProductUsingProductGridActionGroup"> <argument name="product" value="BundleProduct"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Start creating a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml index 161d308044b4a..e204dd01e6367 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml @@ -25,14 +25,13 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Create a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> - <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageBundle"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> @@ -48,9 +47,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> - <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml index add819e2d3f14..88fc5b7171592 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml @@ -26,7 +26,7 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -47,8 +47,7 @@ <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> - <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> - <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> + <actionGroup ref="AdminClickAddProductToOptionActionGroup" stepKey="clickAddProductsToOption"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> <argument name="product" value="$$simpleProduct1$$"/> </actionGroup> @@ -63,7 +62,7 @@ <actionGroup ref="AncillaryPrepBundleProductActionGroup" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Go to category page--> @@ -72,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/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml index 1c7cb39d7746f..7049299987dff 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml @@ -95,7 +95,9 @@ </createData> <!-- Perform CLI reindex --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete all created data --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml index 32662321a611e..58fae75a6fc6b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml @@ -36,7 +36,9 @@ <requiredEntity createDataKey="simpleProduct"/> </createData> <!-- Run reindex stock status --> - <magentoCLI command="indexer:reindex" arguments="cataloginventory_stock" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="cataloginventory_stock"/> + </actionGroup> </before> <after> <deleteData createDataKey="bundleProduct" stepKey="deleteBundleProduct"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml index ac2ab4806fd44..24c481c9ddcb2 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml @@ -163,11 +163,13 @@ <!-- Save the settings --> <scrollToTopOfPage stepKey="scrollToTop"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveTaxOptions"/> - <waitForPageLoad stepKey="waitForTaxSaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveTaxOptions"/> + <see userInput="You saved the configuration." selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccess"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- navigate to the tax configuration page --> @@ -190,8 +192,7 @@ <!-- Save the settings --> <scrollToTopOfPage stepKey="scrollToTop"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveTaxOptions"/> - <waitForPageLoad stepKey="waitForTaxSaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveTaxOptions"/> <see userInput="You saved the configuration." selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccess"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/CheckboxTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/CheckboxTest.php index bf3860fd322eb..79c9c28df708a 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/CheckboxTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/CheckboxTest.php @@ -8,8 +8,10 @@ namespace Magento\Bundle\Test\Unit\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type; use Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Checkbox; +use Magento\Framework\DataObject; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class CheckboxTest extends TestCase { @@ -20,9 +22,20 @@ class CheckboxTest extends TestCase protected function setUp(): void { + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $this->block = (new ObjectManager($this)) ->getObject( - Checkbox::class + Checkbox::class, + ['htmlRenderer' => $secureRendererMock] ); } diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/MultiTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/MultiTest.php index 5173c034e79e2..6c37827d100f0 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/MultiTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/MultiTest.php @@ -10,6 +10,8 @@ use Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Multi; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Framework\DataObject; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class MultiTest extends TestCase { @@ -20,8 +22,18 @@ class MultiTest extends TestCase protected function setUp(): void { + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $this->block = (new ObjectManager($this)) - ->getObject(Multi::class); + ->getObject(Multi::class, ['htmlRenderer' => $secureRendererMock]); } public function testSetValidationContainer() diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/RadioTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/RadioTest.php index cf6fadd3affa2..1cbea7fc85a2f 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/RadioTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/RadioTest.php @@ -10,6 +10,8 @@ use Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Radio; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Framework\DataObject; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class RadioTest extends TestCase { @@ -20,8 +22,18 @@ class RadioTest extends TestCase protected function setUp(): void { + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $this->block = (new ObjectManager($this)) - ->getObject(Radio::class); + ->getObject(Radio::class, ['htmlRenderer' => $secureRendererMock]); } public function testSetValidationContainer() diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/SelectTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/SelectTest.php index 7fde7647e72d7..b289081d2a59a 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/SelectTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/SelectTest.php @@ -10,6 +10,8 @@ use Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Select; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +use Magento\Framework\DataObject; class SelectTest extends TestCase { @@ -20,8 +22,18 @@ class SelectTest extends TestCase protected function setUp(): void { + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $this->block = (new ObjectManager($this)) - ->getObject(Select::class); + ->getObject(Select::class, ['htmlRenderer' => $secureRendererMock]); } public function testSetValidationContainer() diff --git a/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php index 2d86f130767c8..0092c894ac44a 100644 --- a/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php @@ -150,13 +150,13 @@ public function testAfterInitializeIfBundleAnsCustomOptionsAndBundleSelectionsEx $this->productMock->expects($this->once()) ->method('getBundleOptionsData') ->willReturn(['option_1' => ['delete' => 1]]); - $extentionAttribute = $this->getMockBuilder(ProductExtensionInterface::class) + $extensionAttribute = $this->getMockBuilder(ProductExtensionInterface::class) ->disableOriginalConstructor() ->setMethods(['setBundleProductOptions']) ->getMockForAbstractClass(); - $extentionAttribute->expects($this->once())->method('setBundleProductOptions')->with([]); - $this->productMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extentionAttribute); - $this->productMock->expects($this->once())->method('setExtensionAttributes')->with($extentionAttribute); + $extensionAttribute->expects($this->once())->method('setBundleProductOptions')->with([]); + $this->productMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extensionAttribute); + $this->productMock->expects($this->once())->method('setExtensionAttributes')->with($extensionAttribute); $this->model->afterInitialize($this->subjectMock, $this->productMock); } @@ -191,14 +191,14 @@ public function testAfterInitializeIfBundleOptionsNotExist(): void ['affect_bundle_product_selections', null, false], ]; $this->requestMock->expects($this->any())->method('getPost')->willReturnMap($valueMap); - $extentionAttribute = $this->getMockBuilder(ProductExtensionInterface::class) + $extensionAttribute = $this->getMockBuilder(ProductExtensionInterface::class) ->disableOriginalConstructor() ->setMethods(['setBundleProductOptions']) ->getMockForAbstractClass(); - $extentionAttribute->expects($this->once())->method('setBundleProductOptions')->with([]); + $extensionAttribute->expects($this->once())->method('setBundleProductOptions')->with([]); $this->productMock->expects($this->any())->method('getCompositeReadonly')->willReturn(false); - $this->productMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extentionAttribute); - $this->productMock->expects($this->once())->method('setExtensionAttributes')->with($extentionAttribute); + $this->productMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extensionAttribute); + $this->productMock->expects($this->once())->method('setExtensionAttributes')->with($extensionAttribute); $this->productMock->expects($this->once())->method('setCanSaveBundleSelections')->with(false); $this->model->afterInitialize($this->subjectMock, $this->productMock); 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/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php index 771b5c53b3347..b7041051591d8 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php @@ -11,6 +11,7 @@ use Magento\Bundle\Model\Product\Type; use Magento\Bundle\Model\ResourceModel\BundleFactory; use Magento\Bundle\Model\ResourceModel\Option\Collection; +use Magento\CatalogRule\Model\ResourceModel\Product\CollectionProcessor; use Magento\Bundle\Model\ResourceModel\Selection\Collection as SelectionCollection; use Magento\Bundle\Model\ResourceModel\Selection\CollectionFactory; use Magento\Bundle\Model\Selection; @@ -42,6 +43,8 @@ use PHPUnit\Framework\TestCase; /** + * Test for bundle product type + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TypeTest extends TestCase @@ -116,6 +119,11 @@ class TypeTest extends TestCase */ private $arrayUtility; + /** + * @var CollectionProcessor|MockObject + */ + private $catalogRuleProcessor; + /** * @return void */ @@ -172,20 +180,20 @@ protected function setUp(): void ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->serializer = $this->getMockBuilder(Json::class) ->setMethods(null) ->disableOriginalConstructor() ->getMock(); - $this->metadataPool = $this->getMockBuilder(MetadataPool::class) ->disableOriginalConstructor() ->getMock(); - $this->arrayUtility = $this->getMockBuilder(ArrayUtils::class) ->setMethods(['flatten']) ->disableOriginalConstructor() ->getMock(); + $this->catalogRuleProcessor = $this->getMockBuilder(CollectionProcessor::class) + ->disableOriginalConstructor() + ->getMock(); $objectHelper = new ObjectManager($this); $this->model = $objectHelper->getObject( @@ -1542,7 +1550,7 @@ public function testPrepareForCartAdvancedSpecifyProductOptions() $this->parentClass($group, $option, $buyRequest, $product); - $product->expects($this->once()) + $product->expects($this->any()) ->method('getSkipCheckRequiredOption') ->willReturn(true); $buyRequest->expects($this->once()) @@ -2424,9 +2432,6 @@ protected function parentClass($group, $option, $buyRequest, $product) $group->expects($this->once()) ->method('setProcessMode') ->willReturnSelf(); - $group->expects($this->once()) - ->method('validateUserValue') - ->willReturnSelf(); $group->expects($this->once()) ->method('prepareForCart') ->willReturn('someString'); diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml index f028c7013df90..81a6034b9218d 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Attributes\Extend */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $elementHtml = $block->getParentElementHtml(); $attributeCode = $block->getAttribute() @@ -18,18 +19,19 @@ $isElementReadonly = $block->getElement() ->getReadonly(); ?> -<?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)) : ?> +<?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)): ?> <div class="<?= $block->escapeHtmlAttr($attributeCode) ?> "><?= /* @noEscape */ $elementHtml ?></div> <?php endif; ?> <?= $block->getExtendedElement($switchAttributeCode)->toHtml() ?> -<?php if (!$isElementReadonly && $block->getDisableChild()) { ?> - <script> +<?php if (!$isElementReadonly && $block->getDisableChild()) { + $switchAttributeCode = /* @noEscape */ $switchAttributeCode; + $scriptString = <<<script require(['prototype'], function () { - function <?= /* @noEscape */ $switchAttributeCode ?>_change() { - var $attribute = $('<?= $block->escapeJs($attributeCode) ?>'); - if ($('<?= /* @noEscape */ $switchAttributeCode ?>').value == '<?= $block->escapeJs($block::DYNAMIC) ?>') { + function {$switchAttributeCode}_change() { + var $attribute = $('{$block->escapeJs($attributeCode)}'); + if ($('{$switchAttributeCode}').value == '{$block->escapeJs($block::DYNAMIC)}') { if ($attribute) { $attribute.disabled = true; $attribute.value = ''; @@ -40,28 +42,36 @@ $isElementReadonly = $block->getElement() } } else { if ($attribute) { - <?php if ($attributeCode === 'price' && !$block->getCanEditPrice() && $block->getCanReadPrice() - && $block->getProduct()->isObjectNew()) : ?> - <?php $defaultProductPrice = $block->getDefaultProductPrice() ?: "''"; ?> - $attribute.value = <?= /* @noEscape */ (string)$defaultProductPrice ?>; - <?php else : ?> - $attribute.disabled = false; - $attribute.addClassName('required-entry'); - <?php endif; ?> - } - if ($('dynamic-price-warning')) { - $('dynamic-price-warning').hide(); - } +script; + if ($attributeCode === 'price' && !$block->getCanEditPrice() && + $block->getCanReadPrice() && $block->getProduct()->isObjectNew()): + $defaultProductPrice = $block->getDefaultProductPrice() ?: "''"; + $scriptString .= '$attribute.value = ' . /* @noEscape */ (string)$defaultProductPrice . ';'; + else: + $scriptString = <<<script + $attribute.disabled = false; + $attribute.addClassName('required-entry'); +script; + endif; + $scriptString .= <<<script + } + if ($('dynamic-price-warning')) { + $('dynamic-price-warning').hide(); } } - - <?php if (!($attributeCode === 'price' && !$block->getCanEditPrice() - && !$block->getProduct()->isObjectNew())) : ?> - $('<?= /* @noEscape */ $switchAttributeCode ?>').observe('change', <?= /* @noEscape */ $switchAttributeCode ?>_change); - <?php endif; ?> + } +script; + if (!($attributeCode === 'price' && !$block->getCanEditPrice() && !$block->getProduct()->isObjectNew())): + $scriptString .= <<<script + $('{$switchAttributeCode}').observe('change', {$switchAttributeCode}_change); +script; + endif; + $scriptString .= <<<script Event.observe(window, 'load', function(){ - <?= /* @noEscape */ $switchAttributeCode ?>_change(); + {$switchAttributeCode}_change(); }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php } ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml index 53ad0a963244d..91517cf8284dd 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml @@ -5,22 +5,26 @@ */ ?> -<?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Bundle */ ?> +<?php +/* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Bundle */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> + <?php $options = $block->decorateArray($block->getOptions(true)); ?> -<?php if (count($options)) : ?> +<?php if (count($options)): ?> <fieldset id="catalog_product_composite_configure_fields_bundle" class="fieldset admin__fieldset composite-bundle<?= $block->getIsLastFieldset() ? ' last-fieldset' : '' ?>"> <legend class="legend admin__legend"> <span><?= $block->escapeHtml(__('Bundle Items')) ?></span> </legend><br /> - <?php foreach ($options as $option) : ?> - <?php if ($option->getSelections()) : ?> + <?php foreach ($options as $option): ?> + <?php if ($option->getSelections()): ?> <?= $block->getOptionHtml($option) ?> <?php endif; ?> <?php endforeach; ?> </fieldset> -<script> + <?php $scriptString = <<<script require([ "Magento_Catalog/catalog/product/composite/configure" ], function(){ @@ -70,8 +74,12 @@ require([ } } }; - ProductConfigure.bundleControl = new BundleControl(<?= /* @noEscape */ $block->getJsonConfig() ?>); +script; + $scriptString .= 'ProductConfigure.bundleControl = new BundleControl(' . /* @noEscape */ $block->getJsonConfig() . + ');'; + $scriptString .= <<<script }); -</script> - +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml index c8ab6cc5b98d2..89c0f930e21c2 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml @@ -5,27 +5,29 @@ */ /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script if(typeof Bundle=='undefined') { Bundle = {}; } - -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <div id="bundle_product_container" class="entry-edit form-inline"> <fieldset class="fieldset"> <div class="field field-ship-bundle-items"> <label for="shipment_type" class="label"><?= $block->escapeHtml(__('Ship Bundle Items')) ?></label> <div class="control"> - <select <?php if ($block->isReadonly()) : ?>disabled="disabled" <?php endif;?> + <select <?php if ($block->isReadonly()): ?>disabled="disabled" <?php endif;?> id="shipment_type" name="<?= $block->escapeHtmlAttr($block->getFieldSuffix()) ?>[shipment_type]" class="select"> <option value="1"><?= $block->escapeHtml(__('Separately')) ?></option> <option value="0" - <?php if ($block->getProduct()->getShipmentType() == 0) : ?> + <?php if ($block->getProduct()->getShipmentType() == 0): ?> selected="selected" <?php endif; ?> > @@ -47,18 +49,24 @@ if(typeof Bundle=='undefined') { <input type="hidden" name="affect_bundle_product_selections" value="1" /> -<script> +<?php $scriptString = <<<script + require(["prototype", "mage/adminhtml/form"], function(){ // re-bind form elements onchange varienWindowOnload(true); - - <?php if ($block->isReadonly()) :?> +script; +if ($block->isReadonly()): + $scriptString .= <<<script $('product_bundle_container').select('input', 'select', 'textarea', 'button').each(function(input){ input.disabled = true; if (input.tagName.toLowerCase() == 'button') { input.addClassName('disabled'); } }); - <?php endif; ?> +script; +endif; +$scriptString .= <<<script }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml index 4d68d363b7484..d6637401cff9f 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml @@ -5,12 +5,15 @@ */ // phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle\Option */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <script id="bundle-option-template" type="text/x-magento-template"> <div id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>" class="option-box"> - <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-wrapper"> + <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" + id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-wrapper"> <div class="fieldset-wrapper-title"> - <strong class="admin__collapsible-title" data-toggle="collapse" data-target="#<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> + <strong class="admin__collapsible-title" data-toggle="collapse" + data-target="#<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> <span><%- data.default_title %></span> </strong> <div class="actions"> @@ -18,55 +21,67 @@ </div> <div data-role="draggable-handle" class="draggable-handle"></div> </div> - <div class="fieldset-wrapper-content in collapse" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> + <div class="fieldset-wrapper-content in collapse" + id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> <fieldset class="fieldset"> <fieldset class="fieldset-alt"> <div class="field field-option-title required"> - <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title"> + <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>_<%- data.index %>_title"> <?= $block->escapeHtml(__('Option Title')) ?> </label> <div class="control"> - <?php if ($block->isDefaultStore()) : ?> + <?php if ($block->isDefaultStore()): ?> <input class="input-text required-entry" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][title]" - id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][title]" + id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>_<%- data.index %>_title" value="<%- data.title %>" data-original-value="<%- data.title %>" /> - <?php else : ?> + <?php else: ?> <input class="input-text required-entry" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][default_title]" - id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_default_title" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][default_title]" + id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>_<%- data.index %>_default_title" value="<%- data.default_title %>" data-original-value="<%- data.default_title %>" /> <?php endif; ?> <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_id_<%- data.index %>" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][option_id]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][option_id]" value="<%- data.option_id %>" /> <input type="hidden" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][delete]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][delete]" value="" data-state="deleted" /> </div> </div> - <?php if (!$block->isDefaultStore()) : ?> + <?php if (!$block->isDefaultStore()): ?> <div class="field field-option-store-view required"> - <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title_store"> + <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>_<%- data.index %>_title_store"> <?= $block->escapeHtml(__('Store View Title')) ?> </label> <div class="control"> <input class="input-text required-entry" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][title]" - id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title_store" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][title]" + id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>_<%- data.index %>_title_store" value="<%- data.title %>" /> </div> </div> <?php endif; ?> <div class="field field-option-input-type required"> - <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId() . '_<%- data.index %>_type') ?>"> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId() . + '_<%- data.index %>_type') ?>"> <?= $block->escapeHtml(__('Input Type')) ?> </label> <div class="control"> @@ -82,9 +97,13 @@ <label for="field-option-req"> <?= $block->escapeHtml(__('Required')) ?> </label> - <span style="display:none"><?= $block->getRequireSelectHtml() ?></span> + <span><?= $block->getRequireSelectHtml() ?></span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div.field.field-option-req span' + ) ?> <div class="field field-option-position no-display"> <label class="label" for="field-option-position"> <?= $block->escapeHtml(__('Position')) ?> @@ -92,7 +111,8 @@ <div class="control"> <input class="input-text validate-zero-or-greater" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][position]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][position]" value="<%- data.position %>" id="field-option-position" /> </div> @@ -106,13 +126,18 @@ </fieldset> </div> </div> - <div id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_search_<%- data.index %>" class="selection-search"></div> + <div id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_search_<%- data.index %>" + class="selection-search"> + </div> </div> </script> <?= $block->getSelectionHtml() ?> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $helper */ +$helper = $block->getData('jsonHelper'); +$scriptString = <<<script require([ 'jquery', 'mage/template', @@ -140,7 +165,7 @@ function changeInputType(oldObject, oType) { Bundle.Option = Class.create(); Bundle.Option.prototype = { - idLabel : '<?= $block->escapeJs($block->getFieldId()) ?>', + idLabel : '{$block->escapeJs($block->getFieldId())}', templateText : '', itemsCount : 0, initialize : function(template) { @@ -149,7 +174,9 @@ Bundle.Option.prototype = { add : function(data) { if (!data) { - data = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode(['default_title' => __('New Option')]) ?>; +script; +$scriptString .= 'data = ' . $helper->jsonEncode(['default_title' => __('New Option')]) . ';'; +$scriptString .= <<<script } else { data.title = data.title.replace(/</g, "<"); data.title = data.title.replace(/"/g, """); @@ -168,14 +195,14 @@ Bundle.Option.prototype = { //set selected type if (data.type) { - $A($(this.idLabel + '_'+data.index+'_type').options).each(function(option){ + \$A($(this.idLabel + '_'+data.index+'_type').options).each(function(option){ if (option.value==data.type) option.selected = true; }); } //set selected is_require if (data.required) { - $A($(this.idLabel + '_'+data.index+'_required').options).each(function(option){ + \$A($(this.idLabel + '_'+data.index+'_required').options).each(function(option){ if (option.value==data.required) option.selected = true; }); } @@ -215,7 +242,7 @@ Bundle.Option.prototype = { parts = element.id.split('_'); i = parts[2]; if (element.value == 'multi' || element.value == 'checkbox') { - inputs = $A($$('#' + bSelection.idLabel + '_box_' + i + ' tr.selection input.default')); + inputs = \$A($$('#' + bSelection.idLabel + '_box_' + i + ' tr.selection input.default')); inputs.each( function(elem){ //elem.type = "checkbox"; @@ -225,7 +252,7 @@ Bundle.Option.prototype = { /** * Hide not needed elements (user defined qty select box) */ - inputs = $A($$('#' + bSelection.idLabel + '_box_' + i + ' .qty-box')); + inputs = \$A($$('#' + bSelection.idLabel + '_box_' + i + ' .qty-box')); inputs.each( function(elem){ elem.hide(); @@ -233,7 +260,7 @@ Bundle.Option.prototype = { ); } else { - inputs = $A($$('#' + bSelection.idLabel + '_box_' + i + ' tr.selection input.default')); + inputs = \$A($$('#' + bSelection.idLabel + '_box_' + i + ' tr.selection input.default')); have = false; for (j=0; j< inputs.length; j++) { //inputs[j].type = "radio"; @@ -248,7 +275,7 @@ Bundle.Option.prototype = { /** * Show user defined select box */ - inputs = $A($$('#' + bSelection.idLabel + '_box_' + i + ' .qty-box')); + inputs = \$A($$('#' + bSelection.idLabel + '_box_' + i + ' .qty-box')); inputs.each( function(elem){ elem.show(); @@ -258,7 +285,7 @@ Bundle.Option.prototype = { }, priceTypeFixed : function() { - inputs = $A($$('.price-type-box')); + inputs = \$A($$('.price-type-box')); inputs.each( function(elem){ elem.show(); @@ -267,7 +294,7 @@ Bundle.Option.prototype = { }, priceTypeDynamic : function() { - inputs = $A($$('.price-type-box')); + inputs = \$A($$('.price-type-box')); inputs.each( function(elem){ elem.hide(); @@ -278,19 +305,21 @@ Bundle.Option.prototype = { var optionIndex = 0; bOption = new Bundle.Option(optionTemplate); -<?php +script; + foreach ($block->getOptions() as $_option) { /** @var $_option \Magento\Bundle\Model\Option */ - /* @noEscape */ echo 'optionIndex = bOption.add(', $_option->toJson(), ');', PHP_EOL; + $scriptString .= /* @noEscape */ 'optionIndex = bOption.add('. $_option->toJson() . ');' . PHP_EOL; if ($_option->getSelections()) { foreach ($_option->getSelections() as $_selection) { /** @var $_selection \Magento\Catalog\Model\Product */ $_selection->setName($block->escapeHtml($_selection->getName())); - /* @noEscape */ echo 'bSelection.addRow(optionIndex,', $_selection->toJson(), ');', PHP_EOL; + $scriptString .= /* @noEscape */ 'bSelection.addRow(optionIndex,' . $_selection->toJson() . ');' . PHP_EOL; } } } -?> + +$scriptString .= <<<script function togglePriceType() { bOption['priceType' + ($('price_type').value == '1' ? 'Fixed' : 'Dynamic')](); } @@ -304,7 +333,10 @@ jQuery(window).on('load', function() { }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <script type="text/x-magento-init"> { "*": { diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml index 0f1167f3d3eaa..4ada5496ab5a7 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle\Option\Selection */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <script id="bundle-option-selection-box-template" type="text/x-magento-template"> <table class="admin__control-table"> @@ -14,15 +15,16 @@ <th class="col-default"><?= $block->escapeHtml(__('Default')) ?></th> <th class="col-name"><?= $block->escapeHtml(__('Name')) ?></th> <th class="col-sku"><?= $block->escapeHtml(__('SKU')) ?></th> - <?php if ($block->getCanReadPrice() !== false) : ?> + <?php if ($block->getCanReadPrice() !== false): ?> <th class="col-price price-type-box"><?= $block->escapeHtml(__('Price')) ?></th> <th class="col-price price-type-box"><?= $block->escapeHtml(__('Price Type')) ?></th> <?php endif; ?> <th class="col-qty"><?= $block->escapeHtml(__('Default Quantity')) ?></th> <th class="col-uqty qty-box"><?= $block->escapeHtml(__('User Defined')) ?></th> - <th class="col-order type-order" style="display:none"><?= $block->escapeHtml(__('Position')) ?></th> + <th class="col-order type-order"><?= $block->escapeHtml(__('Position')) ?></th> <th class="col-actions"></th> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', 'th.col-order.type-order') ?> </thead> <tbody> </tbody> @@ -33,16 +35,20 @@ <span data-role="draggable-handle" class="draggable-handle"></span> <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_id<%- data.index %>" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_id]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][selection_id]" value="<%- data.selection_id %>"/> <input type="hidden" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][option_id]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][option_id]" value="<%- data.option_id %>"/> <input type="hidden" class="product" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][product_id]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][product_id]" value="<%- data.product_id %>"/> - <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][delete]" + <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][delete]" value="" class="delete"/> </td> @@ -50,19 +56,21 @@ <input onclick="bSelection.checkGroup(event)" type="<%- data.option_type %>" class="default" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][is_default]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][is_default]" value="1" <%- data.checked %> /> </td> <td class="col-name"><%- data.name %></td> <td class="col-sku"><%- data.sku %></td> -<?php if ($block->getCanReadPrice() !== false) : ?> +<?php if ($block->getCanReadPrice() !== false): ?> <td class="col-price price-type-box"> <input id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_value" class="input-text required-entry validate-zero-or-greater" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="<%- data.selection_price_value %>" - <?php if ($block->getCanEditPrice() === false) : ?> + <?php if ($block->getCanEditPrice() === false): ?> disabled="disabled" <?php endif; ?>/> </td> @@ -70,42 +78,51 @@ <?= $block->getPriceTypeSelectHtml() ?> <div><?= $block->getCheckboxScopeHtml() ?></div> </td> -<?php else : ?> +<?php else: ?> <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_value" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="0" /> + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="0" /> <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_type" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_type]" value="0" /> - <?php if ($block->isUsedWebsitePrice()) : ?> + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_type]" value="0" /> + <?php if ($block->isUsedWebsitePrice()): ?> <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_scope" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][default_price_scope]" value="1" /> + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][default_price_scope]" value="1" /> <?php endif; ?> <?php endif; ?> <td class="col-qty"> <input class="input-text required-entry validate-greater-zero-based-on-option validate-zero-or-greater" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_qty]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][selection_qty]" value="<%- data.selection_qty %>" /> </td> <td class="col-uqty qty-box"> <input type="checkbox" class="is-user-defined-qty" checked="checked" /> - <span style="display:none"><?= $block->getQtyTypeSelectHtml() ?></span> + <span><?= $block->getQtyTypeSelectHtml() ?></span> </td> - <td class="col-order type-order" style="display:none"> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', 'td.col-uqty.qty-box span') ?> + <td class="col-order type-order""> <input class="input-text required-entry validate-zero-or-greater" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][position]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][position]" value="<%- data.position %>" /> </td> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', 'td.col-order.type-order') ?> <td class="col-actions"> <span title="Delete Row"> <?= $block->getSelectionDeleteButtonHtml() ?> </span> </td> </script> -<script> + +<?php $isUsedWebsitePrice = (int)$block->isUsedWebsitePrice(); +$scriptString = <<<script require([ 'jquery', 'mage/template', @@ -116,8 +133,8 @@ var bundleTemplateBox = jQuery('#bundle-option-selection-box-template').html(), Bundle.Selection = Class.create(); Bundle.Selection.prototype = { - idLabel : '<?= $block->escapeJs($block->getFieldId()) ?>', - scopePrice : <?= (int)$block->isUsedWebsitePrice() ?>, + idLabel : '{$block->escapeJs($block->getFieldId())}', + scopePrice : {$isUsedWebsitePrice}, templateBox : '', templateRow : '', itemsCount : 0, @@ -125,12 +142,14 @@ Bundle.Selection.prototype = { gridSelection: new Hash(), gridRemoval: new Hash(), gridSelectedProductSkus: [], - selectionSearchUrl: '<?= $block->escapeUrl($block->getSelectionSearchUrl()) ?>', + selectionSearchUrl: '{$block->escapeJs($block->getSelectionSearchUrl())}', initialize : function() { - this.templateBox = '<div class="tier form-list" id="' + this.idLabel + '_box_<%- data.parentIndex %>">' + bundleTemplateBox + '</div>'; + this.templateBox = '<div class="tier form-list" + id="' + this.idLabel + '_box_<%- data.parentIndex %>">' + bundleTemplateBox + '</div>'; - this.templateRow = '<tr class="selection" id="' + this.idLabel + '_row_<%- data.index %>">' + bundleTemplateRow + '</tr>'; + this.templateRow = '<tr class="selection" + id="' + this.idLabel + '_row_<%- data.index %>">' + bundleTemplateRow + '</tr>'; }, gridUpdateCallback: function () { @@ -197,7 +216,7 @@ Bundle.Selection.prototype = { var escapedHTML = this.template({ data: data }).replace(/<(\/?)script/g, '<$1script'); - + Element.insert(tbody[0], {bottom: escapedHTML}); if (data.selection_price_type) { @@ -366,4 +385,6 @@ Bundle.Selection.prototype = { bSelection = new Bundle.Selection(); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/stock/disabler.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/stock/disabler.phtml index b540b59ada343..14cb5f29be69a 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/stock/disabler.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/stock/disabler.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require(['jquery'], function($){ $('[data-tab-panel=product-details]').on('stockbeforedisable', function(e) { if (e.productType === 'bundle') { @@ -13,4 +15,7 @@ require(['jquery'], function($){ } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml index c480d9b126da6..f32fa87c0ac11 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml @@ -3,35 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php + /** * @see \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()) : ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td> </td> <td> </td> <td> </td> @@ -45,160 +50,165 @@ <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-ordered-qty"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()): ?> <tr> <th><?= $block->escapeHtml(__('Invoiced')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyInvoiced() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped() && + $block->isShipmentSeparately($_item)): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyRefunded()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyRefunded()): ?> <tr> <th><?= $block->escapeHtml(__('Refunded')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyRefunded() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyCanceled()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyCanceled()): ?> <tr> <th><?= $block->escapeHtml(__('Canceled')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyCanceled() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php elseif ($block->isShipmentSeparately($_item)) : ?> + <?php elseif ($block->isShipmentSeparately($_item)): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyShipped()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped()): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> - <?php if ($block->canParentReturnToStock($_item)) : ?> + <?php if ($block->canParentReturnToStock($_item)): ?> <td class="col-return-to-stock"> - <?php if ($block->canShowPriceInfo($_item)) : ?> - <?php if ($block->canReturnItemToStock($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canReturnItemToStock($_item)): ?> <input type="checkbox" class="admin__control-checkbox" - name="creditmemo[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>][back_to_stock]" - value="1"<?php if ($_item->getBackToStock()) :?> checked="checked"<?php endif;?> /> + name="creditmemo[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) + ?>][back_to_stock]" + value="1"<?php if ($_item->getBackToStock()):?> checked="checked"<?php endif;?> /> <label class="admin__field-label"></label> <?php endif; ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <?php endif; ?> <td class="col-refund col-qty"> - <?php if ($block->canShowPriceInfo($_item)) : ?> - <?php if ($block->canEditQty()) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canEditQty()): ?> <input type="text" class="input-text admin__control-text qty-input" name="creditmemo[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>][qty]" value="<?= (float)$_item->getQty() * 1 ?>" /> - <?php else : ?> + <?php else: ?> <?= (float)$_item->getQty() * 1 ?> <?php endif; ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax-amount"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-discont"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else : ?> + <?php else: ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml index 0d54e1528dfe9..12a27be743875 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml @@ -3,35 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php + /** * @see \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()) : ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td> </td> <td> </td> <td> </td> @@ -43,83 +48,86 @@ <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= (float)$_item->getQty() * 1 ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-discount"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions()) : ?> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $option) : ?> + <?php foreach ($block->getOrderOptions() as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> 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 a7d49b4b3530a..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 @@ -3,44 +3,50 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php + /** * @see \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> <?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'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $shipTogether = ($_item->getOrderItem()->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) ? !$_item->getOrderItem()->isShipSeparately() : !$_item->getOrderItem()->getParentItem()->isShipSeparately() ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()) : ?> + <?php if ($_item->getOrderItem()->getParentItem()): ?> <?php if ($shipTogether) { - continue; + $canEditItemQty = false; } ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td> </td> <td> </td> <td> </td> @@ -53,148 +59,151 @@ <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"> <div class="option-value"><?= $block->getValueHtml($_item) ?></div> </td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item) || $shipTogether) : ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item) || $shipTogether) : ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><span><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></span></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()): ?> <tr> <th><?= $block->escapeHtml(__('Invoiced')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyInvoiced() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped() && + $block->isShipmentSeparately($_item)): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyRefunded()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyRefunded()): ?> <tr> <th><?= $block->escapeHtml(__('Refunded')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyRefunded() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyCanceled()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyCanceled()): ?> <tr> <th><?= $block->escapeHtml(__('Canceled')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyCanceled() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php elseif ($block->isShipmentSeparately($_item)) : ?> + <?php elseif ($block->isShipmentSeparately($_item)): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyShipped()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped()): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-qty-invoice"> - <?php if ($block->canShowPriceInfo($_item) || $shipTogether) : ?> - <?php if ($block->canEditQty()) : ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> + <?php if ($block->canEditQty() && $canEditItemQty): ?> <input type="text" class="input-text admin__control-text qty-input" name="invoice[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" value="<?= (float)$_item->getQty() * 1 ?>" /> - <?php else : ?> + <?php else: ?> <?= (float)$_item->getQty() * 1 ?> <?php endif; ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-discount"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); - + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else : ?> + <?php else: ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml index e29bb5dbc9479..004c484bb9ba5 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml @@ -3,35 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php + /** * @see \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()) : ?> + <?php if ($_item->getOrderItem()->getParentItem()): ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td> </td> <td> </td> <td> </td> @@ -43,84 +48,87 @@ <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> - <?php else : ?> + <?php else: ?> <td class="col-product"> <div class="option-value"><?= $block->getValueHtml($_item) ?></div> </td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= (float)$_item->getQty() * 1 ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-discount"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions()) : ?> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $option) : ?> + <?php foreach ($block->getOrderOptions() as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml index 233e57a003397..9620f648ae3b8 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml @@ -3,35 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php + /** * @see \Magento\Bundle\Block\Adminhtml\Sales\Order\View\Items\Renderer */ /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\View\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = array_merge([$_item], $_item->getChildrenItems()); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription() || $block->canDisplayGiftmessage()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription() || $block->canDisplayGiftmessage()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_item->getParentItem()) : ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_item->getParentItem()): ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td> </td> <td> </td> <td> </td> @@ -46,156 +51,159 @@ <?php endif; ?> <?php endif; ?> <tr<?= (++$_index==$_count && !$_showlastRow)?' class="border"':'' ?>> - <?php if (!$_item->getParentItem()) : ?> + <?php if (!$_item->getParentItem()): ?> <td class="col-product"> <div class="product-title" id="order_item_<?= $block->escapeHtmlAttr($_item->getId()) ?>_title"> <?= $block->escapeHtml($_item->getName()) ?> </div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"> <div class="option-value"><?= $block->getValueHtml($_item) ?></div> </td> <?php endif; ?> <td class="col-status"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->escapeHtml($_item->getStatus()) ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-price-original"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('original_price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-ordered-qty"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><?= (float)$_item->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getQtyInvoiced()) : ?> + <?php if ((float) $_item->getQtyInvoiced()): ?> <tr> <th><?= $block->escapeHtml(__('Invoiced')) ?></th> <td><?= (float)$_item->getQtyInvoiced() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> + <?php if ((float) $_item->getQtyShipped() && $block->isShipmentSeparately($_item)): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getQtyRefunded()) : ?> + <?php if ((float) $_item->getQtyRefunded()): ?> <tr> <th><?= $block->escapeHtml(__('Refunded')) ?></th> <td><?= (float)$_item->getQtyRefunded() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getQtyCanceled()) : ?> + <?php if ((float) $_item->getQtyCanceled()): ?> <tr> <th><?= $block->escapeHtml(__('Canceled')) ?></th> <td><?= (float)$_item->getQtyCanceled() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php elseif ($block->isShipmentSeparately($_item)) : ?> + <?php elseif ($block->isShipmentSeparately($_item)): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><?= (float)$_item->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getQtyShipped()) : ?> + <?php if ((float) $_item->getQtyShipped()): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax-amount"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax-percent"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayTaxPercent($_item) ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-discont"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr<?php if (!$block->canDisplayGiftmessage()) { echo ' class="border"'; } ?>> <td class="col-product"> - <?php if ($block->getOrderOptions()) : ?> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $option) : ?> + <?php foreach ($block->getOrderOptions() as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?>:</dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else : ?> + <?php else: ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml index 2e52ed906626b..9c3cdcd97bf52 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml @@ -3,11 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ ?> +/** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_item = $block->getItem() ?> <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> @@ -15,19 +19,21 @@ <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()) : ?> + <?php if ($_item->getOrderItem()->getParentItem()): ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td class="col-product"> </td> <td class="col-qty last"> </td> </tr> @@ -35,64 +41,68 @@ <?php endif; ?> <?php endif; ?> <tr class="<?= (++$_index == $_count && !$_showlastRow) ? 'border' : '' ?>"> - <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-ordered-qty"> - <?php if ($block->isShipmentSeparately($_item)) : ?> + <?php if ($block->isShipmentSeparately($_item)): ?> <?= $block->getColumnHtml($_item, 'qty') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-qty last"> - <?php if ($block->isShipmentSeparately($_item)) : ?> + <?php if ($block->isShipmentSeparately($_item)): ?> <input type="text" class="input-text admin__control-text" name="shipment[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" value="<?= (float)$_item->getQty() * 1 ?>" /> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"> + <?= $block->escapeHtml($_remainder) ?> + </span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else : ?> + <?php else: ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml index 521669700e10a..73efa4bddcc1c 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml @@ -3,85 +3,96 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = array_merge([$_item->getOrderItem()], $_item->getOrderItem()->getChildrenItems()) ?> <?php $shipItems = $block->getChildren($_item) ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getParentItem()) : ?> + <?php if ($_item->getParentItem()): ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td class="col-qty last"> </td> </tr> <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getParentItem()) : ?> + <?php if (!$_item->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-qty last"> - <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())) : ?> - <?php if (isset($shipItems[$_item->getId()])) : ?> + <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || + (!$block->isShipmentSeparately() && !$_item->getParentItem())): ?> + <?php if (isset($shipItems[$_item->getId()])): ?> <?= (float)$shipItems[$_item->getId()]->getQty() * 1 ?> - <?php elseif ($_item->getIsVirtual()) : ?> + <?php elseif ($_item->getIsVirtual()): ?> <?= $block->escapeHtml(__('N/A')) ?> - <?php else : ?> + <?php else: ?> 0 <?php endif; ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml index 5b56598dc58e2..4ba6fd6183653 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml @@ -8,40 +8,55 @@ <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox */ ?> <?php $_option = $block->getOption() ?> <?php $_selections = $_option->getSelections() ?> +<?php $inputClass = 'checkbox product bundle option bundle-option-' . $block->escapeHtmlAttr($_option->getId()) ?> +<?php $inputId = 'bundle-option-' . $block->escapeHtmlAttr($_option->getId()) ?> +<?php $inputName = 'bundle_option[' . $block->escapeHtmlAttr($_option->getId()) . ']' ?> +<?php $dataValidation = 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . + $block->escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"' ?> + <div class="field option <?= ($_option->getRequired()) ? ' required': '' ?>"> <label class="label"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> </label> <div class="control"> <div class="nested options-list"> - <?php if ($block->showSingle()) : ?> + <?php if ($block->showSingle()): ?> <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> product bundle option" name="bundle_option[<?= $block->escapeHtml($_option->getId()) ?>]" value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>"/> - <?php else :?> - <?php foreach ($_selections as $_selection) : ?> + <?php else: ?> + <?php foreach ($_selections as $selection): ?> + <?php $sectionId = $selection->getSelectionId() ?> <div class="field choice"> - <input class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> checkbox product bundle option change-container-classname" - id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" + <input class="<?=/* @noEscape */ $inputClass ?> change-container-classname" + id="<?=/* @noEscape */ $inputId . '-' . $block->escapeHtmlAttr($sectionId)?>" type="checkbox" - <?php if ($_option->getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . $block->escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"'; } ?> - name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" - data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" - <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> - <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> - value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/> + <?php if ($_option->getRequired()): ?> + <?= /* @noEscape */ $dataValidation ?> + <?php endif;?> + name="<?=/* @noEscape */ $inputName .'['. $block->escapeHtmlAttr($sectionId)?>]" + data-selector="<?= /* @noEscape */ $inputName.'['.$block->escapeHtmlAttr($sectionId)?>]" + <?php if ($block->isSelected($selection)): ?> + <?= ' checked="checked"' ?> + <?php endif; ?> + <?php if (!$selection->isSaleable()): ?> + <?= ' disabled="disabled"' ?> + <?php endif; ?> + value="<?= $block->escapeHtmlAttr($sectionId) ?>" + data-errors-message-box="#validation-message-box"/> <label class="label" - for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> - <span><?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection) ?></span> + for="<?= /* @noEscape */ $inputId . '-' . $block->escapeHtmlAttr($sectionId) ?>"> + <span><?= /* @noEscape */ $block->getSelectionQtyTitlePrice($selection) ?></span> <br/> - <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($selection) ?> </label> </div> <?php endforeach; ?> <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> + <div id="validation-message-box"></div> <?php endif; ?> </div> </div> diff --git a/app/code/Magento/BundleGraphQl/Model/Order/Shipment/BundleShipmentItemFormatter.php b/app/code/Magento/BundleGraphQl/Model/Order/Shipment/BundleShipmentItemFormatter.php new file mode 100644 index 0000000000000..8e678cdb12d24 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Order/Shipment/BundleShipmentItemFormatter.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Order\Shipment; + +use Magento\Catalog\Model\Product\Type\AbstractType; +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\ShipmentItemInterface; +use Magento\SalesGraphQl\Model\Shipment\Item\ShipmentItemFormatter; +use Magento\SalesGraphQl\Model\Shipment\Item\FormatterInterface; + +/** + * Format Bundle shipment items for GraphQl output + */ +class BundleShipmentItemFormatter implements FormatterInterface +{ + /** + * @var ShipmentItemFormatter + */ + private $itemFormatter; + + /** + * @param ShipmentItemFormatter $itemFormatter + */ + public function __construct(ShipmentItemFormatter $itemFormatter) + { + $this->itemFormatter = $itemFormatter; + } + + /** + * Format bundle product shipment item + * + * @param ShipmentInterface $shipment + * @param ShipmentItemInterface $item + * @return array|null + */ + public function formatShipmentItem(ShipmentInterface $shipment, ShipmentItemInterface $item): ?array + { + $orderItem = $item->getOrderItem(); + $shippingType = $orderItem->getProductOptions()['shipment_type'] ?? null; + if ($shippingType == AbstractType::SHIPMENT_SEPARATELY && !$orderItem->getParentItemId()) { + //When bundle items are shipped separately the children are treated as their own items + return null; + } + return $this->itemFormatter->formatShipmentItem($shipment, $item); + } +} 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/Resolver/Options/BundleItemOptionUid.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php new file mode 100644 index 0000000000000..ce5c12ce69675 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Resolver\Options; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Format new option uid in base64 encode for entered bundle options + */ +class BundleItemOptionUid implements ResolverInterface +{ + /** + * Option type name + */ + private const OPTION_TYPE = 'bundle'; + + /** + * Create a option uid for entered option in "<option-type>/<option-id>/<option-value-id>/<quantity>" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['option_id']) || empty($value['option_id'])) { + throw new GraphQlInputException(__('"option_id" value should be specified.')); + } + + if (!isset($value['selection_id']) || empty($value['selection_id'])) { + throw new GraphQlInputException(__('"selection_id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['option_id'], + $value['selection_id'], + (int) $value['selection_qty'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php new file mode 100644 index 0000000000000..a21bbbb84d735 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Resolver\Order\Item; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Sales\Api\Data\InvoiceItemInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Api\Data\ShipmentItemInterface; +use Magento\Sales\Api\Data\CreditmemoItemInterface; + +/** + * Resolve bundle options items for order item + */ +class BundleOptions implements ResolverInterface +{ + /** + * Serializer + * + * @var Json + */ + private $serializer; + + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @param ValueFactory $valueFactory + * @param Json $serializer + */ + public function __construct( + ValueFactory $valueFactory, + Json $serializer + ) { + $this->valueFactory = $valueFactory; + $this->serializer = $serializer; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + return $this->valueFactory->create(function () use ($value) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + if ($value['model'] instanceof OrderItemInterface) { + $item = $value['model']; + return $this->getBundleOptions($item, $value); + } + if ($value['model'] instanceof InvoiceItemInterface + || $value['model'] instanceof ShipmentItemInterface + || $value['model'] instanceof CreditmemoItemInterface) { + $item = $value['model']; + // Have to pass down order and item to map to avoid refetching all data + return $this->getBundleOptions($item->getOrderItem(), $value); + } + return null; + }); + } + + /** + * Format bundle options and values from a parent bundle order item + * + * @param OrderItemInterface $item + * @param array $formattedItem + * @return array + */ + private function getBundleOptions( + OrderItemInterface $item, + array $formattedItem + ): array { + $bundleOptions = []; + if ($item->getProductType() === 'bundle') { + $options = $item->getProductOptions(); + //loop through options + foreach ($options['bundle_options'] ?? [] as $bundleOptionId => $bundleOption) { + $bundleOptions[$bundleOptionId]['label'] = $bundleOption['label'] ?? ''; + $bundleOptions[$bundleOptionId]['id'] = isset($bundleOption['option_id']) ? + base64_encode($bundleOption['option_id']) : null; + if (isset($bundleOption['option_id'])) { + $bundleOptions[$bundleOptionId]['values'] = $this->formatBundleOptionItems( + $item, + $formattedItem, + $bundleOption['option_id'] + ); + } else { + $bundleOptions[$bundleOptionId]['values'] = []; + } + } + } + return $bundleOptions; + } + + /** + * Format Bundle items + * + * @param OrderItemInterface $item + * @param array $formattedItem + * @param string $bundleOptionId + * @return array + */ + private function formatBundleOptionItems( + OrderItemInterface $item, + array $formattedItem, + string $bundleOptionId + ) { + $optionItems = []; + // Find the item assign to the option + /** @var OrderItemInterface $childrenOrderItem */ + foreach ($item->getChildrenItems() ?? [] as $childrenOrderItem) { + $childOrderItemOptions = $childrenOrderItem->getProductOptions(); + $bundleChildAttributes = $this->serializer + ->unserialize($childOrderItemOptions['bundle_selection_attributes'] ?? ''); + // Value Id is missing from parent, so we have to match the child to parent option + if (isset($bundleChildAttributes['option_id']) + && $bundleChildAttributes['option_id'] == $bundleOptionId) { + $optionItems[$childrenOrderItem->getItemId()] = [ + 'id' => base64_encode($childrenOrderItem->getItemId()), + 'product_name' => $childrenOrderItem->getName(), + 'product_sku' => $childrenOrderItem->getSku(), + 'quantity' => $bundleChildAttributes['qty'], + 'price' => [ + //use options price, not child price + 'value' => $bundleChildAttributes['price'], + //use currency from order + 'currency' => $formattedItem['product_sale_price']['currency'] ?? null, + ] + ]; + } + } + + return $optionItems; + } +} 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/composer.json b/app/code/Magento/BundleGraphQl/composer.json index cb49ab78588b3..e3c54719f4d0e 100644 --- a/app/code/Magento/BundleGraphQl/composer.json +++ b/app/code/Magento/BundleGraphQl/composer.json @@ -10,6 +10,8 @@ "magento/module-quote": "*", "magento/module-quote-graph-ql": "*", "magento/module-store": "*", + "magento/module-sales": "*", + "magento/module-sales-graph-ql": "*", "magento/framework": "*" }, "license": [ diff --git a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml index b847a6672e046..7fe0b2a53677c 100644 --- a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml @@ -65,4 +65,46 @@ </argument> </arguments> </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\OrderItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="bundle" xsi:type="string">BundleOrderItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\InvoiceItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="bundle" xsi:type="string">BundleInvoiceItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\ShipmentItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="bundle" xsi:type="string">BundleShipmentItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\CreditMemoItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="bundle" xsi:type="string">BundleCreditMemoItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\Shipment\ItemProvider"> + <arguments> + <argument name="formatters" xsi:type="array"> + <item name="bundle" xsi:type="object">Magento\BundleGraphQl\Model\Order\Shipment\BundleShipmentItemFormatter\Proxy</item> + </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 0eff0e086180e..a2cba24c7c4d4 100644 --- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls +++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls @@ -66,6 +66,7 @@ type BundleItemOption @doc(description: "BundleItemOption defines characteristic price_type: PriceTypeEnum @doc(description: "One of FIXED, PERCENT, or DYNAMIC.") can_change_quantity: Boolean @doc(description: "Indicates whether the customer can change the number of items for this option.") product: ProductInterface @doc(description: "Contains details about this product option.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\BundleItemOptionUid") # A Base64 string that encodes option details. } type BundleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "BundleProduct defines basic features of a bundle product and contains multiple BundleItems.") { @@ -86,3 +87,37 @@ enum ShipBundleItemsEnum @doc(description: "This enumeration defines whether bun TOGETHER SEPARATELY } + +type BundleOrderItem implements OrderItemInterface { + bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Order\\Item\\BundleOptions") +} + +type BundleInvoiceItem implements InvoiceItemInterface{ + bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Order\\Item\\BundleOptions") +} + +type BundleShipmentItem implements ShipmentItemInterface { + bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Order\\Item\\BundleOptions") +} + +type BundleCreditMemoItem implements CreditMemoItemInterface { + bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Order\\Item\\BundleOptions") +} + +type ItemSelectedBundleOption @doc(description: "A list of options of the selected bundle product") { + id: ID! @doc(description: "The unique identifier of the option") + label: String! @doc(description: "The label of the option") + values: [ItemSelectedBundleOptionValue] @doc(description: "A list of products that represent the values of the parent option") +} + +type ItemSelectedBundleOptionValue @doc(description: "A list of values for the selected bundle product") { + id: ID! @doc(description: "The unique identifier of the value") + product_name: String! @doc(description: "The name of the child bundle product") + product_sku: String! @doc(description: "The SKU of the child bundle product") + 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/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php index e6522054d9f94..49881f67f5c9a 100644 --- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php +++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php @@ -339,7 +339,7 @@ protected function populateSelectionTemplate($selection, $optionId, $parentId, $ /** * Deprecated method for retrieving mapping between skus and products. * - * @deprecated Misspelled method + * @deprecated 100.3.0 Misspelled method * @see retrieveProductsByCachedSkus */ protected function retrieveProducsByCachedSkus() diff --git a/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml b/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml index 9bd9c784b39f2..515c2bc56f067 100644 --- a/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml +++ b/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml @@ -38,8 +38,12 @@ <argument name="importFile" value="catalog_product_import_bundle.csv"/> <argument name="importNoticeMessage" value="Created: 2, Updated: 0, Deleted: 0"/> </actionGroup> - <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCacheAfterCreate"/> - <magentoCLI command="indexer:reindex" stepKey="indexerReindexAfterCreate"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterCreate"> + <argument name="tags" value="full_page"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterCreate"> + <argument name="indices" value=""/> + </actionGroup> <!-- Check Bundle product is visible on the storefront--> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPageAfterCreation"> @@ -56,8 +60,12 @@ <argument name="importFile" value="catalog_product_import_bundle.csv"/> <argument name="importNoticeMessage" value="Created: 0, Updated: 2, Deleted: 0"/> </actionGroup> - <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCacheAfterUpdate"/> - <magentoCLI command="indexer:reindex" stepKey="indexerReindexAfterUpdate"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterUpdate"> + <argument name="tags" value="full_page"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterUpdate"> + <argument name="indices" value=""/> + </actionGroup> <!-- Check Bundle product is still visible on the storefront--> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPageAfterUpdate"> diff --git a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php index aeef8d00f720a..a3ae616dfcf4f 100644 --- a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php +++ b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php @@ -6,21 +6,24 @@ namespace Magento\CacheInvalidate\Model; use Magento\Framework\Cache\InvalidateLogger; +use Magento\PageCache\Model\Cache\Server; +use Laminas\Http\Client\Adapter\Socket; +use Laminas\Uri\Uri; /** - * PurgeCache model + * Invalidate external HTTP cache(s) based on tag pattern */ class PurgeCache { const HEADER_X_MAGENTO_TAGS_PATTERN = 'X-Magento-Tags-Pattern'; /** - * @var \Magento\PageCache\Model\Cache\Server + * @var Server */ protected $cacheServer; /** - * @var \Magento\CacheInvalidate\Model\SocketFactory + * @var SocketFactory */ protected $socketAdapterFactory; @@ -39,39 +42,46 @@ class PurgeCache * * @var int */ - private $requestSize = 7680; + private $maxHeaderSize; /** * Constructor * - * @param \Magento\PageCache\Model\Cache\Server $cacheServer - * @param \Magento\CacheInvalidate\Model\SocketFactory $socketAdapterFactory + * @param Server $cacheServer + * @param SocketFactory $socketAdapterFactory * @param InvalidateLogger $logger + * @param int $maxHeaderSize */ public function __construct( - \Magento\PageCache\Model\Cache\Server $cacheServer, - \Magento\CacheInvalidate\Model\SocketFactory $socketAdapterFactory, - InvalidateLogger $logger + Server $cacheServer, + SocketFactory $socketAdapterFactory, + InvalidateLogger $logger, + int $maxHeaderSize = 7680 ) { $this->cacheServer = $cacheServer; $this->socketAdapterFactory = $socketAdapterFactory; $this->logger = $logger; + $this->maxHeaderSize = $maxHeaderSize; } /** * Send curl purge request to invalidate cache by tags pattern * - * @param string $tagsPattern + * @param array|string $tags * @return bool Return true if successful; otherwise return false */ - public function sendPurgeRequest($tagsPattern) + public function sendPurgeRequest($tags) { + if (is_string($tags)) { + $tags = [$tags]; + } + $successful = true; $socketAdapter = $this->socketAdapterFactory->create(); $servers = $this->cacheServer->getUris(); $socketAdapter->setOptions(['timeout' => 10]); - $formattedTagsChunks = $this->splitTags($tagsPattern); + $formattedTagsChunks = $this->chunkTags($tags); foreach ($formattedTagsChunks as $formattedTagsChunk) { if (!$this->sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk)) { $successful = false; @@ -82,24 +92,24 @@ public function sendPurgeRequest($tagsPattern) } /** - * Split tags by batches + * Split tags into batches to suit Varnish max. header size * - * @param string $tagsPattern + * @param array $tags * @return \Generator */ - private function splitTags($tagsPattern) + private function chunkTags(array $tags): \Generator { - $tagsBatchSize = 0; + $currentBatchSize = 0; $formattedTagsChunk = []; - $formattedTags = explode('|', $tagsPattern); - foreach ($formattedTags as $formattedTag) { - if ($tagsBatchSize + strlen($formattedTag) > $this->requestSize - count($formattedTagsChunk) - 1) { + foreach ($tags as $formattedTag) { + // Check if (currentBatchSize + length of next tag + number of pipe delimiters) would exceed header size. + if ($currentBatchSize + strlen($formattedTag) + count($formattedTagsChunk) > $this->maxHeaderSize) { yield implode('|', $formattedTagsChunk); $formattedTagsChunk = []; - $tagsBatchSize = 0; + $currentBatchSize = 0; } - $tagsBatchSize += strlen($formattedTag); + $currentBatchSize += strlen($formattedTag); $formattedTagsChunk[] = $formattedTag; } if (!empty($formattedTagsChunk)) { @@ -110,12 +120,12 @@ private function splitTags($tagsPattern) /** * Send curl purge request to servers to invalidate cache by tags pattern * - * @param \Laminas\Http\Client\Adapter\Socket $socketAdapter - * @param \Laminas\Uri\Uri[] $servers + * @param Socket $socketAdapter + * @param Uri[] $servers * @param string $formattedTagsChunk * @return bool Return true if successful; otherwise return false */ - private function sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk) + private function sendPurgeRequestToServers(Socket $socketAdapter, array $servers, string $formattedTagsChunk): bool { $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $formattedTagsChunk]; $unresponsiveServerError = []; @@ -145,14 +155,14 @@ private function sendPurgeRequestToServers($socketAdapter, $servers, $formattedT if ($errorCount == count($servers)) { $this->logger->critical( 'No cache server(s) could be purged ' . $loggerMessage, - compact('server', 'formattedTagsChunk') + compact('servers', 'formattedTagsChunk') ); return false; } $this->logger->warning( 'Unresponsive cache server(s) hit' . $loggerMessage, - compact('server', 'formattedTagsChunk') + compact('servers', 'formattedTagsChunk') ); } diff --git a/app/code/Magento/CacheInvalidate/Observer/FlushAllCacheObserver.php b/app/code/Magento/CacheInvalidate/Observer/FlushAllCacheObserver.php index 97e11e8bd2f3f..574263ca1ee0c 100644 --- a/app/code/Magento/CacheInvalidate/Observer/FlushAllCacheObserver.php +++ b/app/code/Magento/CacheInvalidate/Observer/FlushAllCacheObserver.php @@ -7,6 +7,9 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Clear configured Varnish hosts when triggering a full cache flush (e.g. from the Cache Management admin dashboard) + */ class FlushAllCacheObserver implements ObserverInterface { /** @@ -43,7 +46,7 @@ public function __construct( public function execute(\Magento\Framework\Event\Observer $observer) { if ($this->config->getType() == \Magento\PageCache\Model\Config::VARNISH && $this->config->isEnabled()) { - $this->purgeCache->sendPurgeRequest('.*'); + $this->purgeCache->sendPurgeRequest(['.*']); } } } diff --git a/app/code/Magento/CacheInvalidate/Observer/InvalidateVarnishObserver.php b/app/code/Magento/CacheInvalidate/Observer/InvalidateVarnishObserver.php index a3062ea9d6a76..7dba4393d3973 100644 --- a/app/code/Magento/CacheInvalidate/Observer/InvalidateVarnishObserver.php +++ b/app/code/Magento/CacheInvalidate/Observer/InvalidateVarnishObserver.php @@ -77,7 +77,7 @@ public function execute(Observer $observer) $tags[] = sprintf($pattern, $tag); } if (!empty($tags)) { - $this->purgeCache->sendPurgeRequest(implode('|', array_unique($tags))); + $this->purgeCache->sendPurgeRequest(array_unique($tags)); } } } diff --git a/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php b/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php index c88d8f5266fb8..97c3acc194842 100644 --- a/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php +++ b/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php @@ -53,6 +53,7 @@ protected function setUp(): void 'cacheServer' => $this->cacheServer, 'socketAdapterFactory' => $socketFactoryMock, 'logger' => $this->loggerMock, + 'maxHeaderSize' => 256 ] ); } @@ -64,6 +65,7 @@ protected function setUp(): void public function testSendPurgeRequest($hosts) { $uris = []; + /** @var array $host */ foreach ($hosts as $host) { $port = isset($host['port']) ? $host['port'] : Server::DEFAULT_PORT; $uris[] = UriFactory::factory('')->setHost($host['host']) @@ -92,7 +94,50 @@ public function testSendPurgeRequest($hosts) $this->loggerMock->expects($this->once()) ->method('execute'); - $this->assertTrue($this->model->sendPurgeRequest('tags')); + $this->assertTrue($this->model->sendPurgeRequest(['tags'])); + } + + public function testSendMultiPurgeRequest() + { + $tags = [ + '(^|,)cat_p_95(,|$)', '(^|,)cat_p_96(,|$)', '(^|,)cat_p_97(,|$)', '(^|,)cat_p_98(,|$)', + '(^|,)cat_p_99(,|$)', '(^|,)cat_p_100(,|$)', '(^|,)cat_p_10038(,|$)', '(^|,)cat_p_142985(,|$)', + '(^|,)cat_p_199(,|$)', '(^|,)cat_p_300(,|$)', '(^|,)cat_p_12038(,|$)', '(^|,)cat_p_152985(,|$)', + '(^|,)cat_p_299(,|$)', '(^|,)cat_p_400(,|$)', '(^|,)cat_p_13038(,|$)', '(^|,)cat_p_162985(,|$)', + ]; + + $tagsSplitA = array_slice($tags, 0, 12); + $tagsSplitB = array_slice($tags, 12, 4); + + $uri = UriFactory::factory('')->setHost('localhost') + ->setPort(80) + ->setScheme('http'); + + $this->cacheServer->expects($this->once()) + ->method('getUris') + ->willReturn([$uri]); + + $this->socketAdapterMock->expects($this->exactly(2)) + ->method('connect') + ->with($uri->getHost(), $uri->getPort()); + + $this->socketAdapterMock->expects($this->exactly(2)) + ->method('write') + ->withConsecutive( + [ + 'PURGE', $uri, '1.1', + ['X-Magento-Tags-Pattern' => implode('|', $tagsSplitA), 'Host' => $uri->getHost()] + ], + [ + 'PURGE', $uri, '1.1', + ['X-Magento-Tags-Pattern' => implode('|', $tagsSplitB), 'Host' => $uri->getHost()] + ] + ); + + $this->socketAdapterMock->expects($this->exactly(2)) + ->method('close'); + + $this->assertTrue($this->model->sendPurgeRequest($tags)); } /** @@ -130,6 +175,6 @@ public function testSendPurgeRequestWithException() $this->loggerMock->expects($this->once()) ->method('critical'); - $this->assertFalse($this->model->sendPurgeRequest('tags')); + $this->assertFalse($this->model->sendPurgeRequest(['tags'])); } } diff --git a/app/code/Magento/CacheInvalidate/Test/Unit/Observer/FlushAllCacheObserverTest.php b/app/code/Magento/CacheInvalidate/Test/Unit/Observer/FlushAllCacheObserverTest.php index 3db36e4c0e3e9..544e23288d720 100644 --- a/app/code/Magento/CacheInvalidate/Test/Unit/Observer/FlushAllCacheObserverTest.php +++ b/app/code/Magento/CacheInvalidate/Test/Unit/Observer/FlushAllCacheObserverTest.php @@ -56,7 +56,7 @@ public function testFlushAllCache() Config::VARNISH ); - $this->purgeCache->expects($this->once())->method('sendPurgeRequest')->with('.*'); + $this->purgeCache->expects($this->once())->method('sendPurgeRequest')->with(['.*']); $this->model->execute($this->observerMock); } } diff --git a/app/code/Magento/CacheInvalidate/Test/Unit/Observer/InvalidateVarnishObserverTest.php b/app/code/Magento/CacheInvalidate/Test/Unit/Observer/InvalidateVarnishObserverTest.php index d550cd7ea9487..38319dcc724ca 100644 --- a/app/code/Magento/CacheInvalidate/Test/Unit/Observer/InvalidateVarnishObserverTest.php +++ b/app/code/Magento/CacheInvalidate/Test/Unit/Observer/InvalidateVarnishObserverTest.php @@ -82,7 +82,7 @@ protected function setUp(): void public function testInvalidateVarnish() { $tags = ['cache_1', 'cache_group']; - $pattern = '((^|,)cache_1(,|$))|((^|,)cache_group(,|$))'; + $pattern = ['((^|,)cache_1(,|$))', '((^|,)cache_group(,|$))']; $this->configMock->expects($this->once())->method('isEnabled')->willReturn(true); $this->configMock->expects( 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/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml index 9103c4191544c..030c9f5efcf50 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml @@ -14,7 +14,7 @@ <element name="customer" type="button" selector="//div[@class='admin__page-nav-title title _collapsible']//strong[text()='Customers']"/> <element name="customerConfig" type="text" selector="//span[text()='Customer Configuration']"/> <element name="captcha" type="button" selector="#customer_captcha-head"/> - <element name="dependent" type="button" selector="//a[@id='customer_captcha-head' and @class='open']"/> + <element name="dependent" type="button" selector="a#customer_captcha-head.open"/> <element name="forms" type="multiselect" selector="#customer_captcha_forms"/> <element name="createUser" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='user_create']"/> <element name="forgotpassword" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='user_forgotpassword']"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml index 28253fb3c00ef..58cfd7aacd631 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml @@ -23,12 +23,16 @@ <before> <magentoCLI command="config:set {{AdminCaptchaLength3ConfigData.path}} {{AdminCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> <magentoCLI command="config:set {{AdminCaptchaSymbols1ConfigData.path}} {{AdminCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{AdminCaptchaDefaultLengthConfigData.path}} {{AdminCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> <magentoCLI command="config:set {{AdminCaptchaDefaultSymbolsConfigData.path}} {{AdminCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminWithWrongCredentialsFirstAttempt"> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml index bfea4e99996c3..9e99fa96ee766 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml @@ -34,9 +34,7 @@ </after> <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.sku$$)}}" stepKey="openProductPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <waitForText userInput="You added $$createSimpleProduct.name$$ to your shopping cart." stepKey="waitForText"/> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickCart"/> <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml index 54237087227d8..2736888154483 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml @@ -24,7 +24,9 @@ <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerChangePasswordConfigData.path}} {{StorefrontCaptchaOnCustomerChangePasswordConfigData.value}}" stepKey="enableUserEditCaptcha"/> <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <createData entity="Simple_US_Customer" stepKey="customer"/> <!-- Sign in as customer --> @@ -37,7 +39,9 @@ <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml index 0c6a3f31c1df2..22f1ed1af3e28 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml @@ -23,13 +23,17 @@ <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> <magentoCLI command="config:set {{StorefrontCaptchaOnContactUsFormConfigData.path}} {{StorefrontCaptchaOnContactUsFormConfigData.value}}" stepKey="enableUserEditCaptcha"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <!-- Open storefront contact us form --> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml index 5a1be68d3f251..332d7eb6067b5 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml @@ -22,13 +22,17 @@ <before> <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <createData entity="Simple_US_Customer" stepKey="customer"/> </before> <after> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml index 2c331f958e467..b7d5b60ddc632 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml @@ -25,7 +25,9 @@ <magentoCLI command="config:set {{StorefrontCustomerCaptchaModeAlwaysConfigData.path}} {{StorefrontCustomerCaptchaModeAlwaysConfigData.value}}" stepKey="alwaysEnableCaptcha" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <!-- Set default configuration. --> @@ -33,7 +35,9 @@ <magentoCLI command="config:set {{StorefrontCustomerCaptchaModeAfterFailConfigData.path}} {{StorefrontCustomerCaptchaModeAfterFailConfigData.value}}" stepKey="defaultCaptchaMode" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <!-- Open Customer registration page --> diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php index f243718952f4f..3c5aed076a653 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php @@ -63,7 +63,7 @@ class AjaxLoginTest extends TestCase /** * @var array */ - protected $formIds; + protected $formIds = ['user_login']; /** * @var AjaxLogin @@ -97,7 +97,6 @@ protected function setUp(): void ->method('getCaptcha') ->willReturn($this->captchaMock); - $this->formIds = ['user_login']; $this->serializerMock = $this->createMock(Json::class); $this->model = new AjaxLogin( @@ -194,7 +193,10 @@ public function testAroundExecuteCaptchaIsNotRequired($username, $requestContent $this->captchaMock->expects($this->once())->method('isRequired')->with($username) ->willReturn(false); - $this->captchaMock->expects($this->never())->method('logAttempt')->with($username); + $expectLogAttempt = $requestContent['captcha_form_id'] ?? false; + $this->captchaMock + ->expects($expectLogAttempt ? $this->once() : $this->never()) + ->method('logAttempt')->with($username); $this->captchaMock->expects($this->never())->method('isCorrect'); $closure = function () { diff --git a/app/code/Magento/Captcha/etc/adminhtml/system.xml b/app/code/Magento/Captcha/etc/adminhtml/system.xml index ac4197c976ea0..bc3874989435c 100644 --- a/app/code/Magento/Captcha/etc/adminhtml/system.xml +++ b/app/code/Magento/Captcha/etc/adminhtml/system.xml @@ -57,7 +57,7 @@ <depends> <field id="enable">1</field> </depends> - <frontend_class>required-entry</frontend_class> + <frontend_class>required-entry validate-range range-1-8</frontend_class> </field> <field id="symbols" translate="label comment" type="text" sortOrder="8" showInDefault="1" canRestore="1"> <label>Symbols Used in CAPTCHA</label> diff --git a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml index 88e0d5edc2a7d..e73174d3768df 100644 --- a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml +++ b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml @@ -5,8 +5,9 @@ */ /** @var \Magento\Captcha\Block\Captcha\DefaultCaptcha $block */ - /** @var \Magento\Captcha\Model\DefaultModel $captcha */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $captcha = $block->getCaptchaModel(); ?> <div class="admin__field _required"> @@ -18,11 +19,13 @@ $captcha = $block->getCaptchaModel(); id="captcha" class="admin__control-text" type="text" - name="<?= $block->escapeHtmlAttr(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE) ?>[<?= $block->escapeHtml($block->getFormId()) ?>]" + name="<?= $block->escapeHtmlAttr(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE) + ?>[<?= $block->escapeHtml($block->getFormId()) ?>]" data-validate="{required:true}"/> - <?php if ($captcha->isCaseSensitive()) :?> + <?php if ($captcha->isCaseSensitive()):?> <div class="admin__field-note"> - <span><?= $block->escapeHtml(__('<strong>Attention</strong>: Captcha is case sensitive.'), ['strong']) ?></span> + <span><?= $block->escapeHtml(__('<strong>Attention</strong>: Captcha is case sensitive.'), ['strong']) + ?></span> </div> <?php endif; ?> </div> @@ -39,11 +42,16 @@ $captcha = $block->getCaptchaModel(); height="<?= /* @noEscape */ (float) $block->getImgHeight() ?>" src="<?= $block->escapeUrl($captcha->getImgSrc()) ?>" /> </div> -<script> + +<?php +$url = $block->escapeJs($block->getRefreshUrl()); +$formId = $block->escapeJs($block->escapeHtml($block->getFormId())); +$scriptString = <<<script + require(["prototype", "mage/captcha"], function(){ //<![CDATA[ - var captcha = new Captcha('<?= $block->escapeJs($block->escapeUrl($block->getRefreshUrl())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getFormId())) ?>'); + var captcha = new Captcha('{$url}', '{$formId}'); $('captcha-reload').observe('click', function () { captcha.refresh(this); @@ -52,4 +60,6 @@ $captcha = $block->getCaptchaModel(); //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/CardinalCommerce/Model/JwtManagement.php b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php index 953af1751fd65..076e122e8e819 100644 --- a/app/code/Magento/CardinalCommerce/Model/JwtManagement.php +++ b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php @@ -8,6 +8,7 @@ namespace Magento\CardinalCommerce\Model; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Encryption\Helper\Security; /** * JSON Web Token management. @@ -62,7 +63,8 @@ public function decode(string $jwt, string $key): array $payload = $this->json->unserialize($payloadJson); $signature = $this->urlSafeB64Decode($signatureB64); - if ($signature !== $this->sign($headB64 . '.' . $payloadB64, $key, $header['alg'])) { + + if (!Security::compareStrings($signature, $this->sign($headB64 . '.' . $payloadB64, $key, $header['alg']))) { throw new \InvalidArgumentException('JWT signature verification failed'); } 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/CardinalCommerce/etc/csp_whitelist.xml b/app/code/Magento/CardinalCommerce/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..946f22b219909 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/csp_whitelist.xml @@ -0,0 +1,52 @@ +<?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="script-src"> + <values> + <value id="cardinal_commerce_geo_stag" type="host">geostag.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf_stag" type="host">1eafstag.cardinalcommerce.com</value> + <value id="cardinal_commerce_geo" type="host">geoapi.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf" type="host">1eafapi.cardinalcommerce.com</value> + <value id="cardinal_commerce_songbird" type="host">songbird.cardinalcommerce.com</value> + <value id="cardinal_commerce_test" type="host">includestest.ccdc02.com</value> + </values> + </policy> + <policy id="connect-src"> + <values> + <value id="cardinal_commerce_geo_stag" type="host">geostag.cardinalcommerce.com</value> + <value id="cardinal_commerce_geo" type="host">geo.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf_stag" type="host">1eafstag.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf" type="host">1eaf.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent_stag" type="host">centinelapistag.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent" type="host">centinelapi.cardinalcommerce.com</value> + </values> + </policy> + <policy id="frame-src"> + <values> + <value id="cardinal_commerce_geo_stag" type="host">geostag.cardinalcommerce.com</value> + <value id="cardinal_commerce_geo" type="host">geo.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf_stag" type="host">1eafstag.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf" type="host">1eaf.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent_stag" type="host">centinelapistag.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent" type="host">centinelapi.cardinalcommerce.com</value> + </values> + </policy> + <policy id="form-action"> + <values> + <value id="cardinal_commerce_geo_stag" type="host">geostag.cardinalcommerce.com</value> + <value id="cardinal_commerce_geo" type="host">geo.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf_stag" type="host">1eafstag.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf" type="host">1eaf.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent_stag" type="host">centinelapistag.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent" type="host">centinelapi.cardinalcommerce.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php b/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php index 05e5106b287a0..2afa14d874e4b 100644 --- a/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php +++ b/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php @@ -9,7 +9,7 @@ /** * Base prices storage. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface BasePriceStorageInterface { @@ -18,7 +18,7 @@ interface BasePriceStorageInterface * * @param string[] $skus * @return \Magento\Catalog\Api\Data\BasePriceInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function get(array $skus); @@ -33,7 +33,7 @@ public function get(array $skus); * @param \Magento\Catalog\Api\Data\BasePriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] * @throws \Magento\Framework\Exception\CouldNotSaveException - * @since 101.1.0 + * @since 102.0.0 */ public function update(array $prices); } diff --git a/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php b/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php index 62eba5987c35d..08265d6583090 100644 --- a/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php +++ b/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php @@ -8,7 +8,7 @@ /** * @api - * @since 100.0.2 + * @since 104.0.0 */ interface CategoryListDeleteBySkuInterface { @@ -22,6 +22,7 @@ interface CategoryListDeleteBySkuInterface * @throws \Magento\Framework\Exception\CouldNotSaveException * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\InputException + * @since 104.0.0 */ public function deleteBySkus(int $categoryId, array $productSkuList): bool; } diff --git a/app/code/Magento/Catalog/Api/CategoryListInterface.php b/app/code/Magento/Catalog/Api/CategoryListInterface.php index 22a9da00eaffc..2f6cd1f38730a 100644 --- a/app/code/Magento/Catalog/Api/CategoryListInterface.php +++ b/app/code/Magento/Catalog/Api/CategoryListInterface.php @@ -7,7 +7,7 @@ /** * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CategoryListInterface { @@ -16,7 +16,7 @@ interface CategoryListInterface * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \Magento\Catalog\Api\Data\CategorySearchResultsInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria); } diff --git a/app/code/Magento/Catalog/Api/CostStorageInterface.php b/app/code/Magento/Catalog/Api/CostStorageInterface.php index a52d290bd46d8..debb791a7b756 100644 --- a/app/code/Magento/Catalog/Api/CostStorageInterface.php +++ b/app/code/Magento/Catalog/Api/CostStorageInterface.php @@ -9,7 +9,7 @@ /** * Product cost storage. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CostStorageInterface { @@ -19,7 +19,7 @@ interface CostStorageInterface * @param string[] $skus * @return \Magento\Catalog\Api\Data\CostInterface[] * @throws \Magento\Framework\Exception\NoSuchEntityException - * @since 101.1.0 + * @since 102.0.0 */ public function get(array $skus); @@ -33,7 +33,7 @@ public function get(array $skus); * * @param \Magento\Catalog\Api\Data\CostInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function update(array $prices); @@ -45,7 +45,7 @@ public function update(array $prices); * @return bool Will return True if deleted. * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\CouldNotDeleteException - * @since 101.1.0 + * @since 102.0.0 */ public function delete(array $skus); } diff --git a/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php b/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php index a527bbfe947ab..1bce067650313 100644 --- a/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php +++ b/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php @@ -9,7 +9,7 @@ /** * Price interface. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface BasePriceInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -26,7 +26,7 @@ interface BasePriceInterface extends \Magento\Framework\Api\ExtensibleDataInterf * * @param float $price * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPrice($price); @@ -34,7 +34,7 @@ public function setPrice($price); * Get price. * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getPrice(); @@ -43,7 +43,7 @@ public function getPrice(); * * @param int $storeId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setStoreId($storeId); @@ -51,7 +51,7 @@ public function setStoreId($storeId); * Get store id. * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getStoreId(); @@ -60,7 +60,7 @@ public function getStoreId(); * * @param string $sku * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setSku($sku); @@ -68,7 +68,7 @@ public function setSku($sku); * Get SKU. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getSku(); @@ -76,7 +76,7 @@ public function getSku(); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\BasePriceExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -85,7 +85,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\BasePriceExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\BasePriceExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/CategoryLinkInterface.php b/app/code/Magento/Catalog/Api/Data/CategoryLinkInterface.php index e9c0e04c4f746..c9ca57dc1eff1 100644 --- a/app/code/Magento/Catalog/Api/Data/CategoryLinkInterface.php +++ b/app/code/Magento/Catalog/Api/Data/CategoryLinkInterface.php @@ -10,20 +10,20 @@ /** * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CategoryLinkInterface extends ExtensibleDataInterface { /** * @return int|null - * @since 101.1.0 + * @since 102.0.0 */ public function getPosition(); /** * @param int $position * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPosition($position); @@ -31,7 +31,7 @@ public function setPosition($position); * Get category id * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getCategoryId(); @@ -40,7 +40,7 @@ public function getCategoryId(); * * @param string $categoryId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setCategoryId($categoryId); @@ -48,7 +48,7 @@ public function setCategoryId($categoryId); * Retrieve existing extension attributes object. * * @return \Magento\Catalog\Api\Data\CategoryLinkExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -57,7 +57,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\CategoryLinkExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\CategoryLinkExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/CategorySearchResultsInterface.php b/app/code/Magento/Catalog/Api/Data/CategorySearchResultsInterface.php index 38f3f89d6a0c5..0ed03c2d56519 100644 --- a/app/code/Magento/Catalog/Api/Data/CategorySearchResultsInterface.php +++ b/app/code/Magento/Catalog/Api/Data/CategorySearchResultsInterface.php @@ -9,7 +9,7 @@ /** * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CategorySearchResultsInterface extends SearchResultsInterface { @@ -17,7 +17,7 @@ interface CategorySearchResultsInterface extends SearchResultsInterface * Get categories * * @return \Magento\Catalog\Api\Data\CategoryInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function getItems(); @@ -26,7 +26,7 @@ public function getItems(); * * @param \Magento\Catalog\Api\Data\CategoryInterface[] $items * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setItems(array $items); } diff --git a/app/code/Magento/Catalog/Api/Data/CostInterface.php b/app/code/Magento/Catalog/Api/Data/CostInterface.php index a9966f56bafa3..ed5e8cd8a2bb2 100644 --- a/app/code/Magento/Catalog/Api/Data/CostInterface.php +++ b/app/code/Magento/Catalog/Api/Data/CostInterface.php @@ -9,7 +9,7 @@ /** * Cost interface. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CostInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -26,7 +26,7 @@ interface CostInterface extends \Magento\Framework\Api\ExtensibleDataInterface * * @param float $cost * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setCost($cost); @@ -34,7 +34,7 @@ public function setCost($cost); * Get cost value. * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getCost(); @@ -43,7 +43,7 @@ public function getCost(); * * @param int $storeId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setStoreId($storeId); @@ -51,7 +51,7 @@ public function setStoreId($storeId); * Get store id. * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getStoreId(); @@ -60,7 +60,7 @@ public function getStoreId(); * * @param string $sku * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setSku($sku); @@ -68,7 +68,7 @@ public function setSku($sku); * Get SKU. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getSku(); @@ -76,7 +76,7 @@ public function getSku(); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\CostExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -85,7 +85,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\CostExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\CostExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/EavAttributeInterface.php b/app/code/Magento/Catalog/Api/Data/EavAttributeInterface.php index dabd3234c6dab..08696156fa11a 100644 --- a/app/code/Magento/Catalog/Api/Data/EavAttributeInterface.php +++ b/app/code/Magento/Catalog/Api/Data/EavAttributeInterface.php @@ -145,7 +145,7 @@ public function getIsFilterableInGrid(); * * @param bool|null $isUsedInGrid * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setIsUsedInGrid($isUsedInGrid); @@ -154,7 +154,7 @@ public function setIsUsedInGrid($isUsedInGrid); * * @param bool|null $isVisibleInGrid * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setIsVisibleInGrid($isVisibleInGrid); @@ -163,7 +163,7 @@ public function setIsVisibleInGrid($isVisibleInGrid); * * @param bool|null $isFilterableInGrid * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setIsFilterableInGrid($isFilterableInGrid); diff --git a/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php b/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php index 426c5becc7a24..25bd9eeb438c9 100644 --- a/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php +++ b/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php @@ -9,7 +9,7 @@ /** * Interface returned in case of incorrect price passed to efficient price API. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface PriceUpdateResultInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -24,7 +24,7 @@ interface PriceUpdateResultInterface extends \Magento\Framework\Api\ExtensibleDa * Get error message, that contains description of error occurred during price update. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getMessage(); @@ -33,7 +33,7 @@ public function getMessage(); * * @param string $message * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setMessage($message); @@ -41,7 +41,7 @@ public function setMessage($message); * Get parameters, that could be displayed in error message placeholders. * * @return string[] - * @since 101.1.0 + * @since 102.0.0 */ public function getParameters(); @@ -50,7 +50,7 @@ public function getParameters(); * * @param string[] $parameters * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setParameters(array $parameters); @@ -59,7 +59,7 @@ public function setParameters(array $parameters); * If extension attributes do not exist return null. * * @return \Magento\Catalog\Api\Data\PriceUpdateResultExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -68,7 +68,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\PriceUpdateResultExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\PriceUpdateResultExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/ProductAttributeInterface.php b/app/code/Magento/Catalog/Api/Data/ProductAttributeInterface.php index 590c23a0aa0b1..15fd17f41e158 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductAttributeInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductAttributeInterface.php @@ -35,6 +35,7 @@ interface ProductAttributeInterface extends \Magento\Catalog\Api\Data\EavAttribu /** * @return \Magento\Eav\Api\Data\AttributeExtensionInterface|null + * @since 103.0.0 */ public function getExtensionAttributes(); } diff --git a/app/code/Magento/Catalog/Api/Data/ProductFrontendActionInterface.php b/app/code/Magento/Catalog/Api/Data/ProductFrontendActionInterface.php index 9a6cfebc71fa0..610c457215853 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductFrontendActionInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductFrontendActionInterface.php @@ -9,7 +9,7 @@ * Represents Data Object for a Product Frontend Action like Product View or Comparison * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ProductFrontendActionInterface { @@ -17,7 +17,7 @@ interface ProductFrontendActionInterface * Gets Identifier of a Product Frontend Action * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getActionId(); @@ -26,7 +26,7 @@ public function getActionId(); * * @param int $actionId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setActionId($actionId); @@ -34,7 +34,7 @@ public function setActionId($actionId); * Gets Identifier of Visitor who performs a Product Frontend Action * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getVisitorId(); @@ -43,7 +43,7 @@ public function getVisitorId(); * * @param int $visitorId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setVisitorId($visitorId); @@ -51,7 +51,7 @@ public function setVisitorId($visitorId); * Gets Identifier of Customer who performs a Product Frontend Action * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getCustomerId(); @@ -60,7 +60,7 @@ public function getCustomerId(); * * @param int $customerId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setCustomerId($customerId); @@ -68,7 +68,7 @@ public function setCustomerId($customerId); * Gets Identifier of Product a Product Frontend Action is performed on * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getProductId(); @@ -77,7 +77,7 @@ public function getProductId(); * * @param int $productId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setProductId($productId); @@ -85,7 +85,7 @@ public function setProductId($productId); * Gets Identifier of Type of a Product Frontend Action * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getTypeId(); @@ -94,7 +94,7 @@ public function getTypeId(); * * @param string $typeId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setTypeId($typeId); @@ -102,7 +102,7 @@ public function setTypeId($typeId); * Gets JS timestamp of a Product Frontend Action (in microseconds) * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getAddedAt(); @@ -111,7 +111,7 @@ public function getAddedAt(); * * @param int $addedAt * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setAddedAt($addedAt); } diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/ButtonInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/ButtonInterface.php index e2f4dfa201593..592b47d216ae9 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/ButtonInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/ButtonInterface.php @@ -14,7 +14,7 @@ * This interface represents all manner of product buttons: add to cart, add to compare, etc... * The buttons describes by this interface should have interaction with backend * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ButtonInterface extends ExtensibleDataInterface { @@ -22,7 +22,7 @@ interface ButtonInterface extends ExtensibleDataInterface * @param string $postData Post data should be serialized (JSON/serialized) string * Post data can be empty * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setPostData($postData); @@ -33,7 +33,7 @@ public function setPostData($postData); * to handle product action * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getPostData(); @@ -44,7 +44,7 @@ public function getPostData(); * * @param string $url * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setUrl($url); @@ -52,7 +52,7 @@ public function setUrl($url); * Retrieve url, needed to add product to cart * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getUrl(); @@ -62,7 +62,7 @@ public function getUrl(); * * @param bool $requiredOptions * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setRequiredOptions($requiredOptions); @@ -70,7 +70,7 @@ public function setRequiredOptions($requiredOptions); * Retrieve flag whether a product has options or not * * @return bool - * @since 101.1.0 + * @since 102.0.0 */ public function hasRequiredOptions(); @@ -78,7 +78,7 @@ public function hasRequiredOptions(); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\ProductRender\ButtonExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -87,7 +87,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\ProductRender\ButtonExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRender\ButtonExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php index d111de1b04b94..6022c5198769a 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php @@ -15,7 +15,7 @@ * Consider currency, rounding and html * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface FormattedPriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -23,7 +23,7 @@ interface FormattedPriceInfoInterface extends \Magento\Framework\Api\ExtensibleD * Retrieve html with final price * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getFinalPrice(); @@ -34,7 +34,7 @@ public function getFinalPrice(); * * @param string $finalPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setFinalPrice($finalPrice); @@ -44,7 +44,7 @@ public function setFinalPrice($finalPrice); * E.g. for product with custom options is price with the most expensive custom option * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getMaxPrice(); @@ -53,7 +53,7 @@ public function getMaxPrice(); * * @param string $maxPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMaxPrice($maxPrice); @@ -63,7 +63,7 @@ public function setMaxPrice($maxPrice); * The minimal price is for example, the lowest price of all variations for complex product * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getMinimalPrice(); @@ -74,7 +74,7 @@ public function getMinimalPrice(); * * @param string $maxRegularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMaxRegularPrice($maxRegularPrice); @@ -82,7 +82,7 @@ public function setMaxRegularPrice($maxRegularPrice); * Retrieve max regular price * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getMaxRegularPrice(); @@ -91,7 +91,7 @@ public function getMaxRegularPrice(); * * @param string $minRegularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMinimalRegularPrice($minRegularPrice); @@ -99,7 +99,7 @@ public function setMinimalRegularPrice($minRegularPrice); * Retrieve minimal regular price * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getMinimalRegularPrice(); @@ -110,7 +110,7 @@ public function getMinimalRegularPrice(); * * @param string $specialPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setSpecialPrice($specialPrice); @@ -118,7 +118,7 @@ public function setSpecialPrice($specialPrice); * Retrieve special price * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getSpecialPrice(); @@ -127,7 +127,7 @@ public function getSpecialPrice(); * * @param string $minimalPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMinimalPrice($minimalPrice); @@ -137,7 +137,7 @@ public function setMinimalPrice($minimalPrice); * Usually this price is corresponding to price in admin panel of product * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getRegularPrice(); @@ -146,7 +146,7 @@ public function getRegularPrice(); * * @param string $regularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setRegularPrice($regularPrice); @@ -154,7 +154,7 @@ public function setRegularPrice($regularPrice); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\ProductRender\FormattedPriceInfoExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -163,7 +163,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\ProductRender\FormattedPriceInfoExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRender\FormattedPriceInfoExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php index 45b070d2706dc..49789e5ce9ed7 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php @@ -14,7 +14,7 @@ * Represents physical characteristics of image, that can be used in product listing or product view * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ImageInterface extends ExtensibleDataInterface { @@ -24,7 +24,7 @@ interface ImageInterface extends ExtensibleDataInterface * * @param string $url * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setUrl($url); @@ -32,7 +32,7 @@ public function setUrl($url); * Retrieve image url * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getUrl(); @@ -43,7 +43,7 @@ public function getUrl(); * What size should this image have, etc... * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getCode(); @@ -52,7 +52,7 @@ public function getCode(); * * @param string $code * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setCode($code); @@ -61,7 +61,7 @@ public function setCode($code); * * @param string $height * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setHeight($height); @@ -69,7 +69,7 @@ public function setHeight($height); * Retrieve image height * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getHeight(); @@ -77,7 +77,7 @@ public function getHeight(); * Set image width in px * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getWidth(); @@ -86,7 +86,7 @@ public function getWidth(); * * @param string $width * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setWidth($width); @@ -96,7 +96,7 @@ public function setWidth($width); * Image label is short description of this image * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getLabel(); @@ -105,7 +105,7 @@ public function getLabel(); * * @param string $label * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setLabel($label); @@ -115,7 +115,7 @@ public function setLabel($label); * This width is image dimension, which represents the width, that can be used for performance improvements * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getResizedWidth(); @@ -124,7 +124,7 @@ public function getResizedWidth(); * * @param string $width * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setResizedWidth($width); @@ -133,7 +133,7 @@ public function setResizedWidth($width); * * @param string $height * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setResizedHeight($height); @@ -141,7 +141,7 @@ public function setResizedHeight($height); * Retrieve resize height * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getResizedHeight(); @@ -149,7 +149,7 @@ public function getResizedHeight(); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -158,7 +158,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php index 9768b3c08c8ab..0e9b1c53fcd14 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php @@ -10,7 +10,7 @@ * Price interface. * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface PriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -18,7 +18,7 @@ interface PriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterf * Retrieve final price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getFinalPrice(); @@ -29,7 +29,7 @@ public function getFinalPrice(); * * @param float $finalPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setFinalPrice($finalPrice); @@ -39,7 +39,7 @@ public function setFinalPrice($finalPrice); * E.g. for product with custom options is price with the most expensive custom option * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getMaxPrice(); @@ -48,7 +48,7 @@ public function getMaxPrice(); * * @param float $maxPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMaxPrice($maxPrice); @@ -60,7 +60,7 @@ public function setMaxPrice($maxPrice); * * @param float $maxRegularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMaxRegularPrice($maxRegularPrice); @@ -68,7 +68,7 @@ public function setMaxRegularPrice($maxRegularPrice); * Retrieve max regular price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getMaxRegularPrice(); @@ -77,7 +77,7 @@ public function getMaxRegularPrice(); * * @param float $minRegularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMinimalRegularPrice($minRegularPrice); @@ -85,7 +85,7 @@ public function setMinimalRegularPrice($minRegularPrice); * Retrieve minimal regular price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getMinimalRegularPrice(); @@ -96,7 +96,7 @@ public function getMinimalRegularPrice(); * * @param float $specialPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setSpecialPrice($specialPrice); @@ -104,7 +104,7 @@ public function setSpecialPrice($specialPrice); * Retrieve special price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getSpecialPrice(); @@ -112,7 +112,7 @@ public function getSpecialPrice(); * Retrieve minimal price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getMinimalPrice(); @@ -121,7 +121,7 @@ public function getMinimalPrice(); * * @param float $minimalPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMinimalPrice($minimalPrice); @@ -129,7 +129,7 @@ public function setMinimalPrice($minimalPrice); * Retrieve regular price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getRegularPrice(); @@ -140,7 +140,7 @@ public function getRegularPrice(); * * @param float $regularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setRegularPrice($regularPrice); @@ -148,7 +148,7 @@ public function setRegularPrice($regularPrice); * Retrieve dto with formatted prices * * @return \Magento\Catalog\Api\Data\ProductRender\FormattedPriceInfoInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getFormattedPrices(); @@ -157,7 +157,7 @@ public function getFormattedPrices(); * * @param FormattedPriceInfoInterface $formattedPriceInfo * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setFormattedPrices(FormattedPriceInfoInterface $formattedPriceInfo); @@ -165,7 +165,7 @@ public function setFormattedPrices(FormattedPriceInfoInterface $formattedPriceIn * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\ProductRender\PriceInfoExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -174,7 +174,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\ProductRender\PriceInfoExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRender\PriceInfoExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php index 166a1aba76b61..e1bd1a7899b67 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php @@ -15,7 +15,7 @@ * This information is put into part as Add To Cart or Add to Compare Data or Price Data * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ProductRenderInterface extends ExtensibleDataInterface { @@ -23,7 +23,7 @@ interface ProductRenderInterface extends ExtensibleDataInterface * Provide information needed for render "Add To Cart" button on front * * @return \Magento\Catalog\Api\Data\ProductRender\ButtonInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getAddToCartButton(); @@ -32,7 +32,7 @@ public function getAddToCartButton(); * * @param ButtonInterface $cartAddToCartButton * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setAddToCartButton(ButtonInterface $cartAddToCartButton); @@ -40,7 +40,7 @@ public function setAddToCartButton(ButtonInterface $cartAddToCartButton); * Provide information needed for render "Add To Compare" button on front * * @return \Magento\Catalog\Api\Data\ProductRender\ButtonInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getAddToCompareButton(); @@ -49,7 +49,7 @@ public function getAddToCompareButton(); * * @param ButtonInterface $compareButton * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function setAddToCompareButton(ButtonInterface $compareButton); @@ -59,7 +59,7 @@ public function setAddToCompareButton(ButtonInterface $compareButton); * Prices are represented in raw format and in current currency * * @return \Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getPriceInfo(); @@ -68,7 +68,7 @@ public function getPriceInfo(); * * @param \Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface $priceInfo * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setPriceInfo(PriceInfoInterface $priceInfo); @@ -78,7 +78,7 @@ public function setPriceInfo(PriceInfoInterface $priceInfo); * Images can be separated by image codes * * @return \Magento\Catalog\Api\Data\ProductRender\ImageInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function getImages(); @@ -87,7 +87,7 @@ public function getImages(); * * @param \Magento\Catalog\Api\Data\ProductRender\ImageInterface[] $images * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setImages(array $images); @@ -95,7 +95,7 @@ public function setImages(array $images); * Provide product url * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getUrl(); @@ -104,7 +104,7 @@ public function getUrl(); * * @param string $url * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setUrl($url); @@ -112,7 +112,7 @@ public function setUrl($url); * Provide product identifier * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getId(); @@ -121,7 +121,7 @@ public function getId(); * * @param int $id * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setId($id); @@ -129,7 +129,7 @@ public function setId($id); * Provide product name * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getName(); @@ -138,7 +138,7 @@ public function getName(); * * @param string $name * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setName($name); @@ -146,7 +146,7 @@ public function setName($name); * Provide product type. Such as bundle, grouped, simple, etc... * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getType(); @@ -155,7 +155,7 @@ public function getType(); * * @param string $productType * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setType($productType); @@ -163,7 +163,7 @@ public function setType($productType); * Provide information about product saleability (In Stock) * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getIsSalable(); @@ -175,7 +175,7 @@ public function getIsSalable(); * * @param string $isSalable * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setIsSalable($isSalable); @@ -186,7 +186,7 @@ public function setIsSalable($isSalable); * This setting affect store scope attributes * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getStoreId(); @@ -195,7 +195,7 @@ public function getStoreId(); * * @param int $storeId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setStoreId($storeId); @@ -205,7 +205,7 @@ public function setStoreId($storeId); * This setting affect formatted prices* * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getCurrencyCode(); @@ -214,7 +214,7 @@ public function getCurrencyCode(); * * @param string $currencyCode * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setCurrencyCode($currencyCode); @@ -222,7 +222,7 @@ public function setCurrencyCode($currencyCode); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\ProductRenderExtensionInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -231,7 +231,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\ProductRenderExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRenderExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php b/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php index 62028ed788dd5..ec4feb5076238 100644 --- a/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php +++ b/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php @@ -9,7 +9,7 @@ /** * Product Special Price Interface is used to encapsulate data that can be processed by efficient price API. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface SpecialPriceInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -28,7 +28,7 @@ interface SpecialPriceInterface extends \Magento\Framework\Api\ExtensibleDataInt * * @param float $price * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPrice($price); @@ -36,7 +36,7 @@ public function setPrice($price); * Get product special price value. * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getPrice(); @@ -45,7 +45,7 @@ public function getPrice(); * * @param int $storeId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setStoreId($storeId); @@ -53,7 +53,7 @@ public function setStoreId($storeId); * Get ID of store, that contains special price value. * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getStoreId(); @@ -62,7 +62,7 @@ public function getStoreId(); * * @param string $sku * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setSku($sku); @@ -70,7 +70,7 @@ public function setSku($sku); * Get SKU of product, that contains special price value. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getSku(); @@ -79,7 +79,7 @@ public function getSku(); * * @param string $datetime * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPriceFrom($datetime); @@ -87,7 +87,7 @@ public function setPriceFrom($datetime); * Get start date for special price in Y-m-d H:i:s format. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getPriceFrom(); @@ -96,7 +96,7 @@ public function getPriceFrom(); * * @param string $datetime * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPriceTo($datetime); @@ -104,7 +104,7 @@ public function setPriceTo($datetime); * Get end date for special price in Y-m-d H:i:s format. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getPriceTo(); @@ -113,7 +113,7 @@ public function getPriceTo(); * If extension attributes do not exist return null. * * @return \Magento\Catalog\Api\Data\SpecialPriceExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -122,7 +122,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\SpecialPriceExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\SpecialPriceExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php b/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php index eaa1d22726d7c..dae43722bf42c 100644 --- a/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php +++ b/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php @@ -9,7 +9,7 @@ /** * Tier price interface. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface TierPriceInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -31,7 +31,7 @@ interface TierPriceInterface extends \Magento\Framework\Api\ExtensibleDataInterf * * @param float $price * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPrice($price); @@ -39,7 +39,7 @@ public function setPrice($price); * Get tier price. * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getPrice(); @@ -48,7 +48,7 @@ public function getPrice(); * * @param string $type * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPriceType($type); @@ -56,7 +56,7 @@ public function setPriceType($type); * Get tier price type. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getPriceType(); @@ -65,7 +65,7 @@ public function getPriceType(); * * @param int $websiteId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setWebsiteId($websiteId); @@ -73,7 +73,7 @@ public function setWebsiteId($websiteId); * Get website id. * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getWebsiteId(); @@ -82,7 +82,7 @@ public function getWebsiteId(); * * @param string $sku * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setSku($sku); @@ -90,7 +90,7 @@ public function setSku($sku); * Get SKU. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getSku(); @@ -99,7 +99,7 @@ public function getSku(); * * @param string $group * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setCustomerGroup($group); @@ -107,7 +107,7 @@ public function setCustomerGroup($group); * Get customer group. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getCustomerGroup(); @@ -116,7 +116,7 @@ public function getCustomerGroup(); * * @param float $quantity * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setQuantity($quantity); @@ -124,7 +124,7 @@ public function setQuantity($quantity); * Get quantity. * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getQuantity(); @@ -132,7 +132,7 @@ public function getQuantity(); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\TierPriceExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -141,7 +141,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\TierPriceExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\TierPriceExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/ProductAttributeOptionUpdateInterface.php b/app/code/Magento/Catalog/Api/ProductAttributeOptionUpdateInterface.php new file mode 100644 index 0000000000000..c783033b6d7b7 --- /dev/null +++ b/app/code/Magento/Catalog/Api/ProductAttributeOptionUpdateInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Api; + +/** + * Interface to update product attribute option + * + * @api + */ +interface ProductAttributeOptionUpdateInterface +{ + /** + * Update attribute option + * + * @param string $attributeCode + * @param int $optionId + * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @return bool + * @throws \Magento\Framework\Exception\StateException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\InputException + */ + public function update( + string $attributeCode, + int $optionId, + \Magento\Eav\Api\Data\AttributeOptionInterface $option + ): bool; +} diff --git a/app/code/Magento/Catalog/Api/ProductRenderListInterface.php b/app/code/Magento/Catalog/Api/ProductRenderListInterface.php index 954acd35a07db..ddd0f1406f68e 100644 --- a/app/code/Magento/Catalog/Api/ProductRenderListInterface.php +++ b/app/code/Magento/Catalog/Api/ProductRenderListInterface.php @@ -11,7 +11,7 @@ * Interface which provides product renders information for products. * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ProductRenderListInterface { @@ -26,7 +26,7 @@ interface ProductRenderListInterface * @param int $storeId * @param string $currencyCode * @return \Magento\Catalog\Api\Data\ProductRenderSearchResultsInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria, $storeId, $currencyCode); } diff --git a/app/code/Magento/Catalog/Api/ProductTierPriceManagementInterface.php b/app/code/Magento/Catalog/Api/ProductTierPriceManagementInterface.php index a6c3e975bfd79..dcb17a6f467f8 100644 --- a/app/code/Magento/Catalog/Api/ProductTierPriceManagementInterface.php +++ b/app/code/Magento/Catalog/Api/ProductTierPriceManagementInterface.php @@ -8,7 +8,7 @@ /** * @api - * @deprecated 101.1.0 use ScopedProductTierPriceManagementInterface instead + * @deprecated 102.0.0 use ScopedProductTierPriceManagementInterface instead * @since 100.0.2 */ interface ProductTierPriceManagementInterface diff --git a/app/code/Magento/Catalog/Api/ScopedProductTierPriceManagementInterface.php b/app/code/Magento/Catalog/Api/ScopedProductTierPriceManagementInterface.php index 1a3d05de5bcd1..2b005bc685b23 100644 --- a/app/code/Magento/Catalog/Api/ScopedProductTierPriceManagementInterface.php +++ b/app/code/Magento/Catalog/Api/ScopedProductTierPriceManagementInterface.php @@ -8,7 +8,7 @@ /** * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ScopedProductTierPriceManagementInterface { @@ -20,7 +20,7 @@ interface ScopedProductTierPriceManagementInterface * @return boolean * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\CouldNotSaveException - * @since 101.1.0 + * @since 102.0.0 */ public function add($sku, \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice); @@ -32,7 +32,7 @@ public function add($sku, \Magento\Catalog\Api\Data\ProductTierPriceInterface $t * @return boolean * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\CouldNotSaveException - * @since 101.1.0 + * @since 102.0.0 */ public function remove($sku, \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice); @@ -43,7 +43,7 @@ public function remove($sku, \Magento\Catalog\Api\Data\ProductTierPriceInterface * @param string $customerGroupId 'all' can be used to specify 'ALL GROUPS' * @return \Magento\Catalog\Api\Data\ProductTierPriceInterface[] * @throws \Magento\Framework\Exception\NoSuchEntityException - * @since 101.1.0 + * @since 102.0.0 */ public function getList($sku, $customerGroupId); } diff --git a/app/code/Magento/Catalog/Api/SpecialPriceInterface.php b/app/code/Magento/Catalog/Api/SpecialPriceInterface.php index 86dca59004132..543eab2263cbe 100644 --- a/app/code/Magento/Catalog/Api/SpecialPriceInterface.php +++ b/app/code/Magento/Catalog/Api/SpecialPriceInterface.php @@ -9,7 +9,7 @@ /** * Special prices resource model. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface SpecialPriceInterface { @@ -30,6 +30,7 @@ interface SpecialPriceInterface * 'price_to' => (string) Special price to date value in UTC. * ] * @since 101.1.0 + * @since 102.0.0 */ public function get(array $skus); @@ -47,7 +48,7 @@ public function get(array $skus); * ]; * @return bool * @throws \Magento\Framework\Exception\CouldNotSaveException Thrown if error occurred during price save. - * @since 101.1.0 + * @since 102.0.0 */ public function update(array $prices); @@ -65,7 +66,7 @@ public function update(array $prices); * ]; * @return bool * @throws \Magento\Framework\Exception\CouldNotDeleteException Thrown if error occurred during price delete. - * @since 101.1.0 + * @since 102.0.0 */ public function delete(array $prices); } diff --git a/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php b/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php index 2442af103a4e9..6c2d89b51278c 100644 --- a/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php +++ b/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php @@ -9,7 +9,7 @@ /** * Special price storage presents efficient price API and is used to retrieve, update or delete special prices. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface SpecialPriceStorageInterface { @@ -19,7 +19,7 @@ interface SpecialPriceStorageInterface * @param string[] $skus * @return \Magento\Catalog\Api\Data\SpecialPriceInterface[] * @throws \Magento\Framework\Exception\NoSuchEntityException - * @since 101.1.0 + * @since 102.0.0 */ public function get(array $skus); @@ -33,7 +33,7 @@ public function get(array $skus); * @param \Magento\Catalog\Api\Data\SpecialPriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] * @throws \Magento\Framework\Exception\CouldNotSaveException - * @since 101.1.0 + * @since 102.0.0 */ public function update(array $prices); @@ -47,7 +47,7 @@ public function update(array $prices); * @param \Magento\Catalog\Api\Data\SpecialPriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] * @throws \Magento\Framework\Exception\CouldNotDeleteException - * @since 101.1.0 + * @since 102.0.0 */ public function delete(array $prices); } diff --git a/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php b/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php index 584daa9864588..b9102fcfc075c 100644 --- a/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php +++ b/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php @@ -9,7 +9,7 @@ /** * Tier prices storage. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface TierPriceStorageInterface { @@ -19,7 +19,7 @@ interface TierPriceStorageInterface * @param string[] $skus * @return \Magento\Catalog\Api\Data\TierPriceInterface[] * @throws \Magento\Framework\Exception\NoSuchEntityException - * @since 101.1.0 + * @since 102.0.0 */ public function get(array $skus); @@ -33,7 +33,7 @@ public function get(array $skus); * * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function update(array $prices); @@ -47,7 +47,7 @@ public function update(array $prices); * * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function replace(array $prices); @@ -61,7 +61,7 @@ public function replace(array $prices); * * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function delete(array $prices); } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php index 3266922d116ec..acffce3ca0b8c 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php @@ -11,11 +11,45 @@ */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\Escaper; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Pricestep Helper */ class Pricestep extends \Magento\Framework\Data\Form\Element\Text { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param Escaper $escaper + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + Escaper $escaper, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + parent::__construct( + $factoryElement, + $factoryCollection, + $escaper, + $data + ); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Returns js code that is used instead of default toggle code for "Use default config" checkbox * @@ -53,18 +87,23 @@ public function getElementHtml() $html .= ' disabled="disabled"'; } - $html .= ' onclick="toggleValueElements(this, this.parentNode);" class="checkbox" type="checkbox" />'; + $html .= ' class="checkbox" type="checkbox" />'; $html .= ' <label for="' . $htmlId . '" class="normal">' . __('Use Config Settings') . '</label>'; - $html .= '<script>' . + $scriptString = 'require(["prototype"], function(){'. 'toggleValueElements($(\'' . $htmlId . '\'), $(\'' . $htmlId . '\').parentNode);' . - '});'. - '</script>'; + '});'; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + '#' . $htmlId + ); return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php index 2a88d0f4d4f15..b0f00d0f2b04b 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php @@ -11,8 +11,41 @@ */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper\Sortby; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Available extends \Magento\Framework\Data\Form\Element\Multiselect { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param Escaper $escaper + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + Escaper $escaper, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer, $random); + $this->secureRenderer = $secureRenderer; + } + /** * Returns js code that is used instead of default toggle code for "Use default config" checkbox * @@ -49,13 +82,19 @@ public function getElementHtml() $html .= ' disabled="disabled"'; } - $html .= ' onclick="toggleValueElements(this, this.parentNode);" class="checkbox" type="checkbox" />'; + $html .= ' class="checkbox" type="checkbox" />'; $html .= ' <label for="' . $htmlId . '" class="normal">' . __('Use All Available Attributes') . '</label>'; - $html .= '<script>require(["prototype"], function(){toggleValueElements($(\'' . + $scriptString = 'require(["prototype"], function(){toggleValueElements($(\'' . $htmlId . '\'), $(\'' . $htmlId . - '\').parentNode);});</script>'; + '\').parentNode);});'; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + '#' . $htmlId + ); return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php index 2d887d05f62ad..e0836a0d7cb25 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php @@ -11,8 +11,41 @@ */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper\Sortby; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class DefaultSortby extends \Magento\Framework\Data\Form\Element\Select { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param Escaper $escaper + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + Escaper $escaper, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer, $random); + $this->secureRenderer = $secureRenderer; + } + /** * Returns js code that is used instead of default toggle code for "Use default config" checkbox * @@ -49,13 +82,19 @@ public function getElementHtml() $html .= ' disabled="disabled"'; } - $html .= ' onclick="toggleValueElements(this, this.parentNode);" class="checkbox" type="checkbox" />'; + $html .= ' class="checkbox" type="checkbox" />'; $html .= ' <label for="' . $htmlId . '" class="normal">' . __('Use Config Settings') . '</label>'; - $html .= '<script>require(["prototype"], function(){toggleValueElements($(\'' . + $scriptString = 'require(["prototype"], function(){toggleValueElements($(\'' . $htmlId . '\'), $(\'' . $htmlId . - '\').parentNode);});</script>'; + '\').parentNode);});'; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + '#' . $htmlId + ); return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php index 929c181bf820c..a66dcece2bef0 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php @@ -10,16 +10,18 @@ namespace Magento\Catalog\Block\Adminhtml\Category; use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Tree\Node; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use Magento\Store\Model\Store; /** - * Class Tree + * Class Category Tree * * @api - * @package Magento\Catalog\Block\Adminhtml\Category * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @since 100.0.2 */ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory @@ -44,6 +46,11 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory */ protected $_jsonEncoder; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Catalog\Model\ResourceModel\Category\Tree $categoryTree @@ -53,6 +60,7 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory * @param \Magento\Framework\DB\Helper $resourceHelper * @param \Magento\Backend\Model\Auth\Session $backendSession * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -62,12 +70,14 @@ public function __construct( \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Framework\DB\Helper $resourceHelper, \Magento\Backend\Model\Auth\Session $backendSession, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_resourceHelper = $resourceHelper; $this->_backendSession = $backendSession; parent::__construct($context, $categoryTree, $registry, $categoryFactory, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -336,12 +346,14 @@ public function getBreadcrumbsJavascript($path, $javascriptVarName) foreach ($categories as $key => $category) { $categories[$key] = $this->_getNodeJson($category); } - return '<script>require(["prototype"], function(){' . $javascriptVarName . ' = ' . $this->_jsonEncoder->encode( + $scriptString = 'require(["prototype"], function(){' . $javascriptVarName . ' = ' . $this->_jsonEncoder->encode( $categories ) . ';' . ($this->canAddSubCategory() ? '$("add_subcategory_button").show();' : '$("add_subcategory_button").hide();') - . '});</script>'; + . '});'; + + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } /** diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php index c58ed58370e3a..48753bfd6efb4 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php @@ -11,8 +11,13 @@ */ namespace Magento\Catalog\Block\Adminhtml\Helper\Form; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Wysiwyg helper. + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea { @@ -40,6 +45,11 @@ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea */ protected $_layout; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + /** * @param \Magento\Framework\Data\Form\Element\Factory $factoryElement * @param \Magento\Framework\Data\Form\Element\CollectionFactory $factoryCollection @@ -49,6 +59,7 @@ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Backend\Helper\Data $backendData * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Framework\Data\Form\Element\Factory $factoryElement, @@ -58,13 +69,15 @@ public function __construct( \Magento\Framework\View\LayoutInterface $layout, \Magento\Framework\Module\Manager $moduleManager, \Magento\Backend\Helper\Data $backendData, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_wysiwygConfig = $wysiwygConfig; $this->_layout = $layout; $this->_moduleManager = $moduleManager; $this->_backendData = $backendData; parent::__construct($factoryElement, $factoryCollection, $escaper, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -95,8 +108,7 @@ public function getAfterElementHtml() ] ] )->toHtml(); - $html .= <<<HTML -<script> + $scriptString = <<<HTML require([ 'jquery', 'mage/adminhtml/wysiwyg/tiny_mce/setup' @@ -119,9 +131,10 @@ public function getAfterElementHtml() editor ); }); -</script> HTML; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } + return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php index e6afc41ebebac..822580801c4e4 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php @@ -12,10 +12,12 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\Json\Helper\Data as JsonHelper; /** - * Class Edit + * Class for Product Edit. */ class Edit extends \Magento\Backend\Block\Widget { @@ -59,6 +61,7 @@ class Edit extends \Magento\Backend\Block\Widget * @param \Magento\Catalog\Helper\Product $productHelper * @param Escaper $escaper * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -67,13 +70,15 @@ public function __construct( \Magento\Framework\Registry $registry, \Magento\Catalog\Helper\Product $productHelper, Escaper $escaper, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->_productHelper = $productHelper; $this->_attributeSetFactory = $attributeSetFactory; $this->_coreRegistry = $registry; $this->jsonEncoder = $jsonEncoder; $this->escaper = $escaper; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } @@ -288,7 +293,7 @@ public function getDuplicateUrl() /** * Retrieve product header * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return string */ public function getHeader() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php index 1ebfa14200364..287a24e0aaca0 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php @@ -22,9 +22,11 @@ use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Weight; use Magento\Catalog\Helper\Product\Edit\Action\Attribute; use Magento\Catalog\Model\ProductFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Framework\Data\FormFactory; use Magento\Framework\Registry; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Attributes tab block @@ -51,6 +53,11 @@ class Attributes extends Form implements TabInterface */ private $excludeFields; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param Context $context * @param Registry $registry @@ -59,6 +66,7 @@ class Attributes extends Form implements TabInterface * @param Attribute $attributeAction * @param array $data * @param array|null $excludeFields + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( Context $context, @@ -67,13 +75,15 @@ public function __construct( ProductFactory $productFactory, Attribute $attributeAction, array $data = [], - array $excludeFields = null + array $excludeFields = null, + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_attributeAction = $attributeAction; $this->_productFactory = $productFactory; $this->excludeFields = $excludeFields ?: []; parent::__construct($context, $registry, $formFactory, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -146,13 +156,20 @@ protected function _getAdditionalElementHtml($element) // @codingStandardsIgnoreStart $html = <<<HTML <span class="attribute-change-checkbox"> - <input type="checkbox" id="$dataCheckboxName" name="$dataCheckboxName" class="checkbox" $nameAttributeHtml onclick="toogleFieldEditMode(this, '{$elementId}')" $dataAttribute /> + <input type="checkbox" id="$dataCheckboxName" name="$dataCheckboxName" + class="checkbox" $nameAttributeHtml $dataAttribute /> <label class="label" for="$dataCheckboxName"> {$checkboxLabel} </label> </span> HTML; + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toogleFieldEditMode(this, '{$elementId}')", + "#". $dataCheckboxName + ); + // @codingStandardsIgnoreEnd return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php index d95ee7f8f2cf9..6419ae2d70588 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; + /** * Admin AttributeSet block */ @@ -27,13 +30,16 @@ class AttributeSet extends \Magento\Backend\Block\Widget\Form * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->_coreRegistry = $registry; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Js.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Js.php index 9712c0e03d609..2620fd345c667 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Js.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Js.php @@ -9,6 +9,8 @@ use Magento\Customer\Helper\Session\CurrentCustomer; use Magento\Tax\Api\TaxCalculationInterface; use Magento\Tax\Model\TaxClass\Source\Product as ProductTaxClassSource; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; class Js extends \Magento\Backend\Block\Template { @@ -51,6 +53,7 @@ class Js extends \Magento\Backend\Block\Template * @param TaxCalculationInterface $calculationService * @param ProductTaxClassSource $productTaxClassSource * @param array $data + * @param TaxHelper|null $taxHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -59,13 +62,15 @@ public function __construct( \Magento\Framework\Json\Helper\Data $jsonHelper, TaxCalculationInterface $calculationService, ProductTaxClassSource $productTaxClassSource, - array $data = [] + array $data = [], + ?TaxHelper $taxHelper = null ) { $this->coreRegistry = $registry; $this->currentCustomer = $currentCustomer; $this->jsonHelper = $jsonHelper; $this->calculationService = $calculationService; $this->productTaxClassSource = $productTaxClassSource; + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/NewCategory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/NewCategory.php index 0a766bc4c0cb3..ebc269f85d054 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/NewCategory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/NewCategory.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * @SuppressWarnings(PHPMD.DepthOfInheritance) */ @@ -26,13 +29,19 @@ class NewCategory extends \Magento\Backend\Block\Widget\Form\Generic */ protected $_categoryFactory; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Data\FormFactory $formFactory + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Catalog\Model\CategoryFactory $categoryFactory * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -40,12 +49,14 @@ public function __construct( \Magento\Framework\Data\FormFactory $formFactory, \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Catalog\Model\CategoryFactory $categoryFactory, - array $data = [] + array $data = [], + SecureHtmlRenderer $htmlRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_categoryFactory = $categoryFactory; parent::__construct($context, $registry, $formFactory, $data); $this->setUseContainer(true); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -153,14 +164,13 @@ public function getAfterElementHtml() ] ); //TODO: JavaScript logic should be moved to separate file or reviewed - return <<<HTML -<script> + $scriptString = <<<HTML require(["jquery","mage/mage"],function($) { // waiting for dependencies at first $(function(){ // waiting for page to load to have '#category_ids-template' available - $('#new-category').mage('newCategoryDialog', $widgetOptions); + $('#new-category').mage('newCategoryDialog', {$widgetOptions}); }); }); -</script> HTML; + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Ajax/Serializer.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Ajax/Serializer.php index 3d131a6e08810..2962e63313cdc 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Ajax/Serializer.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Ajax/Serializer.php @@ -10,7 +10,7 @@ /** * Class Serializer * @package Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Ajax - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ class Serializer extends \Magento\Framework\View\Element\Template { @@ -47,7 +47,7 @@ public function _construct() /** * @return string - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ public function getProductsJSON() { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php index 42463354926dd..702c77e3a5595 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Attributes; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; + /** * Admin product attribute search block */ @@ -39,17 +42,20 @@ class Search extends \Magento\Backend\Block\Widget * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $collectionFactory * @param \Magento\Framework\Registry $registry * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\DB\Helper $resourceHelper, \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $collectionFactory, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->_resourceHelper = $resourceHelper; $this->_collectionFactory = $collectionFactory; $this->_coreRegistry = $registry; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php index e5ce59c550af1..40e7136da5bf6 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php @@ -15,7 +15,7 @@ * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 - * @deprecated Not used since cross-sell products grid moved to UI components. + * @deprecated 103.0.1 Not used since cross-sell products grid moved to UI components. * @see \Magento\Catalog\Ui\DataProvider\Product\Related\CrossSellDataProvider */ class Crosssell extends Extended diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php index ccf207938ab06..0a9434768737d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php @@ -14,6 +14,9 @@ use Magento\Backend\Block\Widget; use Magento\Catalog\Model\Product; use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use Magento\Store\Model\Store; /** @@ -72,6 +75,11 @@ class Option extends Widget */ protected $_optionType; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Config\Model\Config\Source\Yesno $configYesNo @@ -80,6 +88,8 @@ class Option extends Widget * @param \Magento\Framework\Registry $registry * @param \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig * @param array $data + * @param JsonHelper|null $jsonHelper + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -88,14 +98,18 @@ public function __construct( Product $product, \Magento\Framework\Registry $registry, \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null, + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_optionType = $optionType; $this->_configYesNo = $configYesNo; $this->_product = $product; $this->_productOptionConfig = $productOptionConfig; $this->_coreRegistry = $registry; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -460,8 +474,12 @@ public function getCheckboxScopeHtml($id, $name, $checked = true, $select_id = ' . ' name="' . $localName . '"' . 'id="' . $localId . '"' . ' value=""' . $checkedHtml - . ' onchange="toggleSeveralValueElements(this, [' . $containers . ']);" ' . ' />' + . $this->secureRenderer->renderEventListenerAsTag( + 'onchange', + "toggleSeveralValueElements(this, [' . $containers . ']);", + '#' . $localId + ) . '<label for="' . $localId . '" class="use-default">' . '<span class="use-default-label">' . __('Use Default') . '</span></label></div>'; @@ -482,6 +500,8 @@ public function getPriceValue($value, $type) } elseif ($type == 'fixed') { return number_format((float)$value, 2, null, ''); } + + return ''; } /** diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php index 7cb1c2c9e4263..00cd020e4f525 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php @@ -5,8 +5,15 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Price; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; + /** * Adminhtml tier price item renderer + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ class Tier extends Group\AbstractGroup { @@ -15,6 +22,44 @@ class Tier extends Group\AbstractGroup */ protected $_template = 'Magento_Catalog::catalog/product/edit/price/tier.phtml'; + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param GroupRepositoryInterface $groupRepository + * @param \Magento\Directory\Helper\Data $directoryHelper + * @param \Magento\Framework\Module\Manager $moduleManager + * @param \Magento\Framework\Registry $registry + * @param GroupManagementInterface $groupManagement + * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder + * @param \Magento\Framework\Locale\CurrencyInterface $localeCurrency + * @param array $data + * @param JsonHelper|null $jsonHelper + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + GroupRepositoryInterface $groupRepository, + \Magento\Directory\Helper\Data $directoryHelper, + \Magento\Framework\Module\Manager $moduleManager, + \Magento\Framework\Registry $registry, + GroupManagementInterface $groupManagement, + \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, + \Magento\Framework\Locale\CurrencyInterface $localeCurrency, + array $data = [], + ?JsonHelper $jsonHelper = null + ) { + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); + parent::__construct( + $context, + $groupRepository, + $directoryHelper, + $moduleManager, + $registry, + $groupManagement, + $searchCriteriaBuilder, + $localeCurrency, + $data + ); + } + /** * Retrieve list of initial customer groups * @@ -62,6 +107,7 @@ protected function _sortTierPrices($a, $b) /** * Prepare global layout + * * Add "Add tier" button to layout * * @return $this diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php index 23b927598e8e7..c73ffe5764dfb 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php @@ -13,7 +13,7 @@ * * @api * @since 100.0.2 - * @deprecated Not used since related products grid moved to UI components. + * @deprecated 103.0.1 Not used since related products grid moved to UI components. * @see \Magento\Catalog\Ui\DataProvider\Product\Related\RelatedDataProvider */ class Related extends Extended diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php index 41ad72ca39e53..d196f82f8b48d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php @@ -10,7 +10,7 @@ * * @api * @since 100.0.2 - * @deprecated Not used since upsell products grid moved to UI components. + * @deprecated 103.0.1 Not used since upsell products grid moved to UI components. * @see \Magento\Catalog\Ui\DataProvider\Product\Related\CrossSellDataProvider */ class Upsell extends \Magento\Backend\Block\Widget\Grid\Extended diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Apply.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Apply.php index 86ed3d09d5728..101daec2b4906 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Apply.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Apply.php @@ -11,9 +11,18 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Apply extends \Magento\Framework\Data\Form\Element\Multiselect { /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * Return html of the element. + * * @return string */ public function getElementHtml() @@ -28,12 +37,19 @@ public function getElementHtml() $elementAttributeHtml = $elementAttributeHtml . ' disabled="disabled"'; } - $html = '<select onchange="toggleApplyVisibility(this)"' . $elementAttributeHtml . '>' + $html = '<select id="' . $this->getHtmlId() . '"' . $elementAttributeHtml . '>' . '<option value="0">' . $this->getModeLabels('all') . '</option>' . '<option value="1" ' . ($this->getValue() == null ? '' : 'selected') . '>' . $this->getModeLabels('custom') . '</option>' . '</select><br /><br />'; + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onchange', + "toggleApplyVisibility(this)", + 'select#' . $this->getHtmlId() + ); + $html .= parent::getElementHtml(); + return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php index df372312613f4..698bb12022bc6 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php @@ -7,7 +7,9 @@ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\App\ObjectManager; use Magento\Framework\AuthorizationInterface; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Product form category field helper @@ -41,6 +43,11 @@ class Category extends \Magento\Framework\Data\Form\Element\Multiselect */ protected $authorization; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Framework\Data\Form\Element\Factory $factoryElement * @param \Magento\Framework\Data\Form\Element\CollectionFactory $factoryCollection @@ -51,6 +58,8 @@ class Category extends \Magento\Framework\Data\Form\Element\Multiselect * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param AuthorizationInterface $authorization * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Data\Form\Element\Factory $factoryElement, @@ -61,7 +70,8 @@ public function __construct( \Magento\Framework\View\LayoutInterface $layout, \Magento\Framework\Json\EncoderInterface $jsonEncoder, AuthorizationInterface $authorization, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_collectionFactory = $collectionFactory; @@ -69,6 +79,7 @@ public function __construct( $this->authorization = $authorization; parent::__construct($factoryElement, $factoryCollection, $escaper, $data); $this->_layout = $layout; + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); if (!$this->isAllowed()) { $this->setType('hidden'); $this->addClass('hidden'); @@ -136,13 +147,15 @@ public function getAfterElementHtml() ); $return = <<<HTML <input id="{$htmlId}-suggest" placeholder="$suggestPlaceholder" /> - <script> +HTML; + $scriptString = <<<script require(["jquery", "mage/mage"], function($){ $('#{$htmlId}-suggest').mage('treeSuggest', {$selectorOptions}); }); - </script> -HTML; - return $return . $button->toHtml(); +script; + + return $return . $button->toHtml() . + /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } /** diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Config.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Config.php index 0c82ac537689f..16c00aee6beaa 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Config.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Config.php @@ -11,8 +11,38 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\Escaper; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Config extends \Magento\Framework\Data\Form\Element\Select { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param Escaper $escaper + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + Escaper $escaper, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer); + $this->secureRenderer = $secureRenderer; + } + /** * Retrieve element html * @@ -31,13 +61,19 @@ public function getElementHtml() $disabled = $this->getReadonly() ? ' disabled="disabled"' : ''; $html .= '<input id="' . $htmlId . '" name="product[' . $htmlId . ']" ' . $disabled . ' value="1" ' . $checked; - $html .= ' onclick="toggleValueElements(this, this.parentNode);" class="checkbox" type="checkbox" />'; + $html .= ' class="checkbox" type="checkbox" />'; $html .= ' <label for="' . $htmlId . '">' . __('Use Config Settings') . '</label>'; - $html .= '<script>require(["prototype"], function(){toggleValueElements($(\'' . + $scriptString = 'require(["prototype"], function(){toggleValueElements($(\'' . $htmlId . '\'), $(\'' . $htmlId . - '\').parentNode);});</script>'; + '\').parentNode);});'; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements($('#' . $htmlId), $('#' . $htmlId).parentNode);", + '#' . $htmlId + ); return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php index 8e6011c09a27f..57cea59bee207 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -15,6 +15,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Backend\Block\Media\Uploader; +use Magento\Framework\Json\Helper\Data as JsonHelper; use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; @@ -23,6 +24,8 @@ /** * Block for gallery content. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Content extends \Magento\Backend\Block\Widget { @@ -63,6 +66,7 @@ class Content extends \Magento\Backend\Block\Widget * @param array $data * @param ImageUploadConfigDataProvider $imageUploadConfigDataProvider * @param Database $fileStorageDatabase + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -70,10 +74,12 @@ public function __construct( \Magento\Catalog\Model\Product\Media\Config $mediaConfig, array $data = [], ImageUploadConfigDataProvider $imageUploadConfigDataProvider = null, - Database $fileStorageDatabase = null + Database $fileStorageDatabase = null, + ?JsonHelper $jsonHelper = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_mediaConfig = $mediaConfig; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); $this->imageUploadConfigDataProvider = $imageUploadConfigDataProvider ?: ObjectManager::getInstance()->get(ImageUploadConfigDataProvider::class); diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Image.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Image.php index 1cc6a5b56bf2c..57e82581d83b2 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Image.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Image.php @@ -11,9 +11,43 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Image extends \Magento\Framework\Data\Form\Element\Image { /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param \Magento\Framework\Escaper $escaper + * @param UrlInterface $urlBuilder + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + \Magento\Framework\Escaper $escaper, + UrlInterface $urlBuilder, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $urlBuilder, $data, $secureRenderer); + $this->secureRenderer = $secureRenderer; + } + + /** + * Return generated url. + * * @return bool|string */ protected function _getUrl() @@ -24,10 +58,13 @@ protected function _getUrl() ['_type' => \Magento\Framework\UrlInterface::URL_TYPE_MEDIA] ) . 'catalog/product/' . $this->getValue(); } + return $url; } /** + * Return generated delete checkbox. + * * @return string */ protected function _getDeleteCheckbox() @@ -39,18 +76,19 @@ protected function _getDeleteCheckbox() } else { $inputField = '<input value="%s" id="%s_hidden" type="hidden" class="required-entry" />'; $html .= sprintf($inputField, $this->getValue(), $this->getHtmlId()); - $html .= '<script>require(["prototype"], function(){ + $scriptString = 'require(["prototype"], function(){ syncOnchangeValue(\'' . $this->getHtmlId() . '\', \'' . $this->getHtmlId() . '_hidden\'); - }); - </script>'; + });'; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } } else { $html .= parent::_getDeleteCheckbox(); } + return $html; } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Weight.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Weight.php index 70b2948501d2d..7fe51a7327d64 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Weight.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Weight.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Directory\Helper\Data; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form; use Magento\Catalog\Model\Product\Edit\WeightResolver; use Magento\Framework\Data\Form\Element\CollectionFactory; @@ -17,6 +18,7 @@ use Magento\Framework\Data\Form\Element\Text; use Magento\Framework\Escaper; use Magento\Framework\Locale\Format; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Product form weight field helper @@ -40,6 +42,11 @@ class Weight extends Text */ protected $directoryHelper; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param Factory $factoryElement * @param CollectionFactory $factoryCollection @@ -47,6 +54,7 @@ class Weight extends Text * @param Format $localeFormat * @param Data $directoryHelper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( Factory $factoryElement, @@ -54,7 +62,8 @@ public function __construct( Escaper $escaper, Format $localeFormat, Data $directoryHelper, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->directoryHelper = $directoryHelper; $this->localeFormat = $localeFormat; @@ -75,6 +84,7 @@ public function __construct( ); parent::__construct($factoryElement, $factoryCollection, $escaper, $data); $this->addClass('validate-zero-or-greater'); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -199,8 +209,7 @@ private function getHtmlForWeightSwitcher() $checkboxLabel = __('Change'); $html .= <<<HTML <span class="attribute-change-checkbox"> - <input type="checkbox" id="$dataCheckboxName" name="$dataCheckboxName" class="checkbox" $nameAttributeHtml - onclick="toogleFieldEditMode(this, 'weight-switcher1'); toogleFieldEditMode(this, 'weight-switcher0');" /> + <input type="checkbox" id="$dataCheckboxName" name="$dataCheckboxName" class="checkbox" $nameAttributeHtml/> <label class="label" for="$dataCheckboxName"> {$checkboxLabel} </label> @@ -209,6 +218,12 @@ private function getHtmlForWeightSwitcher() $html .= '</label></div></div>'; + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toogleFieldEditMode(this, 'weight-switcher1'); toogleFieldEditMode(this, 'weight-switcher0');", + "#". $dataCheckboxName + ); + return $html; } } diff --git a/app/code/Magento/Catalog/Block/FrontendStorageManager.php b/app/code/Magento/Catalog/Block/FrontendStorageManager.php index 0c826b95cbb49..112058baf4e05 100644 --- a/app/code/Magento/Catalog/Block/FrontendStorageManager.php +++ b/app/code/Magento/Catalog/Block/FrontendStorageManager.php @@ -15,7 +15,7 @@ * Provide information to frontend storage manager * * @api - * @since 101.1.0 + * @since 102.0.0 */ class FrontendStorageManager extends \Magento\Framework\View\Element\Template { @@ -51,7 +51,7 @@ public function __construct( * in json format * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getConfigurationJson() { diff --git a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php index c8da0f70f73b6..26af19fb85bcb 100644 --- a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php +++ b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php @@ -8,7 +8,7 @@ /** * Class AbstractProduct * @api - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 @@ -99,7 +99,7 @@ class AbstractProduct extends \Magento\Framework\View\Element\Template /** * @var ImageBuilder - * @since 101.1.0 + * @since 102.0.0 */ protected $imageBuilder; diff --git a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php index 76f5dbd1bea88..15e8d41809631 100644 --- a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php +++ b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php @@ -149,7 +149,7 @@ public function getItems() $this->_compareProduct->setAllowUsedFlat(false); $this->_items = $this->_itemCollectionFactory->create(); - $this->_items->useProductItem(true)->setStoreId($this->_storeManager->getStore()->getId()); + $this->_items->useProductItem()->setStoreId($this->_storeManager->getStore()->getId()); if ($this->httpContext->getValue(Context::CONTEXT_AUTH)) { $this->_items->setCustomerId($this->currentCustomer->getCustomerId()); @@ -213,6 +213,7 @@ public function getProductAttributeValue($product, $attribute) * * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute * @return bool + * @since 102.0.6 */ public function hasAttributeValueForProducts($attribute) { diff --git a/app/code/Magento/Catalog/Block/Product/Context.php b/app/code/Magento/Catalog/Block/Product/Context.php index db18eb2bc8a7d..36a3214cab079 100644 --- a/app/code/Magento/Catalog/Block/Product/Context.php +++ b/app/code/Magento/Catalog/Block/Product/Context.php @@ -18,7 +18,7 @@ * As Magento moves from inheritance-based APIs all such classes will be deprecated together with * the classes they were introduced for. * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @SuppressWarnings(PHPMD) */ class Context extends \Magento\Framework\View\Element\Template\Context diff --git a/app/code/Magento/Catalog/Block/Product/Image.php b/app/code/Magento/Catalog/Block/Product/Image.php index ccc37029bedf7..8fc7ba483d980 100644 --- a/app/code/Magento/Catalog/Block/Product/Image.php +++ b/app/code/Magento/Catalog/Block/Product/Image.php @@ -21,19 +21,19 @@ class Image extends \Magento\Framework\View\Element\Template { /** - * @deprecated Property isn't used + * @deprecated 102.0.5 Property isn't used * @var \Magento\Catalog\Helper\Image */ protected $imageHelper; /** - * @deprecated Property isn't used + * @deprecated 102.0.5 Property isn't used * @var \Magento\Catalog\Model\Product */ protected $product; /** - * @deprecated Property isn't used + * @deprecated 102.0.5 Property isn't used * @var array */ protected $attributes = []; diff --git a/app/code/Magento/Catalog/Block/Product/ImageBuilder.php b/app/code/Magento/Catalog/Block/Product/ImageBuilder.php index 06d4fb39109d8..702410a530ea4 100644 --- a/app/code/Magento/Catalog/Block/Product/ImageBuilder.php +++ b/app/code/Magento/Catalog/Block/Product/ImageBuilder.php @@ -11,7 +11,7 @@ use Magento\Catalog\Model\Product; /** - * @deprecated + * @deprecated 103.0.0 * @see ImageFactory */ class ImageBuilder diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php index 76fcdfbf232e5..6cec9bf3ef88a 100644 --- a/app/code/Magento/Catalog/Block/Product/ListProduct.php +++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php @@ -23,6 +23,8 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Pricing\Render; use Magento\Framework\Url\Helper\Data; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Helper\Output as OutputHelper; /** * Product list @@ -75,6 +77,7 @@ class ListProduct extends AbstractProduct implements IdentityInterface * @param CategoryRepositoryInterface $categoryRepository * @param Data $urlHelper * @param array $data + * @param OutputHelper|null $outputHelper */ public function __construct( Context $context, @@ -82,12 +85,14 @@ public function __construct( Resolver $layerResolver, CategoryRepositoryInterface $categoryRepository, Data $urlHelper, - array $data = [] + array $data = [], + ?OutputHelper $outputHelper = null ) { $this->_catalogLayer = $layerResolver->get(); $this->_postDataHelper = $postDataHelper; $this->categoryRepository = $categoryRepository; $this->urlHelper = $urlHelper; + $data['outputHelper'] = $outputHelper ?? ObjectManager::getInstance()->get(OutputHelper::class); parent::__construct( $context, $data @@ -353,18 +358,16 @@ public function getIdentities() $category = $this->getLayer()->getCurrentCategory(); if ($category) { - $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $category->getId(); + $identities[] = [Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $category->getId()]; } //Check if category page shows only static block (No products) - if ($category->getData('display_mode') == Category::DM_PAGE) { - return $identities; - } - - foreach ($this->_getProductCollection() as $item) { - // phpcs:ignore Magento2.Performance.ForeachArrayMerge - $identities = array_merge($identities, $item->getIdentities()); + if ($category->getData('display_mode') != Category::DM_PAGE) { + foreach ($this->_getProductCollection() as $item) { + $identities[] = $item->getIdentities(); + } } + $identities = array_merge(...$identities); return $identities; } @@ -377,7 +380,7 @@ public function getIdentities() */ public function getAddToCartPostParams(Product $product) { - $url = $this->getAddToCartUrl($product); + $url = $this->getAddToCartUrl($product, ['_escape' => false]); return [ 'action' => $url, 'data' => [ diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php index 48725331b27da..5b5a7cd2d342a 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php @@ -79,7 +79,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template /** * @var bool $_paramsMemorizeAllowed - * @deprecated + * @deprecated 103.0.1 */ protected $_paramsMemorizeAllowed = true; @@ -99,7 +99,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template * Catalog session * * @var \Magento\Catalog\Model\Session - * @deprecated + * @deprecated 103.0.1 */ protected $_catalogSession; @@ -188,7 +188,7 @@ public function __construct( * Disable list state params memorizing * * @return $this - * @deprecated + * @deprecated 103.0.1 */ public function disableParamsMemorizing() { @@ -202,7 +202,7 @@ public function disableParamsMemorizing() * @param string $param parameter name * @param mixed $value parameter value * @return $this - * @deprecated + * @deprecated 103.0.1 */ protected function _memorizeParam($param, $value) { diff --git a/app/code/Magento/Catalog/Block/Product/View.php b/app/code/Magento/Catalog/Block/Product/View.php index 437171bcb4bc6..6cc5652352154 100644 --- a/app/code/Magento/Catalog/Block/Product/View.php +++ b/app/code/Magento/Catalog/Block/Product/View.php @@ -30,7 +30,7 @@ class View extends AbstractProduct implements \Magento\Framework\DataObject\Iden /** * @var \Magento\Framework\Pricing\PriceCurrencyInterface - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ protected $priceCurrency; @@ -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/Block/Product/View/AbstractView.php b/app/code/Magento/Catalog/Block/Product/View/AbstractView.php index ec16bc1d2334f..9c569725d98de 100644 --- a/app/code/Magento/Catalog/Block/Product/View/AbstractView.php +++ b/app/code/Magento/Catalog/Block/Product/View/AbstractView.php @@ -9,7 +9,7 @@ * Product view abstract block * * @api - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @since 100.0.2 */ abstract class AbstractView extends \Magento\Catalog\Block\Product\AbstractProduct diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index 5b9777cbfd1e7..9fd840b264085 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -110,6 +110,7 @@ public function getAdditionalData(array $excludeAttr = []) * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute * @param array $excludeAttr * @return bool + * @since 103.0.0 */ protected function isVisibleOnFrontend( \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute, diff --git a/app/code/Magento/Catalog/Block/Product/View/Details.php b/app/code/Magento/Catalog/Block/Product/View/Details.php index 38925e9ae3cd7..67303d177e71e 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Details.php +++ b/app/code/Magento/Catalog/Block/Product/View/Details.php @@ -14,6 +14,7 @@ * Holds a group of blocks to show as tabs. * * @api + * @since 103.0.1 */ class Details extends \Magento\Framework\View\Element\Template { @@ -25,6 +26,7 @@ class Details extends \Magento\Framework\View\Element\Template * @throws \Magento\Framework\Exception\LocalizedException * * @return array + * @since 103.0.1 */ public function getGroupSortedChildNames(string $groupName, string $callback): array { diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php index 0bfdcc678e9f7..1dcbf60db15c3 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -3,14 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); + +/** + * Product options abstract type block + * + * @author Magento Core Team <core@magentocommerce.com> + */ namespace Magento\Catalog\Block\Product\View\Options; -use Magento\Catalog\Pricing\Price\BasePrice; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; use Magento\Catalog\Pricing\Price\CustomOptionPriceInterface; -use Magento\Framework\App\ObjectManager; /** * Product options section abstract block. @@ -45,29 +47,20 @@ abstract class AbstractOptions extends \Magento\Framework\View\Element\Template */ protected $_catalogHelper; - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper * @param \Magento\Catalog\Helper\Data $catalogData * @param array $data - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Framework\Pricing\Helper\Data $pricingHelper, \Magento\Catalog\Helper\Data $catalogData, - array $data = [], - CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null + array $data = [] ) { $this->pricingHelper = $pricingHelper; $this->_catalogHelper = $catalogData; - $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule - ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); parent::__construct($context, $data); } @@ -119,6 +112,7 @@ public function getOption() * Retrieve formatted price * * @return string + * @since 102.0.6 */ public function getFormattedPrice() { @@ -138,7 +132,7 @@ public function getFormattedPrice() * * @return string * - * @deprecated + * @deprecated 102.0.6 * @see getFormattedPrice() */ public function getFormatedPrice() @@ -168,15 +162,6 @@ protected function _formatPrice($value, $flag = true) $priceStr = $sign; $customOptionPrice = $this->getProduct()->getPriceInfo()->getPrice('custom_option_price'); - - if (!$value['is_percent']) { - $value['pricing_value'] = $this->calculateCustomOptionCatalogRule->execute( - $this->getProduct(), - (float)$value['pricing_value'], - (bool)$value['is_percent'] - ); - } - $context = [CustomOptionPriceInterface::CONFIGURATION_OPTION_FLAG => true]; $optionAmount = $customOptionPrice->getCustomAmount($value['pricing_value'], null, $context); $priceStr .= $this->getLayout()->getBlock('product.price.render.default')->renderAmount( diff --git a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php index 6d96ba8e1880e..c5c08a0552f42 100644 --- a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php +++ b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php @@ -26,7 +26,7 @@ * by customer on frontend and data to synchronize this tracks with backend * * @api - * @since 101.1.0 + * @since 102.0.0 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductViewCounter extends Template @@ -122,7 +122,7 @@ public function __construct( * requests and will be flushed with full page cache * * @return string {JSON encoded data} - * @since 101.1.0 + * @since 102.0.0 * @throws \Magento\Framework\Exception\LocalizedException * @throws \Magento\Framework\Exception\NoSuchEntityException */ diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php index 696401e5430d6..3651a9bc6adea 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php @@ -7,15 +7,17 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Eav\Model\Config; use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** - * Class used for saving mass updated products attributes. + * Class responsible for saving product attributes. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute implements HttpPostActionInterface @@ -60,6 +62,11 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribut */ private $eavConfig; + /** + * @var ProductFactory + */ + private $productFactory; + /** * @param Action\Context $context * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper @@ -71,6 +78,7 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribut * @param int $bulkSize * @param TimezoneInterface $timezone * @param Config $eavConfig + * @param ProductFactory $productFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -83,7 +91,8 @@ public function __construct( \Magento\Authorization\Model\UserContextInterface $userContext, int $bulkSize = 100, TimezoneInterface $timezone = null, - Config $eavConfig = null + Config $eavConfig = null, + ProductFactory $productFactory = null ) { parent::__construct($context, $attributeHelper); $this->bulkManagement = $bulkManagement; @@ -96,6 +105,7 @@ public function __construct( ->get(TimezoneInterface::class); $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() ->get(Config::class); + $this->productFactory = $productFactory ?? ObjectManager::getInstance()->get(ProductFactory::class); } /** @@ -121,9 +131,10 @@ public function execute() $attributesData = $this->sanitizeProductAttributes($attributesData); try { + $this->validateProductAttributes($attributesData); $this->publish($attributesData, $websiteRemoveData, $websiteAddData, $storeId, $websiteId, $productIds); $this->messageManager->addSuccessMessage(__('Message is added to queue')); - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addExceptionMessage( @@ -152,10 +163,12 @@ private function sanitizeProductAttributes($attributesData) } $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); + if (!$attribute->getAttributeId()) { unset($attributesData[$attributeCode]); continue; } + if ($attribute->getBackendType() === 'datetime') { if (!empty($value)) { $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]); @@ -183,6 +196,25 @@ private function sanitizeProductAttributes($attributesData) return $attributesData; } + /** + * Validate product attributes data. + * + * @param array $attributesData + * + * @return void + * @throws LocalizedException + */ + private function validateProductAttributes(array $attributesData): void + { + $product = $this->productFactory->create(); + $product->setData($attributesData); + + foreach (array_keys($attributesData) as $attributeCode) { + $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); + $attribute->getBackend()->validate($product); + } + } + /** * Schedule new bulk * @@ -192,7 +224,7 @@ private function sanitizeProductAttributes($attributesData) * @param int $storeId * @param int $websiteId * @param array $productIds - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * * @return void */ @@ -246,7 +278,7 @@ private function publish( $this->userContext->getUserId() ); if (!$result) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Something went wrong while processing the request.') ); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php index cf12e332be86d..4ca9d4b0d0606 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -224,7 +224,7 @@ public function execute() return $this->returnResult('catalog/*/', [], ['error' => true]); } // entity type check - if ($model->getEntityTypeId() != $this->_entityTypeId) { + if ($model->getEntityTypeId() != $this->_entityTypeId || array_key_exists('backend_model', $data)) { $this->messageManager->addErrorMessage(__('We can\'t update the attribute.')); $this->_session->setAttributeData($data); return $this->returnResult('catalog/*/', [], ['error' => true]); @@ -261,6 +261,12 @@ public function execute() unset($data['apply_to']); } + if ($model->getBackendType() == 'static' && !$model->getIsUserDefined()) { + $data['frontend_class'] = $model->getFrontendClass(); + } + + unset($data['entity_type_id']); + $model->addData($data); if (!$attributeId) { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index dcb7074c0d036..a2be7db7e62be 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -3,17 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; +use Magento\Backend\App\Action\Context; use Magento\Catalog\Controller\Adminhtml\Product\Attribute as AttributeAction; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Entity\Attribute\Set; use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; use Magento\Framework\DataObject; use Magento\Framework\Escaper; +use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\FormData; +use Magento\Framework\View\LayoutFactory; +use Magento\Framework\View\Result\PageFactory; /** * Product attribute validate controller. @@ -25,12 +35,12 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo const DEFAULT_MESSAGE_KEY = 'message'; /** - * @var \Magento\Framework\Controller\Result\JsonFactory + * @var JsonFactory */ protected $resultJsonFactory; /** - * @var \Magento\Framework\View\LayoutFactory + * @var LayoutFactory */ protected $layoutFactory; @@ -57,12 +67,12 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo /** * Constructor * - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Cache\FrontendInterface $attributeLabelCache - * @param \Magento\Framework\Registry $coreRegistry - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory - * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory - * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param Context $context + * @param FrontendInterface $attributeLabelCache + * @param Registry $coreRegistry + * @param PageFactory $resultPageFactory + * @param JsonFactory $resultJsonFactory + * @param LayoutFactory $layoutFactory * @param array $multipleAttributeList * @param FormData|null $formDataSerializer * @param AttributeCodeValidator|null $attributeCodeValidator @@ -70,12 +80,12 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Framework\Cache\FrontendInterface $attributeLabelCache, - \Magento\Framework\Registry $coreRegistry, - \Magento\Framework\View\Result\PageFactory $resultPageFactory, - \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, - \Magento\Framework\View\LayoutFactory $layoutFactory, + Context $context, + FrontendInterface $attributeLabelCache, + Registry $coreRegistry, + PageFactory $resultPageFactory, + JsonFactory $resultJsonFactory, + LayoutFactory $layoutFactory, array $multipleAttributeList = [], FormData $formDataSerializer = null, AttributeCodeValidator $attributeCodeValidator = null, @@ -96,7 +106,7 @@ public function __construct( /** * @inheritdoc * - * @return \Magento\Framework\Controller\ResultInterface + * @return ResultInterface * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -118,14 +128,22 @@ public function execute() $attributeCode = $this->getRequest()->getParam('attribute_code'); $frontendLabel = $this->getRequest()->getParam('frontend_label'); - $attributeCode = $attributeCode ?: $this->generateCode($frontendLabel[0]); $attributeId = $this->getRequest()->getParam('attribute_id'); - $attribute = $this->_objectManager->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - )->loadByCode( - $this->_entityTypeId, - $attributeCode - ); + + if ($attributeId) { + $attribute = $this->_objectManager->create( + Attribute::class + )->load($attributeId); + $attributeCode = $attribute->getAttributeCode(); + } else { + $attributeCode = $attributeCode ?: $this->generateCode($frontendLabel[0]); + $attribute = $this->_objectManager->create( + Attribute::class + )->loadByCode( + $this->_entityTypeId, + $attributeCode + ); + } if ($attribute->getId() && !$attributeId || $attributeCode === 'product_type' || $attributeCode === 'type_id') { $message = strlen($this->getRequest()->getParam('attribute_code')) @@ -145,8 +163,8 @@ public function execute() if ($this->getRequest()->has('new_attribute_set_name')) { $setName = $this->getRequest()->getParam('new_attribute_set_name'); - /** @var $attributeSet \Magento\Eav\Model\Entity\Attribute\Set */ - $attributeSet = $this->_objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class); + /** @var $attributeSet Set */ + $attributeSet = $this->_objectManager->create(Set::class); $attributeSet->setEntityTypeId($this->_entityTypeId)->load($setName, 'attribute_set_name'); if ($attributeSet->getId()) { $setName = $this->escaper->escapeHtml($setName); @@ -252,7 +270,7 @@ private function checkUniqueOption(DataObject $response, array $options = null) private function checkEmptyOption(DataObject $response, array $optionsForCheck = null) { foreach ($optionsForCheck as $optionValues) { - if (isset($optionValues[0]) && trim($optionValues[0]) == '') { + if (isset($optionValues[0]) && trim((string)$optionValues[0]) == '') { $this->setMessageToResponse($response, [__("The value of Admin scope can't be empty.")]); $response->setError(true); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index 2ae97223d6359..d948daed1c7d9 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -400,7 +400,7 @@ private function overwriteValue($optionId, $option, $overwriteOptions) * Get link resolver instance * * @return LinkResolver - * @deprecated 101.0.0 + * @deprecated 102.0.0 */ private function getLinkResolver() { diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php index 552af244f0097..e448be9a1df21 100644 --- a/app/code/Magento/Catalog/Controller/Category/View.php +++ b/app/code/Magento/Catalog/Controller/Category/View.php @@ -205,10 +205,9 @@ protected function _initCategory() /** * Category view action * - * @return ResultInterface * @throws NoSuchEntityException */ - public function execute(): ?ResultInterface + public function execute() { $result = null; diff --git a/app/code/Magento/Catalog/Helper/Data.php b/app/code/Magento/Catalog/Helper/Data.php index 3e96763632830..3a55164aa33ef 100644 --- a/app/code/Magento/Catalog/Helper/Data.php +++ b/app/code/Magento/Catalog/Helper/Data.php @@ -451,7 +451,7 @@ public function isUsingStaticUrlsAllowed() * Check if the parsing of URL directives is allowed for the catalog * * @return bool - * @deprecated + * @deprecated 103.0.0 * @see \Magento\Catalog\Helper\Output::isDirectivesExists */ public function isUrlDirectivesParsingAllowed() diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index 110b798df9df9..ab74b5694ce9f 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -297,7 +297,7 @@ public function resize($width, $height = null) * * @param int $quality * @return $this - * @deprecated + * @deprecated 103.0.1 */ public function setQuality($quality) { @@ -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; @@ -446,7 +448,7 @@ public function placeholder($fileName) * @param null|string $placeholder * @return string * - * @deprecated 101.1.0 Returns only default placeholder. + * @deprecated 102.0.0 Returns only default placeholder. * Does not take into account custom placeholders set in Configuration. */ public function getPlaceholder($placeholder = null) @@ -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/Helper/Product/Compare.php b/app/code/Magento/Catalog/Helper/Product/Compare.php index 49a90c590a440..4e476fe8d1568 100644 --- a/app/code/Magento/Catalog/Helper/Product/Compare.php +++ b/app/code/Magento/Catalog/Helper/Product/Compare.php @@ -279,7 +279,7 @@ public function getItemCollection() // cannot be placed in constructor because of the cyclic dependency which cannot be fixed with proxy class // collection uses this helper in constructor when calling isEnabledFlat() method $this->_itemCollection = $this->_itemCollectionFactory->create(); - $this->_itemCollection->useProductItem(true)->setStoreId($this->_storeManager->getStore()->getId()); + $this->_itemCollection->useProductItem()->setStoreId($this->_storeManager->getStore()->getId()); if ($this->_customerSession->isLoggedIn()) { $this->_itemCollection->setCustomerId($this->_customerSession->getCustomerId()); @@ -313,7 +313,7 @@ public function calculate($logout = false) { /** @var $collection Collection */ $collection = $this->_itemCollectionFactory->create() - ->useProductItem(true); + ->useProductItem(); if (!$logout && $this->_customerSession->isLoggedIn()) { $collection->setCustomerId($this->_customerSession->getCustomerId()); } elseif ($this->_customerId) { diff --git a/app/code/Magento/Catalog/Model/AbstractModel.php b/app/code/Magento/Catalog/Model/AbstractModel.php index 78a49cd1e8b14..851055c1bf810 100644 --- a/app/code/Magento/Catalog/Model/AbstractModel.php +++ b/app/code/Magento/Catalog/Model/AbstractModel.php @@ -223,7 +223,7 @@ public function unsetData($key = null) * Get collection instance * * @return \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection - * @deprecated 101.1.0 because collections should be used directly via factory + * @deprecated 102.0.0 because collections should be used directly via factory */ public function getResourceCollection() { diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php b/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php index b5aa5e2035100..c3c331ccf7ef6 100644 --- a/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php @@ -22,7 +22,7 @@ class Customlayoutupdate extends AbstractBackend { /** * @var ValidatorFactory - * @deprecated Is not used anymore. + * @deprecated 103.0.4 Is not used anymore. */ protected $_layoutUpdateValidatorFactory; @@ -117,6 +117,7 @@ private function putValue(AbstractModel $object, ?string $value): void * * @param AbstractModel $object * @throws LocalizedException + * @since 103.0.4 */ public function beforeSave($object) { diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index 330debdc32469..538a721d356d7 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -87,7 +87,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements * * @var string */ - protected $_cacheTag = self::CACHE_TAG; + protected $_cacheTag = false; /** * URL Model instance @@ -98,6 +98,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements /** * @var ResourceModel\Category + * @since 102.0.6 */ protected $_resource; @@ -105,7 +106,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements * URL rewrite model * * @var \Magento\UrlRewrite\Model\UrlRewrite - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ protected $_urlRewrite; @@ -135,7 +136,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements /** * Attributes are that part of interface * - * @deprecated + * @deprecated 103.0.0 * @see CategoryInterface::ATTRIBUTES * @var array */ @@ -319,8 +320,9 @@ protected function getCustomAttributesCodes() * * @throws \Magento\Framework\Exception\LocalizedException * @return \Magento\Catalog\Model\ResourceModel\Category - * @deprecated because resource models should be used directly + * @deprecated 102.0.6 because resource models should be used directly * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod + * @since 102.0.6 */ protected function _getResource() { @@ -1111,6 +1113,17 @@ public function afterSave() return $result; } + /** + * @inheritDoc + */ + public function getCacheTags() + { + $identities = $this->getIdentities(); + $cacheTags = !empty($identities) ? (array) $identities : parent::getCacheTags(); + + return $cacheTags; + } + /** * Init indexing process after category save * diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Sortby.php b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Sortby.php index 057933c55e6de..c1cfbdade3403 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Sortby.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Sortby.php @@ -102,6 +102,11 @@ public function beforeSave($object) } $object->setData($attributeCode, implode(',', $data) ?: null); } + if ($attributeCode == 'default_sort_by') { + $data = $object->getData($attributeCode); + $attributeValue = (is_array($data) ? reset($data) : (!empty($data))) ? $data : null; + $object->setData($attributeCode, $attributeValue); + } if (!$object->hasData($attributeCode)) { $object->setData($attributeCode, null); } diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php index 20ea899a3d0d7..486263f3de5a6 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php @@ -19,7 +19,7 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource /** * @inheritdoc - * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + * @deprecated 103.0.1 since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles */ protected $_options = null; diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index d8c79c485e3e5..0a562a9a80c89 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -246,7 +246,7 @@ public function __construct( /** * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ public function getMeta() { @@ -495,7 +495,7 @@ protected function addUseConfigSettings($categoryData) * @param Category $category * @param array $categoryData * @return array - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @since 101.0.0 */ protected function addUseDefaultSettings($category, $categoryData) diff --git a/app/code/Magento/Catalog/Model/CategoryLink.php b/app/code/Magento/Catalog/Model/CategoryLink.php index fe640a72d0b6d..38c6f77483de2 100644 --- a/app/code/Magento/Catalog/Model/CategoryLink.php +++ b/app/code/Magento/Catalog/Model/CategoryLink.php @@ -3,39 +3,41 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model; +use Magento\Catalog\Api\Data\CategoryLinkExtensionInterface; +use Magento\Catalog\Api\Data\CategoryLinkInterface; +use Magento\Framework\Model\AbstractExtensibleModel; + /** * @codeCoverageIgnore */ -class CategoryLink extends \Magento\Framework\Api\AbstractExtensibleObject implements - \Magento\Catalog\Api\Data\CategoryLinkInterface +class CategoryLink extends AbstractExtensibleModel implements CategoryLinkInterface { - /**#@+ - * Constants - */ - const KEY_POSITION = 'position'; - const KEY_CATEGORY_ID = 'category_id'; - /**#@-*/ + public const KEY_POSITION = 'position'; + public const KEY_CATEGORY_ID = 'category_id'; /** - * {@inheritdoc} + * @inheritdoc */ public function getPosition() { - return $this->_get(self::KEY_POSITION); + return $this->getData(self::KEY_POSITION); } /** - * {@inheritdoc} + * @inheritdoc */ public function getCategoryId() { - return $this->_get(self::KEY_CATEGORY_ID); + return $this->getData(self::KEY_CATEGORY_ID); } /** + * @inheritDoc + * * @param int $position * @return $this */ @@ -56,7 +58,7 @@ public function setCategoryId($categoryId) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Catalog\Api\Data\CategoryLinkExtensionInterface|null */ @@ -66,14 +68,13 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Catalog\Api\Data\CategoryLinkExtensionInterface $extensionAttributes * @return $this */ - public function setExtensionAttributes( - \Magento\Catalog\Api\Data\CategoryLinkExtensionInterface $extensionAttributes - ) { + public function setExtensionAttributes(CategoryLinkExtensionInterface $extensionAttributes) + { return $this->_setExtensionAttributes($extensionAttributes); } } diff --git a/app/code/Magento/Catalog/Model/CategoryList.php b/app/code/Magento/Catalog/Model/CategoryList.php index cc8920203526f..e7c755b379b91 100644 --- a/app/code/Magento/Catalog/Model/CategoryList.php +++ b/app/code/Magento/Catalog/Model/CategoryList.php @@ -97,7 +97,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) /** * Retrieve collection processor * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php index 497ed2fd49953..a928ddea03a70 100644 --- a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php +++ b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php @@ -8,21 +8,21 @@ namespace Magento\Catalog\Model; /** - * Filter custom attributes for product using the blacklist + * Filter custom attributes for product using the excluded list */ class FilterProductCustomAttribute { /** * @var array */ - private $blackList; + private $excludedList; /** - * @param array $blackList + * @param array $excludedList */ - public function __construct(array $blackList = []) + public function __construct(array $excludedList = []) { - $this->blackList = $blackList; + $this->excludedList = $excludedList; } /** @@ -33,6 +33,6 @@ public function __construct(array $blackList = []) */ public function execute(array $attributes): array { - return array_diff_key($attributes, array_flip($this->blackList)); + return array_diff_key($attributes, array_flip($this->excludedList)); } } diff --git a/app/code/Magento/Catalog/Model/FrontendStorageConfigurationInterface.php b/app/code/Magento/Catalog/Model/FrontendStorageConfigurationInterface.php index dac9e03e0b753..c61511d1ed49f 100644 --- a/app/code/Magento/Catalog/Model/FrontendStorageConfigurationInterface.php +++ b/app/code/Magento/Catalog/Model/FrontendStorageConfigurationInterface.php @@ -9,7 +9,7 @@ /** * @api * Storage, which provide information for frontend storages, as product-storage, ids-storage - * @since 101.1.0 + * @since 102.0.0 */ interface FrontendStorageConfigurationInterface { @@ -23,7 +23,7 @@ interface FrontendStorageConfigurationInterface * Prepare dynamic data which will be used in Storage Configuration (e.g. data from App/Config) * * @return array - * @since 101.1.0 + * @since 102.0.0 */ public function get(); } 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/Flat/Plugin/StoreGroup.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Plugin/StoreGroup.php index 670f19db725f7..16f96d794a97b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Plugin/StoreGroup.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Plugin/StoreGroup.php @@ -5,18 +5,13 @@ */ namespace Magento\Catalog\Model\Indexer\Category\Flat\Plugin; -use Magento\Framework\Model\ResourceModel\Db\AbstractDb; -use Magento\Framework\Model\AbstractModel; -use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Model\Indexer\Category\Flat\State; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; class StoreGroup { - /** - * @var bool - */ - private $needInvalidating; - /** * @var IndexerRegistry */ @@ -48,35 +43,21 @@ protected function validate(AbstractModel $group) return $group->dataHasChangedFor('root_category_id') && !$group->isObjectNew(); } - /** - * Check if need invalidate flat category indexer - * - * @param AbstractDb $subject - * @param AbstractModel $group - * - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforeSave(AbstractDb $subject, AbstractModel $group) - { - $this->needInvalidating = $this->validate($group); - } - /** * Invalidate flat category indexer if root category changed for store group * * @param AbstractDb $subject - * @param AbstractDb $objectResource - * + * @param AbstractDb $result + * @param AbstractModel $group * @return AbstractDb * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function afterSave(AbstractDb $subject, AbstractDb $objectResource) + public function afterSave(AbstractDb $subject, AbstractDb $result, AbstractModel $group) { - if ($this->needInvalidating && $this->state->isFlatEnabled()) { + if ($this->validate($group) && $this->state->isFlatEnabled()) { $this->indexerRegistry->get(State::INDEXER_ID)->invalidate(); } - return $objectResource; + return $result; } } 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 178f4172ce6fa..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'; @@ -109,6 +110,7 @@ abstract class AbstractAction /** * @var TableMaintainer + * @since 102.0.5 */ protected $tableMaintainer; @@ -195,7 +197,7 @@ protected function getTable($table) * The name is switched between 'catalog_category_product_index' and 'catalog_category_product_index_replica' * * @return string - * @deprecated + * @deprecated 102.0.5 */ protected function getMainTable() { @@ -206,7 +208,7 @@ protected function getMainTable() * Return temporary index table name * * @return string - * @deprecated + * @deprecated 102.0.5 */ protected function getMainTmpTable() { @@ -220,6 +222,7 @@ protected function getMainTmpTable() * * @param int $storeId * @return string + * @since 102.0.5 */ protected function getIndexTable($storeId) { @@ -502,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/Category/Product/Plugin/StoreGroup.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php index 005936a75e6d6..12a9d85dc416b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php @@ -5,19 +5,14 @@ */ namespace Magento\Catalog\Model\Indexer\Category\Product\Plugin; -use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\Model\ResourceModel\Db\AbstractDb; -use Magento\Framework\Model\AbstractModel; use Magento\Catalog\Model\Indexer\Category\Product; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; class StoreGroup { - /** - * @var bool - */ - private $needInvalidating; - /** * @var IndexerRegistry */ @@ -40,36 +35,23 @@ public function __construct( $this->tableMaintainer = $tableMaintainer; } - /** - * Check if need invalidate flat category indexer - * - * @param AbstractDb $subject - * @param AbstractModel $group - * - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforeSave(AbstractDb $subject, AbstractModel $group) - { - $this->needInvalidating = $this->validate($group); - } - /** * Invalidate flat product * * @param AbstractDb $subject - * @param AbstractDb $objectResource + * @param AbstractDb $result + * @param AbstractModel $group * * @return AbstractDb * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function afterSave(AbstractDb $subject, AbstractDb $objectResource) + public function afterSave(AbstractDb $subject, AbstractDb $result, AbstractModel $group) { - if ($this->needInvalidating) { + if ($this->validate($group)) { $this->indexerRegistry->get(Product::INDEXER_ID)->invalidate(); } - return $objectResource; + return $result; } /** diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php index b6f9e6adf4a1c..78353f56a0e13 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php @@ -5,18 +5,18 @@ */ namespace Magento\Catalog\Model\Indexer\Category\Product\Plugin; -use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; class StoreView extends StoreGroup { /** * Validate changes for invalidating indexer * - * @param \Magento\Framework\Model\AbstractModel $store + * @param AbstractModel $store * @return bool */ - protected function validate(\Magento\Framework\Model\AbstractModel $store) + protected function validate(AbstractModel $store) { return $store->isObjectNew() || $store->dataHasChangedFor('group_id'); } @@ -36,7 +36,7 @@ public function afterSave(AbstractDb $subject, AbstractDb $objectResource, Abstr $this->tableMaintainer->createTablesForStore($store->getId()); } - return parent::afterSave($subject, $objectResource); + return parent::afterSave($subject, $objectResource, $store); } /** diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index 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/Eav/Plugin/StoreView.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Plugin/StoreView.php index 95e525b2601f4..4503ca5ea23c7 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Plugin/StoreView.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Plugin/StoreView.php @@ -3,21 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Model\Indexer\Product\Eav\Plugin; +use Magento\Catalog\Model\Indexer\Product\Eav\Processor; +use Magento\Framework\Model\AbstractModel; +use Magento\Store\Model\ResourceModel\Store; + class StoreView { /** * Product attribute indexer processor * - * @var \Magento\Catalog\Model\Indexer\Product\Eav\Processor + * @var Processor */ protected $_indexerEavProcessor; /** - * @param \Magento\Catalog\Model\Indexer\Product\Eav\Processor $indexerEavProcessor + * @param Processor $indexerEavProcessor */ - public function __construct(\Magento\Catalog\Model\Indexer\Product\Eav\Processor $indexerEavProcessor) + public function __construct(Processor $indexerEavProcessor) { $this->_indexerEavProcessor = $indexerEavProcessor; } @@ -25,18 +30,19 @@ public function __construct(\Magento\Catalog\Model\Indexer\Product\Eav\Processor /** * Before save handler * - * @param \Magento\Store\Model\ResourceModel\Store $subject - * @param \Magento\Framework\Model\AbstractModel $object + * @param Store $subject + * @param Store $result + * @param AbstractModel $object * - * @return void + * @return Store * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeSave( - \Magento\Store\Model\ResourceModel\Store $subject, - \Magento\Framework\Model\AbstractModel $object - ) { - if ((!$object->getId() || $object->dataHasChangedFor('group_id')) && $object->getIsActive()) { + public function afterSave(Store $subject, Store $result, AbstractModel $object) + { + if (($object->isObjectNew() || $object->dataHasChangedFor('group_id')) && $object->getIsActive()) { $this->_indexerEavProcessor->markIndexerAsInvalid(); } + + return $result; } } 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/Plugin/Store.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/Store.php index ef7919193e609..38b9281f42cca 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/Store.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/Store.php @@ -6,19 +6,23 @@ namespace Magento\Catalog\Model\Indexer\Product\Flat\Plugin; +use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Framework\Model\AbstractModel; +use Magento\Store\Model\ResourceModel\Store as StoreResourceModel; + class Store { /** * Product flat indexer processor * - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + * @var Processor */ protected $_productFlatIndexerProcessor; /** - * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor + * @param Processor $productFlatIndexerProcessor */ - public function __construct(\Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor) + public function __construct(Processor $productFlatIndexerProcessor) { $this->_productFlatIndexerProcessor = $productFlatIndexerProcessor; } @@ -26,18 +30,19 @@ public function __construct(\Magento\Catalog\Model\Indexer\Product\Flat\Processo /** * Before save handler * - * @param \Magento\Store\Model\ResourceModel\Store $subject - * @param \Magento\Framework\Model\AbstractModel $object + * @param StoreResourceModel $subject + * @param StoreResourceModel $result + * @param AbstractModel $object * - * @return void + * @return StoreResourceModel * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeSave( - \Magento\Store\Model\ResourceModel\Store $subject, - \Magento\Framework\Model\AbstractModel $object - ) { - if (!$object->getId() || $object->dataHasChangedFor('group_id')) { + public function afterSave(StoreResourceModel $subject, StoreResourceModel $result, AbstractModel $object) + { + if ($object->isObjectNew() || $object->dataHasChangedFor('group_id')) { $this->_productFlatIndexerProcessor->markIndexerAsInvalid(); } + + return $result; } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/StoreGroup.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/StoreGroup.php index df62fe8d349e4..1276e60ac74b0 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/StoreGroup.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/StoreGroup.php @@ -6,19 +6,23 @@ namespace Magento\Catalog\Model\Indexer\Product\Flat\Plugin; +use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Framework\Model\AbstractModel; +use Magento\Store\Model\ResourceModel\Group; + class StoreGroup { /** * Product flat indexer processor * - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + * @var Processor */ protected $_productFlatIndexerProcessor; /** - * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor + * @param Processor $productFlatIndexerProcessor */ - public function __construct(\Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor) + public function __construct(Processor $productFlatIndexerProcessor) { $this->_productFlatIndexerProcessor = $productFlatIndexerProcessor; } @@ -26,18 +30,19 @@ public function __construct(\Magento\Catalog\Model\Indexer\Product\Flat\Processo /** * Before save handler * - * @param \Magento\Store\Model\ResourceModel\Group $subject - * @param \Magento\Framework\Model\AbstractModel $object + * @param Group $subject + * @param Group $result + * @param AbstractModel $object * - * @return void + * @return Group * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeSave( - \Magento\Store\Model\ResourceModel\Group $subject, - \Magento\Framework\Model\AbstractModel $object - ) { - if (!$object->getId() || $object->dataHasChangedFor('root_category_id')) { + public function afterSave(Group $subject, Group $result, AbstractModel $object) + { + if ($object->isObjectNew() || $object->dataHasChangedFor('root_category_id')) { $this->_productFlatIndexerProcessor->markIndexerAsInvalid(); } + + return $result; } } 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 c0722901e3b1c..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); @@ -368,7 +372,7 @@ protected function _fillTemporaryTable( * Get Metadata Pool * * @return \Magento\Framework\EntityManager\MetadataPool - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ private function getMetadataPool() { 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 e9a907f0b5097..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,9 +170,9 @@ 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 Used only for backward compatibility for indexer, which not support indexation by dimensions + * @deprecated 102.0.6 Used only for backward compatibility for indexer, which not support indexation by dimensions */ protected function _syncData(array $processIds = []) { @@ -173,7 +191,7 @@ protected function _syncData(array $processIds = []) } } - $query = $insertSelect->insertFromSelect($this->tableMaintainer->getMainTable($dimensions)); + $query = $insertSelect->insertFromSelect($this->tableMaintainer->getMainTableByDimensions($dimensions)); $this->getConnection()->query($query); } return $this; @@ -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 = []) { @@ -368,14 +386,20 @@ protected function _reindexRows($changedIds = []) $productsTypes = $this->getProductsTypes($changedIds); $parentProductsTypes = $this->getParentProductsTypes($changedIds); - $changedIds = array_merge($changedIds, ...array_values($parentProductsTypes)); + $changedIds = array_unique(array_merge($changedIds, ...array_values($parentProductsTypes))); $productsTypes = array_merge_recursive($productsTypes, $parentProductsTypes); if ($changedIds) { $this->deleteIndexData($changedIds); } - foreach ($productsTypes as $productType => $entityIds) { - $indexer = $this->_getIndexer($productType); + + $typeIndexers = $this->getTypeIndexers(); + foreach ($typeIndexers as $productType => $indexer) { + $entityIds = $productsTypes[$productType] ?? []; + if (empty($entityIds)) { + continue; + } + if ($indexer instanceof DimensionalIndexerInterface) { foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $this->tableMaintainer->createMainTmpTable($dimensions); @@ -385,7 +409,7 @@ protected function _reindexRows($changedIds = []) // copy to index $this->_insertFromTable( $temporaryTable, - $this->tableMaintainer->getMainTable($dimensions) + $this->tableMaintainer->getMainTableByDimensions($dimensions) ); } } else { @@ -401,6 +425,8 @@ protected function _reindexRows($changedIds = []) } /** + * Delete Index data index for list of entities + * * @param array $entityIds * @return void */ @@ -408,9 +434,9 @@ private function deleteIndexData(array $entityIds) { foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $select = $this->getConnection()->select()->from( - ['index_price' => $this->tableMaintainer->getMainTable($dimensions)], + ['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); } @@ -422,7 +448,7 @@ private function deleteIndexData(array $entityIds) * @param null|array $parentIds * @param array $excludeIds * @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * @deprecated Used only for backward compatibility for do not broke custom indexer implementation + * @deprecated 102.0.6 Used only for backward compatibility for do not broke custom indexer implementation * which do not work by dimensions. * For indexers, which support dimensions all composite products read data directly from main price indexer table * or replica table for partial or full reindex correspondingly. @@ -468,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 */ @@ -476,7 +502,7 @@ private function getIndexTargetTableByDimension(array $dimensions) { $indexTargetTable = $this->getIndexTargetTable(); if ($indexTargetTable === self::getIndexTargetTable()) { - $indexTargetTable = $this->tableMaintainer->getMainTable($dimensions); + $indexTargetTable = $this->tableMaintainer->getMainTableByDimensions($dimensions); } if ($indexTargetTable === self::getIndexTargetTable() . '_replica') { $indexTargetTable = $this->tableMaintainer->getMainReplicaTable($dimensions); @@ -497,6 +523,8 @@ protected function getIndexTargetTable() } /** + * Get product Id field name + * * @return string */ protected function getProductIdFieldName() @@ -519,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); @@ -533,6 +561,7 @@ private function getProductsTypes(array $changedIds = []) /** * Get parent products types + * * Used for add composite products to reindex if we have only simple products in changed ids set * * @param array $productsIds @@ -564,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/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php index b30c85cfc52f0..d36c7507afa8a 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php @@ -425,7 +425,7 @@ private function switchTables(): void $mainTablesByDimension = []; foreach ($this->dimensionCollectionFactory->create() as $dimensions) { - $mainTablesByDimension[] = $this->dimensionTableMaintainer->getMainTable($dimensions); + $mainTablesByDimension[] = $this->dimensionTableMaintainer->getMainTableByDimensions($dimensions); //Move data from indexers with old realisation $this->moveDataFromReplicaTableToReplicaTables($dimensions); @@ -488,7 +488,7 @@ private function moveDataFromReplicaTableToReplicaTables(array $dimensions): voi /** * Retrieves the index table that should be used * - * @deprecated + * @deprecated 102.0.6 */ protected function getIndexTargetTable(): string { diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php index c418f2e1f253b..974bd0e8f7860 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php @@ -15,6 +15,8 @@ /** * Class to prepare new tables for new indexer mode + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ModeSwitcher implements \Magento\Indexer\Model\ModeSwitcherInterface { @@ -98,7 +100,6 @@ public function switchMode(string $currentMode, string $previousMode) * Create new tables * * @param string $currentMode - * * @return void * @throws \Zend_Db_Exception */ @@ -116,7 +117,6 @@ public function createTables(string $currentMode) * * @param string $currentMode * @param string $previousMode - * * @return void */ public function moveData(string $currentMode, string $previousMode) @@ -125,17 +125,17 @@ public function moveData(string $currentMode, string $previousMode) $dimensionsArrayForPreviousMode = $this->getDimensionsArray($previousMode); foreach ($dimensionsArrayForCurrentMode as $dimensionsForCurrentMode) { - $newTable = $this->tableMaintainer->getMainTable($dimensionsForCurrentMode); + $newTable = $this->tableMaintainer->getMainTableByDimensions($dimensionsForCurrentMode); if (empty($dimensionsForCurrentMode)) { // new mode is 'none' foreach ($dimensionsArrayForPreviousMode as $dimensionsForPreviousMode) { - $oldTable = $this->tableMaintainer->getMainTable($dimensionsForPreviousMode); + $oldTable = $this->tableMaintainer->getMainTableByDimensions($dimensionsForPreviousMode); $this->insertFromOldTablesToNew($newTable, $oldTable); } } else { // new mode is not 'none' foreach ($dimensionsArrayForPreviousMode as $dimensionsForPreviousMode) { - $oldTable = $this->tableMaintainer->getMainTable($dimensionsForPreviousMode); + $oldTable = $this->tableMaintainer->getMainTableByDimensions($dimensionsForPreviousMode); $this->insertFromOldTablesToNew($newTable, $oldTable, $dimensionsForCurrentMode); } } @@ -146,7 +146,6 @@ public function moveData(string $currentMode, string $previousMode) * Drop old tables * * @param string $previousMode - * * @return void */ public function dropTables(string $previousMode) @@ -164,7 +163,6 @@ public function dropTables(string $previousMode) * Get dimensions array * * @param string $mode - * * @return \Magento\Framework\Indexer\MultiDimensionProvider */ private function getDimensionsArray(string $mode): \Magento\Framework\Indexer\MultiDimensionProvider @@ -184,7 +182,6 @@ private function getDimensionsArray(string $mode): \Magento\Framework\Indexer\Mu * @param string $newTable * @param string $oldTable * @param Dimension[] $dimensions - * * @return void */ private function insertFromOldTablesToNew(string $newTable, string $oldTable, array $dimensions = []) diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php index e3077baaeb7a6..5eed262352c76 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php @@ -7,36 +7,27 @@ namespace Magento\Catalog\Model\Indexer\Product\Price; -use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Indexer\Table\StrategyInterface; +use Magento\Framework\Model\ResourceModel\Db\Context as DbContext; use Magento\Framework\Search\Request\Dimension; -use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Search\Request\IndexScopeResolverInterface as TableResolver; +use Magento\Indexer\Model\ResourceModel\AbstractResource as AbstractIndexerResource; /** * Class encapsulate logic of work with tables per store in Product Price indexer */ -class TableMaintainer +class TableMaintainer extends AbstractIndexerResource { /** * Catalog product price index table name */ const MAIN_INDEX_TABLE = 'catalog_product_index_price'; - /** - * @var ResourceConnection - */ - private $resource; - /** * @var TableResolver */ private $tableResolver; - /** - * @var AdapterInterface - */ - private $connection; - /** * Catalog tmp category index table name */ @@ -53,47 +44,27 @@ class TableMaintainer private $mainTmpTable; /** - * @var null|string - */ - private $connectionName; - - /** - * @param ResourceConnection $resource + * @param DbContext $context + * @param StrategyInterface $tableStrategy * @param TableResolver $tableResolver - * @param null $connectionName + * @param string|null $connectionName */ public function __construct( - ResourceConnection $resource, + DbContext $context, + StrategyInterface $tableStrategy, TableResolver $tableResolver, $connectionName = null ) { - $this->resource = $resource; + parent::__construct($context, $tableStrategy, $connectionName); $this->tableResolver = $tableResolver; - $this->connectionName = $connectionName; - } - - /** - * Get connection for work with price indexer - * - * @return AdapterInterface - */ - public function getConnection(): AdapterInterface - { - if (null === $this->connection) { - $this->connection = $this->resource->getConnection($this->connectionName); - } - return $this->connection; } /** - * Return validated table name - * - * @param string $table - * @return string + * @inheritDoc */ - private function getTable(string $table): string + protected function _construct() { - return $this->resource->getTableName($table); + $this->_init(self::MAIN_INDEX_TABLE, 'entity_id'); } /** @@ -101,9 +72,7 @@ private function getTable(string $table): string * * @param string $mainTableName * @param string $newTableName - * * @return void - * * @throws \Zend_Db_Exception */ private function createTable(string $mainTableName, string $newTableName) @@ -119,7 +88,6 @@ private function createTable(string $mainTableName, string $newTableName) * Drop table * * @param string $tableName - * * @return void */ private function dropTable(string $tableName) @@ -133,7 +101,6 @@ private function dropTable(string $tableName) * Truncate table * * @param string $tableName - * * @return void */ private function truncateTable(string $tableName) @@ -147,7 +114,6 @@ private function truncateTable(string $tableName) * Get array key for tmp table * * @param Dimension[] $dimensions - * * @return string */ private function getArrayKeyForTmpTable(array $dimensions): string @@ -160,13 +126,12 @@ private function getArrayKeyForTmpTable(array $dimensions): string } /** - * Return main index table name + * Return main index table name using dimensions * * @param Dimension[] $dimensions - * * @return string */ - public function getMainTable(array $dimensions): string + public function getMainTableByDimensions(array $dimensions): string { return $this->tableResolver->resolve(self::MAIN_INDEX_TABLE, $dimensions); } @@ -175,14 +140,12 @@ public function getMainTable(array $dimensions): string * Create main and replica index tables for dimensions * * @param Dimension[] $dimensions - * * @return void - * * @throws \Zend_Db_Exception */ public function createTablesForDimensions(array $dimensions) { - $mainTableName = $this->getMainTable($dimensions); + $mainTableName = $this->getMainTableByDimensions($dimensions); //Create index table for dimensions based on main replica table //Using main replica table is necessary for backward capability and TableResolver plugin work $this->createTable( @@ -190,7 +153,7 @@ public function createTablesForDimensions(array $dimensions) $mainTableName ); - $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix; + $mainReplicaTableName = $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix; //Create replica table for dimensions based on main replica table $this->createTable( $this->getTable(self::MAIN_INDEX_TABLE . $this->additionalTableSuffix), @@ -202,15 +165,14 @@ public function createTablesForDimensions(array $dimensions) * Drop main and replica index tables for dimensions * * @param Dimension[] $dimensions - * * @return void */ public function dropTablesForDimensions(array $dimensions) { - $mainTableName = $this->getMainTable($dimensions); + $mainTableName = $this->getMainTableByDimensions($dimensions); $this->dropTable($mainTableName); - $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix; + $mainReplicaTableName = $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix; $this->dropTable($mainReplicaTableName); } @@ -218,15 +180,14 @@ public function dropTablesForDimensions(array $dimensions) * Truncate main and replica index tables for dimensions * * @param Dimension[] $dimensions - * * @return void */ public function truncateTablesForDimensions(array $dimensions) { - $mainTableName = $this->getMainTable($dimensions); + $mainTableName = $this->getMainTableByDimensions($dimensions); $this->truncateTable($mainTableName); - $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix; + $mainReplicaTableName = $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix; $this->truncateTable($mainReplicaTableName); } @@ -234,26 +195,24 @@ public function truncateTablesForDimensions(array $dimensions) * Return replica index table name * * @param Dimension[] $dimensions - * * @return string */ public function getMainReplicaTable(array $dimensions): string { - return $this->getMainTable($dimensions) . $this->additionalTableSuffix; + return $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix; } /** * Create temporary index table for dimensions * * @param Dimension[] $dimensions - * * @return void */ public function createMainTmpTable(array $dimensions) { // Create temporary table based on template table catalog_product_index_price_tmp without indexes - $templateTableName = $this->resource->getTableName(self::MAIN_INDEX_TABLE . '_tmp'); - $temporaryTableName = $this->getMainTable($dimensions) . $this->tmpTableSuffix; + $templateTableName = $this->_resources->getTableName(self::MAIN_INDEX_TABLE . '_tmp'); + $temporaryTableName = $this->getMainTableByDimensions($dimensions) . $this->tmpTableSuffix; $this->getConnection()->createTemporaryTableLike($temporaryTableName, $templateTableName, true); $this->mainTmpTable[$this->getArrayKeyForTmpTable($dimensions)] = $temporaryTableName; } @@ -262,9 +221,7 @@ public function createMainTmpTable(array $dimensions) * Return temporary index table name * * @param Dimension[] $dimensions - * * @return string - * * @throws \LogicException */ public function getMainTmpTable(array $dimensions): string diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/UpdateIndexInterface.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/UpdateIndexInterface.php index 3d1809997bd61..091131508ef66 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/UpdateIndexInterface.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/UpdateIndexInterface.php @@ -11,7 +11,7 @@ * Defines strategy for updating price index * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface UpdateIndexInterface { @@ -21,7 +21,7 @@ interface UpdateIndexInterface * @param GroupInterface $group * @param bool $isGroupNew * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function update(GroupInterface $group, $isGroupNew); } diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php index 77dedb9eb0121..3494fd00a8b6c 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php @@ -72,6 +72,8 @@ public function renderRangeLabel($fromPrice, $toPrice) } /** + * Prepare range data + * * @param int $range * @param int[] $dbRanges * @return array @@ -81,12 +83,10 @@ public function renderRangeData($range, $dbRanges) if (empty($dbRanges)) { return []; } - $lastIndex = array_keys($dbRanges); - $lastIndex = $lastIndex[count($lastIndex) - 1]; foreach ($dbRanges as $index => $count) { - $fromPrice = $index == 1 ? '' : ($index - 1) * $range; - $toPrice = $index == $lastIndex ? '' : $index * $range; + $fromPrice = $index == 1 ? 0 : ($index - 1) * $range; + $toPrice = $index * $range; $this->itemDataBuilder->addItemData( $this->renderRangeLabel($fromPrice, $toPrice), $fromPrice . '-' . $toPrice, diff --git a/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php b/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php index b812de1dfc2ae..640a6539f0041 100644 --- a/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php +++ b/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php @@ -11,7 +11,10 @@ namespace Magento\Catalog\Model\Plugin; use Magento\Catalog\Model\Category\DataProvider; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Sets the default value for Category Design Layout if provided @@ -21,11 +24,28 @@ class SetPageLayoutDefaultValue private $defaultValue; /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager * @param string $defaultValue */ - public function __construct(string $defaultValue = "") - { + public function __construct( + ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager, + string $defaultValue = "" + ) { $this->defaultValue = $defaultValue; + $this->scopeConfig = $scopeConfig; + $this->storeManager = $storeManager; } /** @@ -42,7 +62,15 @@ public function afterGetDefaultMetaData(DataProvider $subject, array $result): a $currentCategory = $subject->getCurrentCategory(); if ($currentCategory && !$currentCategory->getId() && array_key_exists('page_layout', $result)) { - $result['page_layout']['default'] = $this->defaultValue ?: null; + $defaultAdminValue = $this->scopeConfig->getValue( + 'web/default_layouts/default_category_layout', + ScopeInterface::SCOPE_STORE, + $this->storeManager->getStore()->getId() + ); + + $defaultValue = $defaultAdminValue ?: $this->defaultValue; + + $result['page_layout']['default'] = $defaultValue ?: null; } return $result; diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index bc8d274fb6e63..7c463267e5a58 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -11,7 +11,6 @@ use Magento\Catalog\Api\ProductLinkRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Backend\Media\EntryConverterPool; use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; -use Magento\Catalog\Model\FilterProductCustomAttribute; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; @@ -122,6 +121,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * @var ResourceModel\Product + * @since 102.0.6 */ protected $_resource; @@ -278,7 +278,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface - * @deprecated Not used anymore due to performance issue (loaded all product attributes) + * @deprecated 102.0.6 Not used anymore due to performance issue (loaded all product attributes) */ protected $metadataService; @@ -315,7 +315,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * List of attributes in ProductInterface * - * @deprecated + * @deprecated 103.0.0 * @see ProductInterface::ATTRIBUTES * @var array */ @@ -493,7 +493,8 @@ protected function _construct() * * @throws \Magento\Framework\Exception\LocalizedException * @return \Magento\Catalog\Model\ResourceModel\Product - * @deprecated because resource models should be used directly + * @deprecated 102.0.6 because resource models should be used directly + * @since 102.0.6 */ protected function _getResource() { @@ -640,7 +641,7 @@ public function getUpdatedAt() * * @param bool $calculate * @return void - * @deprecated + * @deprecated 102.0.4 */ public function setPriceCalculation($calculate = true) { @@ -977,6 +978,17 @@ public function afterSave() return $result; } + /** + * @inheritDoc + */ + public function getCacheTags() + { + $identities = $this->getIdentities(); + $cacheTags = !empty($identities) ? (array) $identities : parent::getCacheTags(); + + return $cacheTags; + } + /** * Set quantity for product * @@ -1168,6 +1180,7 @@ public function getTierPrice($qty = null) * Get formatted by currency product price * * @return array|double + * @since 102.0.6 */ public function getFormattedPrice() { @@ -1179,7 +1192,7 @@ public function getFormattedPrice() * * @return array|double * - * @deprecated + * @deprecated 102.0.6 * @see getFormattedPrice() */ public function getFormatedPrice() @@ -2152,13 +2165,13 @@ public function reset() /** * Get cache tags associated with object id * - * @deprecated + * @deprecated 102.0.5 * @see \Magento\Catalog\Model\Product::getIdentities * @return string[] */ public function getCacheIdTags() { - // phpstan:ignore + // phpstan:ignore "Call to an undefined static method" $tags = parent::getCacheIdTags(); $affectedCategoryIds = $this->getAffectedCategoryIds(); if (!$affectedCategoryIds) { @@ -2339,7 +2352,8 @@ public function isDisabled() public function getImage() { $this->getTypeInstance()->setImageFromChildProduct($this); - // phpstan:ignore + + // phpstan:ignore "Call to an undefined static method" return parent::getImage(); } @@ -2403,6 +2417,8 @@ public function reloadPriceInfo() } } + //phpcs:disable PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore + /** * Return Data Object data in array format. * @@ -2430,6 +2446,8 @@ public function __toArray() //phpcs:ignore PHPCompatibility.FunctionNameRestrict return $data; } + //phpcs:enable PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore + /** * Convert Category model into flat array. * @@ -2726,11 +2744,11 @@ public function setAssociatedProductIds(array $productIds) * * @return array|null * - * @deprecated 101.1.0 as Product model shouldn't be responsible for stock status + * @deprecated 102.0.0 as Product model shouldn't be responsible for stock status * @see StockItemInterface when you want to change the stock data * @see StockStatusInterface when you want to read the stock data for representation layer (storefront) * @see StockItemRepositoryInterface::save as extension point for customization of saving process - * @since 101.1.0 + * @since 102.0.0 */ public function getQuantityAndStockStatus() { @@ -2743,11 +2761,11 @@ public function getQuantityAndStockStatus() * @param array $quantityAndStockStatusData * @return $this * - * @deprecated 101.1.0 as Product model shouldn't be responsible for stock status + * @deprecated 102.0.0 as Product model shouldn't be responsible for stock status * @see StockItemInterface when you want to change the stock data * @see StockStatusInterface when you want to read the stock data for representation layer (storefront) * @see StockItemRepositoryInterface::save as extension point for customization of saving process - * @since 101.1.0 + * @since 102.0.0 */ public function setQuantityAndStockStatus($quantityAndStockStatusData) { @@ -2760,11 +2778,11 @@ public function setQuantityAndStockStatus($quantityAndStockStatusData) * * @return array|null * - * @deprecated 101.1.0 as Product model shouldn't be responsible for stock status + * @deprecated 102.0.0 as Product model shouldn't be responsible for stock status * @see StockItemInterface when you want to change the stock data * @see StockStatusInterface when you want to read the stock data for representation layer (storefront) * @see StockItemRepositoryInterface::save as extension point for customization of saving process - * @since 101.1.0 + * @since 102.0.0 */ public function getStockData() { @@ -2777,11 +2795,11 @@ public function getStockData() * @param array $stockData * @return $this * - * @deprecated 101.1.0 as Product model shouldn't be responsible for stock status + * @deprecated 102.0.0 as Product model shouldn't be responsible for stock status * @see StockItemInterface when you want to change the stock data * @see StockStatusInterface when you want to read the stock data for representation layer (storefront) * @see StockItemRepositoryInterface::save as extension point for customization of saving process - * @since 101.1.0 + * @since 102.0.0 */ public function setStockData($stockData) { 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/Attribute/Backend/Stock.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php index 994ff98dee217..713a0a35abec7 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php @@ -10,7 +10,7 @@ /** * Quantity and Stock Status attribute processing * - * @deprecated 101.1.0 as this attribute should be removed + * @deprecated 102.0.0 as this attribute should be removed * @see StockItemInterface when you want to change the stock data * @see StockStatusInterface when you want to read the stock data for representation layer (storefront) * @see StockItemRepositoryInterface::save as extension point for customization of saving process diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php index b797308c30fb0..1554293661c02 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php @@ -6,25 +6,39 @@ */ namespace Magento\Catalog\Model\Product\Attribute; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeOptionManagementInterface; +use Magento\Catalog\Api\ProductAttributeOptionUpdateInterface; +use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Eav\Api\AttributeOptionUpdateInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Framework\Exception\InputException; /** * Option management model for product attribute. */ -class OptionManagement implements \Magento\Catalog\Api\ProductAttributeOptionManagementInterface +class OptionManagement implements ProductAttributeOptionManagementInterface, ProductAttributeOptionUpdateInterface { /** - * @var \Magento\Eav\Api\AttributeOptionManagementInterface + * @var AttributeOptionManagementInterface */ protected $eavOptionManagement; /** - * @param \Magento\Eav\Api\AttributeOptionManagementInterface $eavOptionManagement + * @var AttributeOptionUpdateInterface + */ + private $eavOptionUpdate; + + /** + * @param AttributeOptionManagementInterface $eavOptionManagement + * @param AttributeOptionUpdateInterface $eavOptionUpdate */ public function __construct( - \Magento\Eav\Api\AttributeOptionManagementInterface $eavOptionManagement + AttributeOptionManagementInterface $eavOptionManagement, + AttributeOptionUpdateInterface $eavOptionUpdate ) { $this->eavOptionManagement = $eavOptionManagement; + $this->eavOptionUpdate = $eavOptionUpdate; } /** @@ -33,7 +47,7 @@ public function __construct( public function getItems($attributeCode) { return $this->eavOptionManagement->getItems( - \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode ); } @@ -44,8 +58,21 @@ public function getItems($attributeCode) public function add($attributeCode, $option) { return $this->eavOptionManagement->add( - \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeCode, + $option + ); + } + + /** + * @inheritdoc + */ + public function update(string $attributeCode, int $optionId, AttributeOptionInterface $option): bool + { + return $this->eavOptionUpdate->update( + ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode, + $optionId, $option ); } @@ -60,7 +87,7 @@ public function delete($attributeCode, $optionId) } return $this->eavOptionManagement->delete( - \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode, $optionId ); diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php index bc362430089c4..c0a13aa8b934a 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php @@ -82,7 +82,7 @@ public function getAllOptions() * Get serializer * * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ private function getSerializer() { diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php index dbc7535dccfa9..333e8021d30b5 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php @@ -19,7 +19,7 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource /** * @inheritdoc - * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + * @deprecated 103.0.1 since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles */ protected $_options = null; diff --git a/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php index 35c0a7835cb6c..b340e5dea5eb8 100644 --- a/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php +++ b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php @@ -13,6 +13,7 @@ * Resolves the product from a configured item. * * @api + * @since 102.0.7 */ interface ItemResolverInterface { @@ -21,6 +22,7 @@ interface ItemResolverInterface * * @param ItemInterface $item * @return ProductInterface + * @since 102.0.7 */ public function getFinalProduct(ItemInterface $item) : ProductInterface; } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index 225a3a4c44a9b..5fefcf995e0c7 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -596,10 +596,21 @@ private function canRemoveImage(ProductInterface $product, string $imageFile) :b $canRemoveImage = true; $gallery = $this->getImagesForAllStores($product); $storeId = $product->getStoreId(); + $storeIds = []; + $storeIds[] = 0; + $websiteIds = array_map('intval', $product->getWebsiteIds() ?? []); + foreach ($this->storeManager->getStores() as $store) { + if (in_array((int) $store->getWebsiteId(), $websiteIds, true)) { + $storeIds[] = (int) $store->getId(); + } + } if (!empty($gallery)) { foreach ($gallery as $image) { - if ($image['filepath'] === $imageFile && (int) $image['store_id'] !== $storeId) { + if (in_array((int) $image['store_id'], $storeIds) + && $image['filepath'] === $imageFile + && (int) $image['store_id'] !== $storeId + ) { $canRemoveImage = false; } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index 049846ef36490..8061422d84288 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -5,17 +5,69 @@ */ namespace Magento\Catalog\Model\Product\Gallery; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Media\Config; use Magento\Catalog\Model\ResourceModel\Product\Gallery; -use Magento\Framework\EntityManager\Operation\ExtensionInterface; +use Magento\Eav\Model\ResourceModel\AttributeValue; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Filesystem; +use Magento\Framework\Json\Helper\Data; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Update handler for catalog product gallery. * * @api * @since 101.0.0 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler +class UpdateHandler extends CreateHandler { + /** + * @var AttributeValue + */ + private $attributeValue; + + /** + * @param MetadataPool $metadataPool + * @param ProductAttributeRepositoryInterface $attributeRepository + * @param Gallery $resourceModel + * @param Data $jsonHelper + * @param Config $mediaConfig + * @param Filesystem $filesystem + * @param Database $fileStorageDb + * @param StoreManagerInterface|null $storeManager + * @param AttributeValue|null $attributeValue + */ + public function __construct( + MetadataPool $metadataPool, + ProductAttributeRepositoryInterface $attributeRepository, + Gallery $resourceModel, + Data $jsonHelper, + Config $mediaConfig, + Filesystem $filesystem, + Database $fileStorageDb, + StoreManagerInterface $storeManager = null, + ?AttributeValue $attributeValue = null + ) { + parent::__construct( + $metadataPool, + $attributeRepository, + $resourceModel, + $jsonHelper, + $mediaConfig, + $filesystem, + $fileStorageDb, + $storeManager + ); + $this->attributeValue = $attributeValue ?: ObjectManager::getInstance()->get(AttributeValue::class); + } + /** * @inheritdoc * @@ -26,6 +78,7 @@ protected function processDeletedImages($product, array &$images) $filesToDelete = []; $recordsToDelete = []; $picturesInOtherStores = []; + $imagesToDelete = []; foreach ($this->resourceModel->getProductImages($product, $this->extractStoreIds($product)) as $image) { $picturesInOtherStores[$image['filepath']] = true; @@ -38,6 +91,7 @@ protected function processDeletedImages($product, array &$images) continue; } $recordsToDelete[] = $image['value_id']; + $imagesToDelete[] = $image['file']; $catalogPath = $this->mediaConfig->getBaseMediaPath(); $isFile = $this->mediaDirectory->isFile($catalogPath . $image['file']); // only delete physical files if they are not used by any other products and if this file exist @@ -48,8 +102,8 @@ protected function processDeletedImages($product, array &$images) } } + $this->deleteMediaAttributeValues($product, $imagesToDelete); $this->resourceModel->deleteGallery($recordsToDelete); - $this->removeDeletedImages($filesToDelete); } @@ -94,14 +148,14 @@ protected function processNewImage($product, array &$image) /** * Retrieve store ids from product. * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return array * @since 101.0.0 */ protected function extractStoreIds($product) { $storeIds = $product->getStoreIds(); - $storeIds[] = \Magento\Store\Model\Store::DEFAULT_STORE_ID; + $storeIds[] = Store::DEFAULT_STORE_ID; // Removing current storeId. $storeIds = array_flip($storeIds); @@ -125,5 +179,35 @@ protected function removeDeletedImages(array $files) foreach ($files as $filePath) { $this->mediaDirectory->delete($catalogPath . '/' . $filePath); } + return null; + } + + /** + * Delete media attributes values for given images + * + * @param Product $product + * @param string[] $images + */ + private function deleteMediaAttributeValues(Product $product, array $images): void + { + if ($images) { + $values = $this->attributeValue->getValues( + ProductInterface::class, + $product->getData($this->metadata->getLinkField()), + $this->mediaConfig->getMediaAttributeCodes() + ); + $valuesToDelete = []; + foreach ($values as $value) { + if (in_array($value['value'], $images, true)) { + $valuesToDelete[] = $value; + } + } + if ($valuesToDelete) { + $this->attributeValue->deleteValues( + ProductInterface::class, + $valuesToDelete + ); + } + } } } diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php index a0be36c5a327c..3c60d81e9a4d8 100644 --- a/app/code/Magento/Catalog/Model/Product/Image.php +++ b/app/code/Magento/Catalog/Model/Product/Image.php @@ -45,7 +45,7 @@ class Image extends \Magento\Framework\Model\AbstractModel * Default quality value (for JPEG images only). * * @var int - * @deprecated use config setting with path self::XML_PATH_JPEG_QUALITY + * @deprecated 103.0.1 use config setting with path self::XML_PATH_JPEG_QUALITY */ protected $_quality = null; @@ -305,7 +305,7 @@ public function getHeight() * * @param int $quality * @return $this - * @deprecated use config setting with path self::XML_PATH_JPEG_QUALITY + * @deprecated 103.0.1 use config setting with path self::XML_PATH_JPEG_QUALITY */ public function setQuality($quality) { @@ -454,7 +454,7 @@ public function getBaseFile() /** * Get new file * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return bool|string */ public function getNewFile() diff --git a/app/code/Magento/Catalog/Model/Product/Link.php b/app/code/Magento/Catalog/Model/Product/Link.php index 5c07d3d32b257..f2b07bad8891c 100644 --- a/app/code/Magento/Catalog/Model/Product/Link.php +++ b/app/code/Magento/Catalog/Model/Product/Link.php @@ -58,7 +58,7 @@ class Link extends \Magento\Framework\Model\AbstractModel /** * @var \Magento\CatalogInventory\Helper\Stock - * @deprecated 101.0.1 + * @deprecated 101.0.0 */ protected $stockHelper; diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index 128f420e033c2..e83982b8ce672 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Catalog\Model\Product; @@ -17,10 +16,8 @@ use Magento\Catalog\Model\Product\Option\Type\File; use Magento\Catalog\Model\Product\Option\Type\Select; use Magento\Catalog\Model\Product\Option\Type\Text; -use Magento\Catalog\Model\Product\Option\Value; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; -use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Pricing\Price\BasePrice; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractExtensibleModel; @@ -126,11 +123,6 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter */ private $customOptionValuesFactory; - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -146,7 +138,6 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter * @param ProductCustomOptionValuesInterfaceFactory|null $customOptionValuesFactory * @param array $optionGroups * @param array $optionTypesToGroups - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -163,17 +154,14 @@ public function __construct( array $data = [], ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null, array $optionGroups = [], - array $optionTypesToGroups = [], - CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null + array $optionTypesToGroups = [] ) { $this->productOptionValue = $productOptionValue; $this->optionTypeFactory = $optionFactory; $this->string = $string; $this->validatorPool = $validatorPool; $this->customOptionValuesFactory = $customOptionValuesFactory ?: - ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); - $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? - ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); + \Magento\Framework\App\ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); $this->optionGroups = $optionGroups ?: [ self::OPTION_GROUP_DATE => Date::class, self::OPTION_GROUP_FILE => File::class, @@ -208,7 +196,7 @@ public function __construct( * Get resource instance * * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb - * @deprecated 101.1.0 because resource models should be used directly + * @deprecated 102.0.0 because resource models should be used directly */ protected function _getResource() { @@ -258,7 +246,7 @@ public function getValueById($valueId) * * @param string $type * @return bool - * @since 101.1.0 + * @since 102.0.0 */ public function hasValues($type = null) { @@ -474,12 +462,10 @@ public function afterSave() */ public function getPrice($flag = false) { - if ($flag) { - return $this->calculateCustomOptionCatalogRule->execute( - $this->getProduct(), - (float)$this->getData(self::KEY_PRICE), - $this->getPriceType() === Value::TYPE_PERCENT - ); + if ($flag && $this->getPriceType() == self::$typePercent) { + $basePrice = $this->getProduct()->getPriceInfo()->getPrice(BasePrice::PRICE_CODE)->getValue(); + $price = $basePrice * ($this->_getData(self::KEY_PRICE) / 100); + return $price; } return $this->_getData(self::KEY_PRICE); } @@ -966,7 +952,7 @@ public function setExtensionAttributes( private function getOptionRepository() { if (null === $this->optionRepository) { - $this->optionRepository = ObjectManager::getInstance() + $this->optionRepository = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Catalog\Model\Product\Option\Repository::class); } return $this->optionRepository; @@ -980,7 +966,7 @@ private function getOptionRepository() private function getMetadataPool() { if (null === $this->metadataPool) { - $this->metadataPool = ObjectManager::getInstance() + $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\EntityManager\MetadataPool::class); } return $this->metadataPool; diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php index be7f1921afccf..16fdd4cdeeb1c 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -3,19 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Catalog\Model\Product\Option\Type; +use Magento\Framework\Exception\LocalizedException; use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; -use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; use Magento\Catalog\Model\Product\Option\Value; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Exception\LocalizedException; /** * Catalog product option default type @@ -63,30 +60,21 @@ class DefaultType extends \Magento\Framework\DataObject */ protected $_checkoutSession; - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - /** * Construct * * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param array $data - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - array $data = [], - CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null + array $data = [] ) { $this->_checkoutSession = $checkoutSession; parent::__construct($data); $this->_scopeConfig = $scopeConfig; - $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? ObjectManager::getInstance() - ->get(CalculateCustomOptionCatalogRule::class); } /** @@ -104,12 +92,12 @@ public function setOption($option) /** * Option Instance getter * - * @return Option * @throws \Magento\Framework\Exception\LocalizedException + * @return Option */ public function getOption() { - if ($this->_option instanceof Option) { + if ($this->_option instanceof \Magento\Catalog\Model\Product\Option) { return $this->_option; } throw new LocalizedException(__('The option instance type in options group is incorrect.')); @@ -130,8 +118,8 @@ public function setProduct($product) /** * Product Instance getter * - * @return Product * @throws \Magento\Framework\Exception\LocalizedException + * @return Product */ public function getProduct() { @@ -169,8 +157,7 @@ public function getConfigurationItemOption() */ public function getConfigurationItem() { - if ($this->_getData('configuration_item') instanceof ItemInterface - ) { + if ($this->_getData('configuration_item') instanceof ItemInterface) { return $this->_getData('configuration_item'); } @@ -354,11 +341,7 @@ public function getOptionPrice($optionValue, $basePrice) { $option = $this->getOption(); - return $this->calculateCustomOptionCatalogRule->execute( - $option->getProduct(), - (float)$option->getPrice(), - $option->getPriceType() === Value::TYPE_PERCENT - ); + return $this->_getChargeableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice); } /** @@ -418,8 +401,8 @@ public function getProductOptions() * @param boolean $isPercent Price type - percent or fixed * @param float $basePrice For percent price type * @return float - * @deprecated 102.0.4 typo in method name - * @see CalculateCustomOptionCatalogRule::execute + * @deprecated 102.0.6 typo in method name + * @see _getChargeableOptionPrice */ protected function _getChargableOptionPrice($price, $isPercent, $basePrice) { @@ -433,8 +416,7 @@ protected function _getChargableOptionPrice($price, $isPercent, $basePrice) * @param boolean $isPercent Price type - percent or fixed * @param float $basePrice For percent price type * @return float - * @deprecated - * @see CalculateCustomOptionCatalogRule::execute + * @since 102.0.6 */ protected function _getChargeableOptionPrice($price, $isPercent, $basePrice) { diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php index 9f1eae207e116..77ef8ef4853e1 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php @@ -16,8 +16,9 @@ /** * Catalog product option file type * - * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class File extends \Magento\Catalog\Model\Product\Option\Type\DefaultType { @@ -181,6 +182,7 @@ protected function _getProcessingParams() /** * Returns file info array if we need to get file from already existing file. + * * Or returns null, if we need to get file from uploaded array. * * @return null|array @@ -262,7 +264,6 @@ public function validateUserValue($values) . "Make sure the options are entered and try again." ) ); - break; default: $this->setUserValue(null); break; @@ -330,7 +331,11 @@ public function prepareForCart() public function getFormattedOptionValue($optionValue) { if ($this->_formattedOptionValue === null) { - $value = $this->serializer->unserialize($optionValue); + try { + $value = $this->serializer->unserialize($optionValue); + } catch (\InvalidArgumentException $e) { + return $optionValue; + } if ($value === null) { return $optionValue; } @@ -411,7 +416,7 @@ public function getPrintableOptionValue($optionValue) * @param string $optionValue Prepared for cart option value * @return string * - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ public function getEditableOptionValue($optionValue) { @@ -435,7 +440,7 @@ public function getEditableOptionValue($optionValue) * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ public function parseOptionValue($optionValue, $productOptionValues) { @@ -476,13 +481,13 @@ public function copyQuoteToOrder() try { $value = $this->serializer->unserialize($quoteOption->getValue()); if (!isset($value['quote_path'])) { - throw new \Exception(); + return $this; } $quotePath = $value['quote_path']; $orderPath = $value['order_path']; if (!$this->mediaDirectory->isFile($quotePath) || !$this->mediaDirectory->isReadable($quotePath)) { - throw new \Exception(); + return $this; } if ($this->_coreFileStorageDatabase->checkDbUsage()) { @@ -524,6 +529,8 @@ protected function _getOptionDownloadUrl($route, $params) } /** + * Prepare size + * * @param array $value * @return string */ diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php index 8eebd3e91c2ee..d2766b1bbb054 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -3,13 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Catalog\Model\Product\Option\Type; -use Magento\Catalog\Model\Product\Option\Value; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; /** @@ -41,11 +37,6 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType */ private $singleSelectionTypes; - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - /** * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig @@ -53,7 +44,6 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType * @param \Magento\Framework\Escaper $escaper * @param array $data * @param array $singleSelectionTypes - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, @@ -61,8 +51,7 @@ public function __construct( \Magento\Framework\Stdlib\StringUtils $string, \Magento\Framework\Escaper $escaper, array $data = [], - array $singleSelectionTypes = [], - CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null + array $singleSelectionTypes = [] ) { $this->string = $string; $this->_escaper = $escaper; @@ -72,8 +61,6 @@ public function __construct( 'drop_down' => \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, 'radio' => \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO, ]; - $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? ObjectManager::getInstance() - ->get(CalculateCustomOptionCatalogRule::class); } /** @@ -261,10 +248,10 @@ public function getOptionPrice($optionValue, $basePrice) foreach (explode(',', $optionValue) as $value) { $_result = $option->getValueById($value); if ($_result) { - $result += $this->calculateCustomOptionCatalogRule->execute( - $option->getProduct(), - (float)$_result->getPrice(), - $_result->getPriceType() === Value::TYPE_PERCENT + $result += $this->_getChargeableOptionPrice( + $_result->getPrice(), + $_result->getPriceType() == 'percent', + $basePrice ); } else { if ($this->getListener()) { @@ -276,10 +263,10 @@ public function getOptionPrice($optionValue, $basePrice) } elseif ($this->_isSingleSelection()) { $_result = $option->getValueById($optionValue); if ($_result) { - $result = $this->calculateCustomOptionCatalogRule->execute( - $option->getProduct(), - (float)$_result->getPrice(), - $_result->getPriceType() === Value::TYPE_PERCENT + $result = $this->_getChargeableOptionPrice( + $_result->getPrice(), + $_result->getPriceType() == 'percent', + $basePrice ); } else { if ($this->getListener()) { diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php index 783bda4699792..313513a9151dc 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php @@ -3,16 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Option; -use Magento\Catalog\Pricing\Price\BasePrice; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Model\AbstractModel; +use Magento\Catalog\Pricing\Price\BasePrice; use Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator; use Magento\Catalog\Pricing\Price\RegularPrice; @@ -72,11 +69,6 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu */ private $customOptionPriceCalculator; - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -85,7 +77,6 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param CustomOptionPriceCalculator|null $customOptionPriceCalculator - * @param CalculateCustomOptionCatalogRule|null $CalculateCustomOptionCatalogRule */ public function __construct( \Magento\Framework\Model\Context $context, @@ -94,14 +85,11 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - CustomOptionPriceCalculator $customOptionPriceCalculator = null, - CalculateCustomOptionCatalogRule $CalculateCustomOptionCatalogRule = null + CustomOptionPriceCalculator $customOptionPriceCalculator = null ) { $this->_valueCollectionFactory = $valueCollectionFactory; $this->customOptionPriceCalculator = $customOptionPriceCalculator - ?? ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); - $this->calculateCustomOptionCatalogRule = $CalculateCustomOptionCatalogRule - ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); parent::__construct( $context, @@ -123,7 +111,7 @@ protected function _construct() } /** - * Add value. + * Add value to values array * * @codeCoverageIgnoreStart * @param mixed $value @@ -136,7 +124,7 @@ public function addValue($value) } /** - * Get values. + * Returns array of values * * @return array */ @@ -146,7 +134,7 @@ public function getValues() } /** - * Set values. + * Set values array * * @param array $values * @return $this @@ -158,7 +146,7 @@ public function setValues($values) } /** - * Unset values. + * Unset all from values array * * @return $this */ @@ -169,7 +157,7 @@ public function unsetValues() } /** - * Set option. + * Set option * * @param Option $option * @return $this @@ -181,7 +169,7 @@ public function setOption(Option $option) } /** - * Unset option. + * Unset option * * @return $this */ @@ -192,7 +180,7 @@ public function unsetOption() } /** - * Get option. + * Enter description here... * * @return Option */ @@ -202,7 +190,7 @@ public function getOption() } /** - * Set product. + * Set product * * @param Product $product * @return $this @@ -216,7 +204,7 @@ public function setProduct($product) //@codeCoverageIgnoreEnd /** - * Get product. + * Get product * * @return Product */ @@ -229,10 +217,9 @@ public function getProduct() } /** - * Save values. + * Save array of values * * @return $this - * @throws \Exception */ public function saveValues() { @@ -258,9 +245,7 @@ public function saveValues() } /** - * Return price. - * - * If $flag is true and price is percent return converted percent to price + * Return price. If $flag is true and price is percent return converted percent to price * * @param bool $flag * @return float|int @@ -268,11 +253,7 @@ public function saveValues() public function getPrice($flag = false) { if ($flag) { - return $this->calculateCustomOptionCatalogRule->execute( - $this->getProduct(), - (float)$this->getData(self::KEY_PRICE), - $this->getPriceType() === self::TYPE_PERCENT - ); + return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, BasePrice::PRICE_CODE); } return $this->_getData(self::KEY_PRICE); } @@ -288,7 +269,7 @@ public function getRegularPrice() } /** - * Get values collection. + * Enter description here... * * @param Option $option * @return \Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection @@ -306,7 +287,7 @@ public function getValuesCollection(Option $option) } /** - * Get values by option. + * Returns values by option * * @param array $optionIds * @param int $option_id @@ -327,7 +308,7 @@ public function getValuesByOption($optionIds, $option_id, $store_id) } /** - * Delete value. + * Delete value by option * * @param int $option_id * @return $this @@ -339,7 +320,7 @@ public function deleteValue($option_id) } /** - * Delete values. + * Delete values by option * * @param int $option_type_id * @return $this 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/Product/Price/Validation/Result.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php index 3d4d9f607da48..5b9b1c5e4816a 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php +++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php @@ -10,7 +10,7 @@ * Validation Result is used to aggregate errors that occurred during price update. * * @api - * @since 101.1.0 + * @since 102.0.0 */ class Result { @@ -43,7 +43,7 @@ public function __construct( * @param array $parameters (optional). Placeholder values in ['placeholder key' => 'placeholder value'] format * for failure reason message. * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function addFailedItem($id, $message, array $parameters = []) { @@ -57,7 +57,7 @@ public function addFailedItem($id, $message, array $parameters = []) * Get ids of rows, that contained errors during price update. * * @return int[] - * @since 101.1.0 + * @since 102.0.0 */ public function getFailedRowIds() { @@ -68,7 +68,7 @@ public function getFailedRowIds() * Get price update errors, that occurred during price update. * * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function getFailedItems() { @@ -83,6 +83,12 @@ public function getFailedItems() } } + /** + * Clear validation messages to prevent wrong validation for subsequent price update. + * Work around for backward compatible changes. + */ + $this->failedItems = []; + return $failedItems; } } diff --git a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php b/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php deleted file mode 100644 index 404760a51eff5..0000000000000 --- a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Model\Product; - -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Store\Model\StoreManagerInterface; - -/** - * Class to check that product is saleable. - */ -class SalabilityChecker -{ - /** - * @var ProductRepositoryInterface - */ - private $productRepository; - - /** - * @var StoreManagerInterface - */ - private $storeManager; - - /** - * @param ProductRepositoryInterface $productRepository - * @param StoreManagerInterface $storeManager - */ - public function __construct( - ProductRepositoryInterface $productRepository, - StoreManagerInterface $storeManager - ) { - $this->productRepository = $productRepository; - $this->storeManager = $storeManager; - } - - /** - * Check if product is salable. - * - * @param int|string $productId - * @param int|null $storeId - * @return bool - */ - public function isSalable($productId, $storeId = null): bool - { - if ($storeId === null) { - $storeId = $this->storeManager->getStore()->getId(); - } - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->productRepository->getById($productId, false, $storeId); - - return $product->isSalable(); - } -} diff --git a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php index 16bdec2533fe6..d14984d3d3478 100644 --- a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php +++ b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php @@ -12,11 +12,12 @@ use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\TemporaryStateExceptionInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\ScopeInterface; /** - * Product tier price management - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TierPriceManagement implements \Magento\Catalog\Api\ProductTierPriceManagementInterface @@ -100,7 +101,7 @@ public function add($sku, $customerGroupId, $price, $qty) $product = $this->productRepository->get($sku, ['edit_mode' => true]); $tierPrices = $product->getData('tier_price'); $websiteIdentifier = 0; - $value = $this->config->getValue('catalog/price/scope', \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITE); + $value = $this->config->getValue('catalog/price/scope', ScopeInterface::SCOPE_WEBSITE); if ($value != 0) { $websiteIdentifier = $this->storeManager->getWebsite()->getId(); } @@ -160,9 +161,8 @@ public function remove($sku, $customerGroupId, $qty) { $product = $this->productRepository->get($sku, ['edit_mode' => true]); $websiteIdentifier = 0; - $value = $this->config->getValue('catalog/price/scope', \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITE); - if ($value != 0) { - $websiteIdentifier = $this->storeManager->getWebsite()->getId(); + if ($this->getPriceScopeConfig() !== 0) { + $websiteIdentifier = $this->getCurrentWebsite()->getId(); } $this->priceModifier->removeTierPrice($product, $customerGroupId, $qty, $websiteIdentifier); return true; @@ -175,23 +175,17 @@ public function getList($sku, $customerGroupId) { $product = $this->productRepository->get($sku, ['edit_mode' => true]); - $priceKey = 'website_price'; - $value = $this->config->getValue('catalog/price/scope', \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITE); - if ($value == 0) { - $priceKey = 'price'; - } - - $cgi = ($customerGroupId === 'all' + $cgi = $customerGroupId === 'all' ? $this->groupManagement->getAllCustomersGroup()->getId() - : $customerGroupId); + : $customerGroupId; $prices = []; $tierPrices = $product->getData('tier_price'); if ($tierPrices !== null) { + $priceKey = $this->getPriceKey(); + foreach ($tierPrices as $price) { - if ((is_numeric($customerGroupId) && (int) $price['cust_group'] === (int) $customerGroupId) - || ($customerGroupId === 'all' && $price['all_groups']) - ) { + if ($this->isCustomerGroupApplicable($customerGroupId, $price)) { /** @var \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice */ $tierPrice = $this->priceFactory->create(); $tierPrice->setValue($price[$priceKey]) @@ -203,4 +197,48 @@ public function getList($sku, $customerGroupId) } return $prices; } + + /** + * Returns attribute code (key) that contains price + * + * @return string + */ + private function getPriceKey(): string + { + return $this->getPriceScopeConfig() === 0 ? 'price' : 'website_price'; + } + + /** + * Returns whether Price is applicable for provided Customer Group + * + * @param string $customerGroupId + * @param array $priceArray + * @return bool + */ + private function isCustomerGroupApplicable(string $customerGroupId, array $priceArray): bool + { + return ($customerGroupId === 'all' && $priceArray['all_groups']) + || (is_numeric($customerGroupId) && (int)$priceArray['cust_group'] === (int)$customerGroupId); + } + + /** + * Returns current Price Scope configuration value + * + * @return int + */ + private function getPriceScopeConfig(): int + { + return (int)$this->config->getValue('catalog/price/scope', ScopeInterface::SCOPE_WEBSITE); + } + + /** + * Returns current Website object + * + * @return WebsiteInterface + * @throws LocalizedException + */ + private function getCurrentWebsite(): WebsiteInterface + { + return $this->storeManager->getWebsite(); + } } diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php index e6804d9246faa..eb4a71cb90a8c 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php +++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php @@ -9,15 +9,18 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\ObjectManager; /** - * @api * Abstract model for product type implementation + * + * phpcs:disable Magento2.Classes.AbstractApi + * @api + * @since 100.0.2 * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @since 100.0.2 */ abstract class AbstractType { @@ -167,7 +170,7 @@ abstract public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $ * Serializer interface instance. * * @var \Magento\Framework\Serialize\Serializer\Json - * @since 101.1.0 + * @since 102.0.0 */ protected $serializer; @@ -207,7 +210,7 @@ public function __construct( $this->_filesystem = $filesystem; $this->_logger = $logger; $this->productRepository = $productRepository; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() + $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); } @@ -355,6 +358,7 @@ public function isSalable($product) /** * Prepare product and its configuration to be added to some products list. + * * Perform standard preparation process and then prepare options belonging to specific product type. * * @param \Magento\Framework\DataObject $buyRequest @@ -440,6 +444,7 @@ public function processConfiguration( /** * Initialize product(s) for add to cart process. + * * Advanced version of func to prepare product for cart - processMode can be specified there. * * @param \Magento\Framework\DataObject $buyRequest @@ -476,6 +481,7 @@ public function prepareForCart(\Magento\Framework\DataObject $buyRequest, $produ * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * phpcs:disable Generic.Metrics.NestingLevel */ public function processFileQueue() { @@ -492,6 +498,7 @@ public function processFileQueue() /** @var $uploader \Zend_File_Transfer_Adapter_Http */ $uploader = isset($queueOptions['uploader']) ? $queueOptions['uploader'] : null; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $path = dirname($dst); try { @@ -529,9 +536,11 @@ public function processFileQueue() return $this; } + //phpcs:enable /** * Add file to File Queue + * * @param array $queueOptions Array of File Queue * (eg. ['operation'=>'move', * 'src_name'=>'filename', @@ -572,6 +581,7 @@ public function getSpecifyOptionMessage() * @param string $processMode * @return array * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _prepareOptions(\Magento\Framework\DataObject $buyRequest, $product, $processMode) { @@ -583,6 +593,7 @@ protected function _prepareOptions(\Magento\Framework\DataObject $buyRequest, $p } if ($options !== null) { $results = []; + $optionsFromRequest = $buyRequest->getOptions(); foreach ($options as $option) { /* @var $option \Magento\Catalog\Model\Product\Option */ try { @@ -590,8 +601,14 @@ protected function _prepareOptions(\Magento\Framework\DataObject $buyRequest, $p ->setOption($option) ->setProduct($product) ->setRequest($buyRequest) - ->setProcessMode($processMode) - ->validateUserValue($buyRequest->getOptions()); + ->setProcessMode($processMode); + + if ($product->getSkipCheckRequiredOption() !== true) { + $group->validateUserValue($optionsFromRequest); + } elseif ($optionsFromRequest !== null && isset($optionsFromRequest[$option->getId()])) { + $transport->options[$option->getId()] = $optionsFromRequest[$option->getId()]; + } + } catch (LocalizedException $e) { $results[] = $e->getMessage(); continue; @@ -643,8 +660,7 @@ public function checkProductBuyState($product) } /** - * Prepare additional options/information for order item which will be - * created from this product + * Prepare additional options/information for order item which will be created from this product * * @param \Magento\Catalog\Model\Product $product * @return array @@ -900,7 +916,7 @@ public function getStoreFilter($product) /** * Set store filter for associated products * - * @param $store int|\Magento\Store\Model\Store + * @param int|\Magento\Store\Model\Store $store * @param \Magento\Catalog\Model\Product $product * @return $this */ @@ -913,6 +929,7 @@ public function setStoreFilter($store, $product) /** * Allow for updates of children qty's + * * (applicable for complicated product types. As default returns false) * * @param \Magento\Catalog\Model\Product $product @@ -940,6 +957,7 @@ public function prepareQuoteItemQty($qty, $product) /** * Implementation of product specify logic of which product needs to be assigned to option. + * * For example if product which was added to option already removed from catalog. * * @param \Magento\Catalog\Model\Product $optionProduct @@ -979,6 +997,7 @@ public function setConfig($config) /** * Retrieve additional searchable data from type instance + * * Using based on product id and store_id data * * @param \Magento\Catalog\Model\Product $product @@ -999,6 +1018,7 @@ public function getSearchableData($product) /** * Retrieve products divided into groups required to purchase + * * At least one product in each group has to be purchased * * @param \Magento\Catalog\Model\Product $product @@ -1092,6 +1112,8 @@ public function getIdentities(\Magento\Catalog\Model\Product $product) } /** + * Get Associated Products + * * @param \Magento\Catalog\Model\Product\Type\AbstractType $product * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -1106,7 +1128,7 @@ public function getAssociatedProducts($product) * * @param \Magento\Catalog\Model\Product $product * @return bool - * @since 101.1.0 + * @since 101.0.11 */ public function isPossibleBuyFromList($product) { diff --git a/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php index dabfdb74f0118..a692ec5d463d0 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php +++ b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php @@ -18,7 +18,7 @@ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * - * @deprecated + * @deprecated 103.0.2 * @see \Magento\Catalog\Model\Product\Type\Price */ class FrontSpecialPrice extends Price @@ -70,7 +70,7 @@ public function __construct( /** * @inheritdoc * - * @deprecated + * @deprecated 103.0.2 */ protected function _applySpecialPrice($product, $finalPrice) { diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index e702965270639..74a6c7f634f81 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -484,6 +484,7 @@ public function getTierPriceCount($product) * @param Product $product * * @return array|float + * @since 102.0.6 */ public function getFormattedTierPrice($qty, $product) { @@ -509,7 +510,7 @@ public function getFormattedTierPrice($qty, $product) * * @return array|float * - * @deprecated + * @deprecated 102.0.6 * @see getFormattedTierPrice() */ public function getFormatedTierPrice($qty, $product) @@ -522,6 +523,7 @@ public function getFormatedTierPrice($qty, $product) * * @param Product $product * @return array|float + * @since 102.0.6 */ public function getFormattedPrice($product) { @@ -534,7 +536,7 @@ public function getFormattedPrice($product) * @param Product $product * @return array || float * - * @deprecated + * @deprecated 102.0.6 * @see getFormattedPrice() */ public function getFormatedPrice($product) diff --git a/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php b/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php index 91570b58b7328..521d53629d99c 100644 --- a/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php +++ b/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php @@ -8,7 +8,7 @@ /** * Product ID locator provides all product IDs by SKU. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ProductIdLocatorInterface { @@ -17,7 +17,7 @@ interface ProductIdLocatorInterface * * @param array $skus * @return array - * @since 101.1.0 + * @since 102.0.0 */ public function retrieveProductIdsBySkus(array $skus); } diff --git a/app/code/Magento/Catalog/Model/ProductLink/Repository.php b/app/code/Magento/Catalog/Model/ProductLink/Repository.php index 960044efbc2ec..7a0b224a6153a 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/Repository.php +++ b/app/code/Magento/Catalog/Model/ProductLink/Repository.php @@ -54,14 +54,14 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface /** * @var CollectionProvider - * @deprecated Not used anymore. + * @deprecated 103.0.4 Not used anymore. * @see query */ protected $entityCollectionProvider; /** * @var LinksInitializer - * @deprecated Not used. + * @deprecated 103.0.4 Not used. */ protected $linkInitializer; @@ -77,14 +77,14 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface /** * @var ProductLinkInterfaceFactory - * @deprecated Not used anymore, search delegated. + * @deprecated 103.0.4 Not used anymore, search delegated. * @see getList() */ protected $productLinkFactory; /** * @var ProductLinkExtensionFactory - * @deprecated Not used anymore, search delegated. + * @deprecated 103.0.4 Not used anymore, search delegated. * @see getList() */ protected $productLinkExtensionFactory; diff --git a/app/code/Magento/Catalog/Model/ProductNotFoundPageCacheTags.php b/app/code/Magento/Catalog/Model/ProductNotFoundPageCacheTags.php new file mode 100644 index 0000000000000..685e9a69a0f8a --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductNotFoundPageCacheTags.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\PageCache\Model\Spi\PageCacheTagsPreprocessorInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Add product identities to "noroute" page + * + * Ensure that "noroute" page has necessary product tags + * so it can be invalidated once the product becomes visible again + */ +class ProductNotFoundPageCacheTags implements PageCacheTagsPreprocessorInterface +{ + private const NOROUTE_ACTION_NAME = 'cms_noroute_index'; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** + * @var RequestInterface + */ + private $request; + + /** + * @param RequestInterface $request + * @param ProductRepositoryInterface $productRepository + * @param StoreManagerInterface $storeManager + */ + public function __construct( + RequestInterface $request, + ProductRepositoryInterface $productRepository, + StoreManagerInterface $storeManager + ) { + $this->productRepository = $productRepository; + $this->storeManager = $storeManager; + $this->request = $request; + } + + /** + * @inheritDoc + */ + public function process(array $tags): array + { + if ($this->request->getFullActionName() === self::NOROUTE_ACTION_NAME) { + try { + $productId = (int) $this->request->getParam('id'); + $product = $this->productRepository->getById( + $productId, + false, + $this->storeManager->getStore()->getId() + ); + } catch (NoSuchEntityException $e) { + $product = null; + } + if ($product) { + $tags = array_merge($tags, $product->getIdentities()); + } + } + return $tags; + } +} diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index d656a0a9ac5b4..fefeafe46e1c4 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -30,7 +30,8 @@ use Magento\Framework\Exception\ValidatorException; /** - * Product Repository. + * @inheritdoc + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ @@ -122,14 +123,14 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa protected $fileSystem; /** - * @deprecated + * @deprecated 103.0.2 * * @var ImageContentInterfaceFactory */ protected $contentFactory; /** - * @deprecated + * @deprecated 103.0.2 * * @var ImageProcessorInterface */ @@ -141,7 +142,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa protected $extensionAttributesJoinProcessor; /** - * @deprecated + * @deprecated 103.0.2 * * @var \Magento\Catalog\Model\Product\Gallery\Processor */ @@ -404,7 +405,7 @@ private function assignProductToWebsites(\Magento\Catalog\Model\Product $product /** * Process new gallery media entry. * - * @deprecated + * @deprecated 103.0.2 * @see MediaGalleryProcessor::processNewMediaGalleryEntry() * * @param ProductInterface $product @@ -543,7 +544,9 @@ public function save(ProductInterface $product, $saveOptions = false) if (!$ignoreLinksFlag && $ignoreLinksFlag !== null) { $productLinks = $product->getProductLinks(); } - $productDataArray['store_id'] = (int)$this->storeManager->getStore()->getId(); + if (!isset($productDataArray['store_id'])) { + $productDataArray['store_id'] = (int) $this->storeManager->getStore()->getId(); + } $product = $this->initializeProductData($productDataArray, empty($existingProduct)); $this->processLinks($product, $productLinks); @@ -669,7 +672,7 @@ private function addExtensionAttributes(Collection $collection) : Collection /** * Helper function that adds a FilterGroup to the collection. * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup * @param Collection $collection * @return void @@ -728,13 +731,14 @@ private function getMediaGalleryProcessor() /** * Retrieve collection processor * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() { if (!$this->collectionProcessor) { $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( + // phpstan:ignore "Class Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor not found." \Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor::class ); } 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/AggregateCount.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php index fab2441db26c9..939f9d354af85 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php @@ -8,11 +8,15 @@ use Magento\Catalog\Model\Category; /** + * Aggregate count for parent category after deleting child category + * * Class AggregateCount */ class AggregateCount { /** + * Reduces children count for parent categories + * * @param Category $category * @return void */ @@ -25,9 +29,7 @@ public function processDelete(Category $category) */ $parentIds = $category->getParentIds(); if ($parentIds) { - $childDecrease = $category->getChildrenCount() + 1; - // +1 is itself - $data = ['children_count' => new \Zend_Db_Expr('children_count - ' . $childDecrease)]; + $data = ['children_count' => new \Zend_Db_Expr('children_count - 1')]; $where = ['entity_id IN(?)' => $parentIds]; $resourceModel->getConnection()->update($resourceModel->getEntityTable(), $data, $where); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 4711828d8f78d..351e3314c9fb4 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -456,6 +456,7 @@ public function addRootLevelFilter() * Add navigation max depth filter * * @return $this + * @since 103.0.0 */ public function addNavigationMaxDepthFilter() { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php index 05950531e2178..759866de4b49d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php @@ -69,7 +69,7 @@ class Flat extends \Magento\Indexer\Model\ResourceModel\AbstractResource * Category collection factory * * @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory - * @deprecated 100.0.12 + * @deprecated 100.0.2 */ protected $_categoryCollectionFactory; @@ -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/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index e1c90017327cd..d1769ded93d29 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -258,6 +258,7 @@ public function afterSave() * Is attribute enabled for flat indexing * * @return bool + * @since 103.0.0 */ public function isEnabledInFlat() { @@ -874,7 +875,7 @@ public function __wakeup() /** * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ public function setIsUsedInGrid($isUsedInGrid) { @@ -884,7 +885,7 @@ public function setIsUsedInGrid($isUsedInGrid) /** * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ public function setIsVisibleInGrid($isVisibleInGrid) { @@ -894,7 +895,7 @@ public function setIsVisibleInGrid($isVisibleInGrid) /** * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ public function setIsFilterableInGrid($isFilterableInGrid) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php index 585da2af529a4..ab9f0e76854a6 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php @@ -410,6 +410,7 @@ protected function _construct() /** * {@inheritdoc} * @return string + * @since 102.0.6 */ public function getMainTable() { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index c5587d3b25665..b174e4beb6353 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -180,7 +180,7 @@ public function getProductWebsiteTable() /** * Product Category table name getter * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return string */ public function getProductCategoryTable() @@ -204,7 +204,7 @@ protected function _getDefaultAttributes() /** * Retrieve product website identifiers * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @param \Magento\Catalog\Model\Product|int $product * @return array */ @@ -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); } @@ -379,7 +383,7 @@ public function delete($object) /** * Save product website relations * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @param \Magento\Catalog\Model\Product $product * @return $this */ @@ -408,7 +412,7 @@ protected function _saveWebsiteIds($product) * @param DataObject $object * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ protected function _saveCategories(DataObject $object) { @@ -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); } @@ -776,7 +782,7 @@ private function getEntityManager() /** * Retrieve ProductWebsiteLink instance. * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return ProductWebsiteLink */ private function getProductWebsiteLink() @@ -787,7 +793,7 @@ private function getProductWebsiteLink() /** * Retrieve CategoryLink instance. * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return Product\CategoryLink */ private function getProductCategoryLink() @@ -805,7 +811,7 @@ private function getProductCategoryLink() * Store id is required to correctly identify attribute value we are working with. * * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ protected function getAttributeRow($entity, $object, $attribute) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php index cf5760b0c33a9..8d03eb3ccafc9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php @@ -147,7 +147,7 @@ private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositi * @param bool $insert * @return array */ - private function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false) + public function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false) { if (empty($insertLinks)) { return []; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 6e1efbc9db003..7dbfe0d5fccea 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -731,6 +731,7 @@ protected function _afterLoad() * Add Store ID to products from collection. * * @return $this + * @since 102.0.8 */ protected function prepareStoreId() { @@ -857,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 @@ -1357,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; } @@ -1365,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; @@ -2164,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); @@ -2220,7 +2222,7 @@ public function addTierPriceData() * * @param int $customerGroupId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function addTierPriceDataByGroupId($customerGroupId) { @@ -2400,7 +2402,7 @@ function ($item) use ($linkField) { * Get product entity metadata * * @return \Magento\Framework\EntityManager\EntityMetadataInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getProductEntityMetadata() { 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/CompositeProductBatchSizeAdjusterInterface.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductBatchSizeAdjusterInterface.php index c0e8858b4cfa8..2d282c5bf9741 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductBatchSizeAdjusterInterface.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductBatchSizeAdjusterInterface.php @@ -9,7 +9,7 @@ /** * Correct batch size according to number of composite related items. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CompositeProductBatchSizeAdjusterInterface { @@ -18,7 +18,7 @@ interface CompositeProductBatchSizeAdjusterInterface * * @param int $batchSize * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function adjust($batchSize); } 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 b64cca4ff1b26..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 @@ -18,7 +18,7 @@ * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 - * @deprecated Not used anymore for price indexation. Class left for backward compatibility + * @deprecated 102.0.6 Not used anymore for price indexation. Class left for backward compatibility * @see DimensionalIndexerInterface */ class DefaultPrice extends AbstractIndexer implements PriceInterface @@ -240,7 +240,7 @@ protected function _getDefaultFinalPriceTable() * Prepare final price temporary index table * * @return $this - * @deprecated + * @deprecated 102.0.5 * @see prepareFinalPriceTable() */ protected function _prepareDefaultFinalPriceTable() @@ -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/SimpleProductPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php index 5a055e5ed9603..47365929e159e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php @@ -62,7 +62,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function executeByDimensions(array $dimensions, \Traversable $entityIds) { @@ -81,8 +81,7 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) 'tierPriceField' => 'tier_price', ]); $select = $this->baseFinalPrice->getQuery($dimensions, $this->productType, iterator_to_array($entityIds)); - $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false); - $this->tableMaintainer->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []); $this->basePriceModifier->modifyPrice($temporaryPriceTable, iterator_to_array($entityIds)); } 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/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php b/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php index 0b49ef8796ce6..d398c9c14787f 100644 --- a/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php +++ b/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php @@ -98,7 +98,7 @@ public function __construct( * Get instance of ScopePool * * @return \Magento\Framework\App\Config - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ private function getAppConfig() { @@ -140,7 +140,7 @@ public function afterSave() /** * {@inheritdoc} - * @since 101.1.0 + * @since 102.0.0 */ public function afterDeleteCommit() { diff --git a/app/code/Magento/Catalog/Model/Template/Filter.php b/app/code/Magento/Catalog/Model/Template/Filter.php index 8cd61415b958a..0a46af3ef021d 100644 --- a/app/code/Magento/Catalog/Model/Template/Filter.php +++ b/app/code/Magento/Catalog/Model/Template/Filter.php @@ -14,8 +14,6 @@ /** * Work with catalog(store, website) urls - * - * @package Magento\Catalog\Model\Template */ class Filter extends \Magento\Framework\Filter\Template { @@ -30,6 +28,7 @@ class Filter extends \Magento\Framework\Filter\Template * Whether to allow SID in store directive: NO * * @var bool + * @deprecated SID query parameter is not used in URLs anymore. */ protected $_useSessionInUrl = false; @@ -81,10 +80,14 @@ public function setUseAbsoluteLinks($flag) * * @param bool $flag * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @deprecated SID query parameter is not used in URLs anymore. */ public function setUseSessionInUrl($flag) { - $this->_useSessionInUrl = $flag; + // phpcs:disable Magento2.Functions.DiscouragedFunction + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); + return $this; } @@ -126,6 +129,7 @@ public function viewDirective($construction) */ public function mediaDirective($construction) { + // phpcs:disable Magento2.Functions.DiscouragedFunction $params = $this->getParameters(html_entity_decode($construction[2], ENT_QUOTES)); return $this->_storeManager->getStore() ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . $params['url']; diff --git a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php deleted file mode 100644 index b3f3ac7bf68ef..0000000000000 --- a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Pricing\Price; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\PriceModifierInterface; -use Magento\CatalogRule\Pricing\Price\CatalogRulePrice; -use Magento\Framework\Pricing\Price\BasePriceProviderInterface; -use Magento\Framework\Pricing\PriceCurrencyInterface; - -/** - * Calculates prices of custom options of the product with catalog rules applied. - */ -class CalculateCustomOptionCatalogRule -{ - /** - * @var PriceCurrencyInterface - */ - private $priceCurrency; - - /** - * @var PriceModifierInterface - */ - private $priceModifier; - - /** - * @param PriceCurrencyInterface $priceCurrency - * @param PriceModifierInterface $priceModifier - */ - public function __construct( - PriceCurrencyInterface $priceCurrency, - PriceModifierInterface $priceModifier - ) { - $this->priceModifier = $priceModifier; - $this->priceCurrency = $priceCurrency; - } - - /** - * Calculate prices of custom options of the product with catalog rules applied. - * - * @param Product $product - * @param float $optionPriceValue - * @param bool $isPercent - * @return float - */ - public function execute( - Product $product, - float $optionPriceValue, - bool $isPercent - ): float { - $regularPrice = (float)$product->getPriceInfo() - ->getPrice(RegularPrice::PRICE_CODE) - ->getValue(); - $catalogRulePrice = $this->priceModifier->modifyPrice( - $regularPrice, - $product - ); - $basePriceWithOutCatalogRules = (float)$this->getGetBasePriceWithOutCatalogRules($product); - // Apply catalog price rules to product options only if catalog price rules are applied to product. - if ($catalogRulePrice < $basePriceWithOutCatalogRules) { - $optionPrice = $this->getOptionPriceWithoutPriceRule($optionPriceValue, $isPercent, $regularPrice); - $totalCatalogRulePrice = $this->priceModifier->modifyPrice( - $regularPrice + $optionPrice, - $product - ); - $finalOptionPrice = $totalCatalogRulePrice - $catalogRulePrice; - } else { - $finalOptionPrice = $this->getOptionPriceWithoutPriceRule( - $optionPriceValue, - $isPercent, - $this->getGetBasePriceWithOutCatalogRules($product) - ); - } - - return $this->priceCurrency->convertAndRound($finalOptionPrice); - } - - /** - * Get product base price without catalog rules applied. - * - * @param Product $product - * @return float - */ - private function getGetBasePriceWithOutCatalogRules(Product $product): float - { - $basePrice = null; - foreach ($product->getPriceInfo()->getPrices() as $price) { - if ($price instanceof BasePriceProviderInterface - && $price->getPriceCode() !== CatalogRulePrice::PRICE_CODE - && $price->getValue() !== false - ) { - $basePrice = min( - $price->getValue(), - $basePrice ?? $price->getValue() - ); - } - } - - return $basePrice ?? $product->getPrice(); - } - - /** - * Calculate option price without catalog price rule discount. - * - * @param float $optionPriceValue - * @param bool $isPercent - * @param float $basePrice - * @return float - */ - private function getOptionPriceWithoutPriceRule(float $optionPriceValue, bool $isPercent, float $basePrice): float - { - return $isPercent ? $basePrice * $optionPriceValue / 100 : $optionPriceValue; - } -} diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php index 6ec282e45a1a0..c1af0b41741df 100644 --- a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php @@ -65,7 +65,7 @@ public function setItem(ItemInterface $item) /** * Get value of configured options. * - * @deprecated ConfiguredOptions::getItemOptionsValue is used instead + * @deprecated 102.0.4 ConfiguredOptions::getItemOptionsValue is used instead * @return float */ protected function getOptionsValue(): float diff --git a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php index f250927889c29..1aa43a39af442 100644 --- a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php @@ -29,7 +29,7 @@ class TierPrice extends AbstractPrice implements TierPriceInterface, BasePricePr /** * @var Session - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ protected $customerSession; diff --git a/app/code/Magento/Catalog/Setup/Patch/Data/UpdateProductDescriptionOrder.php b/app/code/Magento/Catalog/Setup/Patch/Data/UpdateProductDescriptionOrder.php new file mode 100644 index 0000000000000..db113448eb238 --- /dev/null +++ b/app/code/Magento/Catalog/Setup/Patch/Data/UpdateProductDescriptionOrder.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Setup\Patch\Data; + +use Magento\Catalog\Setup\CategorySetup; +use Magento\Catalog\Setup\CategorySetupFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Reorder Short Description/Description Product Attributes + */ +class UpdateProductDescriptionOrder implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var CategorySetupFactory + */ + private $categorySetupFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param CategorySetupFactory $categorySetupFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + CategorySetupFactory $categorySetupFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->categorySetupFactory = $categorySetupFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var CategorySetup $categorySetup */ + $categorySetup = $this->categorySetupFactory->create(['setup' => $this->moduleDataSetup]); + $entityTypeId = $categorySetup->getEntityTypeId(\Magento\Catalog\Model\Product::ENTITY); + + // Content + $categorySetup->updateAttribute( + $entityTypeId, + 'short_description', + 'frontend_label', + 'Short Description', + 100 + ); + $categorySetup->updateAttribute( + $entityTypeId, + 'description', + 'frontend_label', + 'Description', + 110 + ); + + return $this; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + UpdateMediaAttributesBackendTypes::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml new file mode 100644 index 0000000000000..020fb27063be7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.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="AdminChangeCategoryNameActionGroup"> + <annotations> + <description>Switch the Storefront to the provided Store.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string" defaultValue="{{_defaultCategory.name}}"/> + </arguments> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryName}}" stepKey="updateCategoryName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml new file mode 100644 index 0000000000000..14a7967422332 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminChangeCategoryNameOnStoreViewLevelActionGroup"> + <annotations> + <description>Updates the Category Name for proper Store View.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryName}}" stepKey="changeNameField"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCategoryWithInactiveIncludeInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCategoryWithInactiveIncludeInMenuActionGroup.xml new file mode 100644 index 0000000000000..c407e9ba829d7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCategoryWithInactiveIncludeInMenuActionGroup.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="AdminCreateCategoryWithInactiveIncludeInMenuActionGroup" extends="CreateCategoryActionGroup"> + <annotations> + <description>EXTENDS: CreateCategory. Add "disableIncludeInMenuOption" step.</description> + </annotations> + <arguments> + <argument name="categoryEntity" defaultValue="_defaultCategory"/> + </arguments> + + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIncludeInMenuOption" + after="seeCategoryPageTitle"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateInactiveCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateInactiveCategoryActionGroup.xml new file mode 100644 index 0000000000000..b16ff59b91329 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateInactiveCategoryActionGroup.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="AdminCreateInactiveCategoryActionGroup" extends="CreateCategoryActionGroup"> + <annotations> + <description>EXTENDS: CreateCategory. Add "disableCategory" step.</description> + </annotations> + <arguments> + <argument name="categoryEntity" defaultValue="_defaultCategory"/> + </arguments> + + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory" + after="seeCategoryPageTitle"/> +</actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductsFromGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductsFromGridActionGroup.xml new file mode 100644 index 0000000000000..d5c4f97c88da2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductsFromGridActionGroup.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="AdminDeleteAllProductsFromGridActionGroup"> + <annotations> + <description>Select and delete products in product grid.</description> + </annotations> + <click selector="{{AdminProductFiltersSection.allCheckbox}}" stepKey="selectAllProducts"/> + <click selector="{{AdminProductFiltersSection.actions}}" stepKey="clickOnActionsChangingView"/> + <click selector="{{AdminProductFiltersSection.delete}}" stepKey="clickDelete"/> + <click selector="//button[@class='action-primary action-accept']" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitingProductGridLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml new file mode 100644 index 0000000000000..bd7eb664819dd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.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="AdminEnableCategoryActionGroup"> + <annotations> + <description>Enable the category</description> + </annotations> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="enableCategory"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetGridPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetGridPageActionGroup.xml index c6f0c3332b1d5..38193fe547e52 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetGridPageActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetGridPageActionGroup.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="AdminOpenAttributeSetGridPageActionGroup"> + <annotations> + <description>Open the Attribute Sets grid page.</description> + </annotations> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSetPage"/> <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenCatalogProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenCatalogProductPageActionGroup.xml new file mode 100644 index 0000000000000..0e606b00d5913 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenCatalogProductPageActionGroup.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="AdminOpenCatalogProductPageActionGroup"> + <annotations> + <description>Open page with product grid.</description> + </annotations> + + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="openCatalogProductPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml index ca1303f180ca4..153227e462f32 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.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="AdminOpenProductIndexPageActionGroup"> + <annotations> + <description>Go to products grid page.</description> + </annotations> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndexPage"/> <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSaveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSaveActionGroup.xml new file mode 100644 index 0000000000000..57d4a6c702c89 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSaveActionGroup.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="AdminProductAttributeSaveActionGroup"> + <annotations> + <description>Clicks on Save button to save the attribute and check success message.</description> + </annotations> + + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <waitForElementVisible selector="{{AdminMainActionsSection.save}}" stepKey="waitForSaveButton"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product attribute." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCatalogPageOpenActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCatalogPageOpenActionGroup.xml new file mode 100644 index 0000000000000..f25f73977bf4e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCatalogPageOpenActionGroup.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="AdminProductCatalogPageOpenActionGroup"> + <annotations> + <description>Goes to the Admin Product Catalog Page grid page.</description> + </annotations> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryExistInCategoryListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryExistInCategoryListActionGroup.xml new file mode 100644 index 0000000000000..c9ad309dcadc1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryExistInCategoryListActionGroup.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="AdminProductFormCategoryExistInCategoryListActionGroup"> + <annotations> + <description>Check Category exist in Category list for Assign to Product.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="{{categoryName}}" + stepKey="fillSearchCategory"/> + <see selector="{{AdminProductFormSection.selectCategory(categoryName)}}" userInput="{{categoryName}}" + stepKey="seeCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryNotExistInCategoryListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryNotExistInCategoryListActionGroup.xml new file mode 100644 index 0000000000000..fb0717fe173af --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryNotExistInCategoryListActionGroup.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="AdminProductFormCategoryNotExistInCategoryListActionGroup"> + <annotations> + <description>Check Category not exist in Category list for Assign to Product.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="{{categoryName}}" + stepKey="fillSearchCategory"/> + <dontSee selector="{{AdminProductFormSection.selectCategory(categoryName)}}" userInput="{{categoryName}}" + stepKey="seeCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveActionGroup.xml new file mode 100644 index 0000000000000..3db88bf3054bf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveActionGroup.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="AdminProductFormSaveActionGroup"> + <annotations> + <description>Click save button for saving product.</description> + </annotations> + + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <waitForPageLoad stepKey="waitForProductSaving"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveButtonClickActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveButtonClickActionGroup.xml new file mode 100644 index 0000000000000..cb481c43c5484 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveButtonClickActionGroup.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="AdminProductFormSaveButtonClickActionGroup"> + <annotations> + <description>Click Save button of product form.</description> + </annotations> + + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForProductSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridSectionClickFirstRowActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridSectionClickFirstRowActionGroup.xml new file mode 100644 index 0000000000000..da71e7816ef0f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridSectionClickFirstRowActionGroup.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="AdminProductGridSectionClickFirstRowActionGroup"> + <annotations> + <description>Click first row on the product grid page.</description> + </annotations> + + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductAttributeActionGroup.xml index 956dc3bf6fa52..66e08b222b60e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductAttributeActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminSaveProductAttributeActionGroup"> <annotations> - <description>Clicks on Save button to save the attribute.</description> + <description>DEPRECATED. Use AdminProductAttributeSaveActionGroup instead. Clicks on Save button to save the attribute.</description> </annotations> <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml new file mode 100644 index 0000000000000..8ecef0df400be --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.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="AdminSetManageStockConfigActionGroup"> + <annotations> + <description>Set "Manage Stock" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="value" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="{{value}}" + stepKey="setManageStockConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml new file mode 100644 index 0000000000000..0f6a8df1ebf8c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.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="AdminSetMaxAllowedQtyForProductActionGroup"> + <annotations> + <description>Fills in the "Maximum Qty Allowed in Shopping Cart" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="{{qty}}" + stepKey="fillMaxAllowedQty"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml new file mode 100644 index 0000000000000..abbfdacc15395 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.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="AdminSetMinAllowedQtyForProductActionGroup"> + <annotations> + <description>Fills in the "Minimum Qty Allowed in Shopping Cart" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="{{qty}}" + stepKey="fillMinAllowedQty"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml new file mode 100644 index 0000000000000..4ecfa0762db9f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetNotifyBelowQtyValueActionGroup"> + <annotations> + <description>Fills in the "Notify for Quantity Below" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" + stepKey="uncheckNotifyBelowQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="{{qty}}" + stepKey="fillNotifyBelowQty"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup.xml new file mode 100644 index 0000000000000..37947ffca7c5d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup.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="AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup"> + <arguments> + <argument name="useInLayeredNavigationValue" type="string" defaultValue="Filterable (with results)"/> + </arguments> + <conditionalClick selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" dependentSelector="{{AttributePropertiesSection.useInLayeredNavigation}}" visible="false" stepKey="clickStoreFrontTab"/> + <waitForElementVisible selector="{{AttributePropertiesSection.useInLayeredNavigation}}" stepKey="waitForStorefrontTabLoad"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="{{useInLayeredNavigationValue}}" stepKey="selectUseInLayeredNavigationOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml new file mode 100644 index 0000000000000..7846689a8d643 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.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="AdminSetQtyUsesDecimalsConfigActionGroup"> + <annotations> + <description>Set "Qty Uses Decimals" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="value" type="string"/> + </arguments> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="{{value}}" + stepKey="setQtyUsesDecimalsConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml new file mode 100644 index 0000000000000..98156eb1ad9b1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.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="AdminSetStockStatusConfigActionGroup"> + <annotations> + <description>Set "Stock status" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="stockStatus" type="string"/> + </arguments> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" + userInput="{{stockStatus}}" stepKey="selectStockStatus"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml index b859ed2ea5942..f94bec1789068 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml @@ -16,5 +16,6 @@ </annotations> <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> + <waitForPageLoad stepKey="waitForProductPageToLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml new file mode 100644 index 0000000000000..59958f4266084 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.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="AdminSubmitCategoriesPopupActionGroup"> + <annotations> + <description>Clicks the "Done" button on the Search Categories popup.</description> + </annotations> + + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneButton" /> + <waitForPageLoad stepKey="waitForCategoryApply"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsInactiveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsInactiveActionGroup.xml new file mode 100644 index 0000000000000..cec6d42fc2dc4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsInactiveActionGroup.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="AssertAdminCategoryIsInactiveActionGroup"> + <annotations> + <description>Verify the category is disabled</description> + </annotations> + + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="seeCategoryIsDisabled"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml new file mode 100644 index 0000000000000..3a75b0a3cd361 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.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="AssertAdminCategoryIsListedInCategoriesTreeActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="seeCategoryInTree"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml new file mode 100644 index 0000000000000..e0a98a8932d4d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.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="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="doNotSeeCategoryInTree"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.xml new file mode 100644 index 0000000000000..84e14269d24c2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.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="AssertStorefrontCategoryCurrentPageIsNthActionGroup"> + <arguments> + <argument name="expectedPage" type="string"/> + </arguments> + + <grabTextFrom selector="{{StorefrontCategoryBottomToolbarSection.currentPage}}" stepKey="currentPageText"/> + <assertEquals stepKey="assertIsPageNth"> + <expectedResult type="string">{{expectedPage}}</expectedResult> + <actualResult type="variable">currentPageText</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAttributeGridPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAttributeGridPageActionGroup.xml index 2b5fe9d76875c..6e44c33d81ba4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAttributeGridPageActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAttributeGridPageActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="GoToAttributeGridPageActionGroup"> + <actionGroup name="GoToAttributeGridPageActionGroup" deprecated="Use AdminOpenAttributeSetGridPageActionGroup instead."> <annotations> <description>Goes to the Attribute Sets grid page.</description> </annotations> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToProductCatalogPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToProductCatalogPageActionGroup.xml index 08bf948c2223b..7e64dd520844d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToProductCatalogPageActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToProductCatalogPageActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="GoToProductCatalogPageActionGroup"> + <actionGroup name="GoToProductCatalogPageActionGroup" deprecated="Use AdminOpenCatalogProductPageActionGroup instead."> <annotations> <description>Goes to the Admin Products grid page.</description> </annotations> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml index e1bf2dea21318..796577bf84b65 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="SaveProductAttributeActionGroup"> <annotations> - <description>Clicks on Save. Validates that the Success Message is present.</description> + <description>DEPRECATED. Use AdminProductAttributeSaveActionGroup instead. Clicks on Save. Validates that the Success Message is present.</description> </annotations> <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml index 4da8232e8405d..660bd314c4bf3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="SaveProductAttributeInUseActionGroup"> <annotations> - <description>Clicks on Save. Validates that the Success Message is present.</description> + <description>DEPRECATED. Use AdminProductAttributeSaveActionGroup instead. Clicks on Save. Validates that the Success Message is present.</description> </annotations> <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml index e3370864e7f61..6dd7f45dd0e64 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml @@ -19,5 +19,6 @@ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{keyword}}" stepKey="fillKeywordSearchField"/> <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearch"/> + <waitForPageLoad stepKey="waitForProductSearch"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml new file mode 100644 index 0000000000000..cead98091d268 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.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="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup"> + <annotations> + <description>Validate that the Category is not present in menu on Frontend.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" + stepKey="doNotSeeCatergoryInStoreFront"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml new file mode 100644 index 0000000000000..c56a18b4895a4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.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="StorefrontAssertCategoryNameIsShownInMenuActionGroup"> + <annotations> + <description>Validate that the Category is present in menu on Frontend.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" + stepKey="seeCatergoryInStoreFront"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml new file mode 100644 index 0000000000000..65858be673dfa --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup"> + <dontSee userInput="Add to Wish List" selector="{{StorefrontProductPageSection.addToWishlist}}" stepKey="dontSeeElement"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProperUrlIsShownActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProperUrlIsShownActionGroup.xml new file mode 100644 index 0000000000000..6fb7f68f64320 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProperUrlIsShownActionGroup.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="StorefrontAssertProperUrlIsShownActionGroup"> + <annotations> + <description>Validate that the URL path is correct</description> + </annotations> + <arguments> + <argument name="urlPath" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{urlPath}}" stepKey="checkUrl"/> + </actionGroup> +</actionGroups> 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/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml new file mode 100644 index 0000000000000..5b7dd3026a905 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.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="StorefrontClickOnProductFromSidebarCompareListActionGroup"> + <annotations> + <description>Click on the product item from the sidebar comparing list.</description> + </annotations> + + <arguments> + <argument name="product" type="entity"/> + </arguments> + + <waitForElementVisible selector="{{StorefrontComparisonSidebarSection.ProductTitleByName((product.name)}}" stepKey="waitForAddedCompareProduct"/> + <click selector="{{StorefrontComparisonSidebarSection.ProductTitleByName((product.name))}}" stepKey="clickOnProductLink"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.xml new file mode 100644 index 0000000000000..4776c9d32a34d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.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="StorefrontNavigateCategoryNextPageActionGroup"> + <annotations> + <description>Navigates storefront category next page from toolbar</description> + </annotations> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage"/> + <waitForPageLoad stepKey="waitForNextCategoryPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml new file mode 100644 index 0000000000000..4a403364a91e3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.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="StorefrontSwitchStoreActionGroup"> + <annotations> + <description>Switch the Storefront to the provided Store.</description> + </annotations> + <arguments> + <argument name="storeName" type="string"/> + </arguments> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickOnSwitchStoreButton"/> + <click selector="{{StorefrontFooterSection.storeLink(storeName)}}" stepKey="selectStoreToSwitchOn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml index c9b67e0db4398..1d6bb970ea4d3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml @@ -30,4 +30,9 @@ <data key="label">No</data> <data key="value">0</data> </entity> + <entity name="CatalogInventoryOptionsOnlyXleftThreshold"> + <!-- Magento default value --> + <data key="path">cataloginventory/options/stock_threshold_qty</data> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml index daf4809a4781a..9639bc39b45f4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -265,4 +265,8 @@ <data key="level">0</data> <var key="parent_id" entityType="category" entityKey="id"/> </entity> + <entity name="AssignProductToCategory" type="category_product_link"> + <var key="category_id" entityKey="id" entityType="category"/> + <var key="sku" entityKey="sku" entityType="product"/> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 7aabedbf1c3f7..716c4b07d2f1d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -284,6 +284,9 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="ApiSimpleProductWithCategory" type="product2" extends="ApiSimpleOne"> + <requiredEntity type="custom_attribute">CustomAttributeCategoryIds</requiredEntity> + </entity> <entity name="ApiSimpleProductWithShortSKU" type="product2" extends="ApiSimpleOne"> <data key="sku" unique="suffix">pr</data> </entity> @@ -1249,6 +1252,20 @@ <requiredEntity type="product_extension_attribute">EavStock777</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="SimpleProduct_zero" type="product"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">testProductName</data> + <data key="price">0.00</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <data key="status">1</data> + <data key="quantity">777</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStock777</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> <entity name="ApiSimpleOneQty10" type="product2"> <data key="sku" unique="suffix">api-simple-product</data> <data key="type_id">simple</data> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml index c79756507794a..731754ef01959 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml @@ -72,4 +72,12 @@ <data key="quantity">3</data> <var key="sku" entityType="product" entityKey="sku" /> </entity> + <entity name="TierProductPrice50PercentDiscount" type="catalogTierPrice"> + <data key="price">50</data> + <data key="price_type">discount</data> + <data key="website_id">0</data> + <data key="customer_group">ALL GROUPS</data> + <data key="quantity">1</data> + <var key="sku" entityType="product" entityKey="sku" /> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/CategoryMeta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/CategoryMeta.xml index ae491aefc10cf..c0a92b7e1d1ad 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Metadata/CategoryMeta.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/CategoryMeta.xml @@ -57,4 +57,12 @@ <operation name="DeleteCategory" dataType="category" type="delete" auth="adminOauth" url="/V1/categories/{id}" method="DELETE"> <contentType>application/json</contentType> </operation> + + <operation name="AssignProductToCategory" dataType="category_product_link" type="create" auth="adminOauth" url="/V1/categories/{id}/products" method="POST"> + <contentType>application/json</contentType> + <object key="productLink" dataType="category_product_link"> + <field key="sku">string</field> + <field key="category_id">string</field> + </object> + </operation> </operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml index 1cb095974d0fd..034150ef45460 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml @@ -14,6 +14,7 @@ <element name="selectFromGalleryButton" type="button" selector="//*[@class='file-uploader-area']/label[text()='Select from Gallery']"/> <element name="uploadImageFile" type="input" selector=".file-uploader-area>input"/> <element name="imageFileName" type="text" selector=".file-uploader-filename"/> + <element name="imageFileMeta" type="text" selector=".file-uploader-meta"/> <element name="removeImageButton" type="button" selector=".file-uploader-summary .action-remove"/> <element name="AddCMSBlock" type="select" selector="//*[@name='landing_page']"/> <element name="description" type="input" selector="//*[@name='description']"/> 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/AdminProductContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml index fafae5d535546..4b4aa20300d14 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml @@ -15,5 +15,6 @@ <element name="shortDescriptionTextArea" type="textarea" selector="#product_form_short_description"/> <element name="sectionHeaderIfNotShowing" type="button" selector="//div[@data-index='content']//div[contains(@class, '_hide')]"/> <element name="pageHeader" type="textarea" selector="//*[@class='page-header row']"/> + <element name="attributeInput" type="input" selector="input[name='product[{{attributeCode}}]']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml index 26946692ce050..7a829a5475758 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml @@ -8,7 +8,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ProductDescriptionWYSIWYGToolbarSection"> - <element name="TinyMCE4" type="button" selector="//div[@id='editorproduct_form_description']//*[contains(@class,'mce-branding')]"/> + <element name="TinyMCE4" type="button" selector="div#editorproduct_form_description .mce-branding"/> <element name="showHideBtn" type="button" selector="#toggleproduct_form_description"/> <element name="InsertImageBtn" type="button" selector="#buttonsproduct_form_description > .scalable.action-add-image.plugin"/> <element name="Style" type="button" selector="//div[@id='editorproduct_form_description']//span[text()='Paragraph']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml index 544bdf85681c9..f4d5d20bcfb83 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml @@ -8,12 +8,13 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ProductWYSIWYGSection"> - <element name="Switcher" type="button" selector="//select[@id='dropdown-switcher']"/> - <element name="v436" type="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 4.3.6']"/> - <element name="v3" type="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 3.6(Deprecated)']"/> + <element name="Switcher" type="button" selector="select#dropdown-switcher"/> + <element name="v436" type="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 4.3.6']" deprecated="New element was introduced. Please use 'ProductWYSIWYGSection.v4910'"/> + <element name="v3" type="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 3.6(Deprecated)']" deprecated="New element was introduced. Please use 'ProductWYSIWYGSection.v4910'"/> + <element name="v4910" type ="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 4.9.10']" /> <element name="TinymceDescription3" type="button" selector="//span[text()='Description']"/> <element name="SaveConfig" type="button" selector="#save"/> <element name="v4" type="button" selector="#category_form_description_v4"/> - <element name="WYSIWYGBtn" type="button" selector=".//button[@class='action-default scalable action-wysiwyg']"/> + <element name="WYSIWYGBtn" type="button" selector="button.action-default.scalable.action-wysiwyg"/> </section> </sections> 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/AdminProductSEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml index 8685e84a347f2..f2cd9f4de3570 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml @@ -12,6 +12,7 @@ <element name="sectionHeader" type="button" selector="div[data-index='search-engine-optimization']" timeout="30"/> <element name="urlKeyInput" type="input" selector="input[name='product[url_key]']"/> <element name="useDefaultUrl" type="checkbox" selector="input[name='use_default[url_key]']"/> + <element name="urlKeyRedirectCheckbox" type="checkbox" selector="input[name='product[url_key_create_redirect]']"/> <element name="metaTitleInput" type="input" selector="input[name='product[meta_title]']"/> <element name="metaKeywordsInput" type="textarea" selector="textarea[name='product[meta_keyword]']"/> <element name="metaDescriptionInput" type="textarea" selector="textarea[name='product[meta_description]']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml index 09eb4ad954274..c27a6107e5e35 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml @@ -12,6 +12,6 @@ <element name="previousPage" type="button" selector=".//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'previous')]" timeout="30"/> <element name="pageNumber" type="text" selector="//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'page')]//span[2][contains(text() ,'{{var1}}')]" parameterized="true"/> <element name="perPage" type="select" selector="//*[@class='toolbar toolbar-products'][2]//select[@id='limiter']"/> - <element name="currentPage" type="text" selector=".products.wrapper + .toolbar-products .pages .current span:nth-of-type(2)"/> + <element name="currentPage" type="text" selector=".//*[@class='toolbar toolbar-products'][2]//li[contains(@class, 'current')]//span[2]" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml index 186d0cf313d96..5ec493aef0cea 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml @@ -17,5 +17,7 @@ <element name="removeFilter" type="button" selector="div.filter-current .remove"/> <element name="activeFilterOptions" type="text" selector=".filter-options-item.active .items"/> <element name="activeFilterOptionItemByPosition" type="text" selector=".filter-options-item.active .items li:nth-child({{itemPosition}}) a" parameterized="true"/> + <element name="enabledFilterOptionItemByLabel" type="text" selector="//div[@class='filter-options']//li[@class='item']//a[contains(text(), '{{optionLabel}}')]" parameterized="true"/> + <element name="disabledFilterOptionItemByLabel" type="text" selector="//div[@class='filter-options']//li[@class='item' and contains(text(), '{{optionLabel}}')]" 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/Section/StorefrontProductActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml index 5ee754904b702..13ced1c0263e0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml @@ -10,6 +10,7 @@ <section name="StorefrontProductActionSection"> <element name="quantity" type="input" selector="#qty"/> <element name="addToCart" type="button" selector="#product-addtocart-button" timeout="60"/> + <element name="addToCartEnabledWithTranslation" type="button" selector="button#product-addtocart-button[data-translate]:enabled" timeout="60"/> <element name="addToCartButtonTitleIsAdding" type="text" selector="//button/span[text()='Adding...']"/> <element name="addToCartButtonTitleIsAdded" type="text" selector="//button/span[text()='Added']"/> <element name="addToCartButtonTitleIsAddToCart" type="text" selector="//button/span[text()='Add to Cart']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml index 78818dd37a5d4..7be02126e3a0f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml @@ -25,5 +25,6 @@ <element name="customOptionDropDown" type="select" selector="//*[@id='product-options-wrapper']//select[contains(@class, 'product-custom-option admin__control-select')]"/> <element name="qtyInputWithProduct" type="input" selector="//tr//strong[contains(.,'{{productName}}')]/../../td[@class='col qty']//input" parameterized="true"/> <element name="customOptionRadio" type="input" selector="//span[contains(text(),'{{customOption}}')]/../../input" parameterized="true"/> + <element name="onlyProductsLeft" type="block" selector="//div[@class='product-info-price']//div[@class='product-info-stock-sku']//div[@class='availability only']"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml index e42dd8b8ab12e..92be79fdfe720 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml @@ -22,7 +22,9 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockFalse"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="SimpleSubCategory" stepKey="category"/> <createData entity="SimpleProduct4" stepKey="product"> <requiredEntity createDataKey="category"/> @@ -30,7 +32,9 @@ </before> <after> <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockFalse"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="product" stepKey="deleteProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> @@ -47,8 +51,12 @@ <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockEnable.path}} {{CatalogInventoryOptionsShowOutOfStockEnable.value}}" stepKey="setConfigShowOutOfStockTrue"/> <!--Clear cache and reindex--> <comment userInput="Clear cache and reindex" stepKey="cleanCache"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> +</actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> +</actionGroup> <!--Open product page--> <comment userInput="Open product page" stepKey="openProductPage"/> <amOnPage url="{{StorefrontProductPage.url($$product.custom_attributes[url_key]$$)}}" stepKey="goToSimpleProductPage2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml index e00b3fe2994eb..b677fae5e58ea 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml @@ -53,8 +53,7 @@ <actionGroup ref="AddCrossSellProductBySkuActionGroup" stepKey="addProduct3ToSimp1"> <argument name="sku" value="$simpleProduct3.sku$"/> </actionGroup> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <!-- Go to simpleProduct3, add simpleProduct1 and simpleProduct2 as cross-sell--> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProduct3"> @@ -68,8 +67,7 @@ <actionGroup ref="AddCrossSellProductBySkuActionGroup" stepKey="addProduct2ToSimp3"> <argument name="sku" value="$simpleProduct2.sku$"/> </actionGroup> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave2"/> - <waitForPageLoad stepKey="waitForPageLoad2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave2"/> <!-- Go to frontend, add simpleProduct1 to cart--> <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimp1ToCart"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml index 92f24fe76502d..2dc840b60f3b8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml @@ -22,12 +22,11 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateSimpleProduct"> <argument name="product" value="SimpleProduct3"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml index 7cf388914207b..d1e292ff56444 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml @@ -22,12 +22,11 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="defaultVirtualProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml index 6eb4de39726f0..bcdf6ad39124a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml @@ -32,8 +32,11 @@ <waitForPageLoad stepKey="waitForPageLoad" /> <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> - <argument name="ImageFolder" value="ImageFolder"/> + <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> <actionGroup ref="AttachImageActionGroup" stepKey="attachImage1"> <argument name="Image" value="ImageUpload3"/> @@ -44,12 +47,16 @@ </actionGroup> <actionGroup ref="SaveImageActionGroup" stepKey="insertImage"/> <actionGroup ref="FillOutUploadImagePopupActionGroup" stepKey="fillOutUploadImagePopup" /> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCatalog"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCatalog"/> <amOnPage url="/{{SimpleSubCategory.name_lwr}}.html" stepKey="goToCategoryFrontPage"/> <waitForPageLoad stepKey="waitForPageLoad2"/> <seeElement selector="{{StorefrontCategoryMainSection.mediaDescription(ImageUpload3.content)}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontCategoryMainSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> <actionGroup ref="DeleteCategoryActionGroup" stepKey="DeleteCategory"> <argument name="categoryEntity" value="SimpleSubCategory"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml index d86a696880bae..acc22f2e611d6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml @@ -32,6 +32,7 @@ <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillBasicProductInfo" /> <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab" /> + <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToDescription" /> <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForDescription" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageIcon}}" stepKey="clickInsertImageIcon1" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.Browse}}" stepKey="clickBrowse1" /> @@ -67,7 +68,7 @@ <fillField selector="{{ProductDescriptionWYSIWYGToolbarSection.ImageDescription}}" userInput="{{ImageUpload1.content}}" stepKey="fillImageDescription1" /> <fillField selector="{{ProductDescriptionWYSIWYGToolbarSection.Height}}" userInput="{{ImageUpload1.height}}" stepKey="fillImageHeight1" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.OkBtn}}" stepKey="clickOkBtn1" /> - <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="scrollToTinyMCE4" /> + <scrollTo selector="{{ProductShortDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToTinyMCE4" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageIcon}}" stepKey="clickInsertImageIcon2" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.Browse}}" stepKey="clickBrowse2" /> <waitForLoadingMaskToDisappear stepKey="waitForLoading13"/> @@ -98,8 +99,7 @@ <fillField selector="{{ProductShortDescriptionWYSIWYGToolbarSection.Height}}" userInput="{{ImageUpload3.height}}" stepKey="fillImageHeight2" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.OkBtn}}" stepKey="clickOkBtn2" /> <waitForPageLoad stepKey="waitForPageLoad6"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading12" /> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <amOnPage url="{{_defaultProduct.name}}.html" stepKey="navigateToProductPage"/> <waitForPageLoad stepKey="waitForPageLoad7"/> <seeElement selector="{{StorefrontProductInfoMainSection.mediaDescription}}" stepKey="assertMediaDescription"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml index 52da8c70a3bc8..94d3b46aaa5f1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml @@ -34,16 +34,14 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Open Product Index Page and filter the product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> <!-- Update product Advanced Inventory setting --> <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> <waitForPageLoad stepKey="waitForProductToLoad"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> @@ -54,15 +52,19 @@ <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuatityUsesDecimal"/> <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyheckBox"/> <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="In Stock" stepKey="selectOutOfStock"/> - <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> - <waitForPageLoad stepKey="waitForProductPageToLoad"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + <actionGroup ref="AdminSetStockStatusConfigActionGroup" stepKey="selectOutOfStock"> + <argument name="stockStatus" value="In Stock"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Clear cache and reindex--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Verify product is visible in category front page --> <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> @@ -74,13 +76,13 @@ <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{SimpleProduct.price}}" stepKey="seeProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProduct.sku}}" stepKey="seeProductSkuInStoreFront"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront"> + <argument name="productSku" value="{{SimpleProduct.sku}}"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> <!--Add Product to the cart--> <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1" stepKey="fillProductQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> - <waitForPageLoad stepKey="waitForProductToAddInCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeSuccessSaveMessage"/> <seeElement selector="{{StorefrontMinicartSection.quantity(1)}}" stepKey="seeAddedProductQuantityInCart"/> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml index cbbd496e8cb34..1ed079b12d1fd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml @@ -31,8 +31,7 @@ <deleteData createDataKey="createSimpleUSCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex1"/> - <waitForPageLoad time="30" stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex1"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> @@ -69,8 +68,7 @@ <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabel_2"/> <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmount_2"/> <!--Case: Tier Price for General Customer Group--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex2"/> - <waitForPageLoad time="30" stepKey="waitForProductPageToLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex2"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> @@ -94,8 +92,7 @@ <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabel_4"/> <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmount_3"/> <!--Case: Tier Price applied if Product quantity meets Tier Price Condition--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex3"/> - <waitForPageLoad time="30" stepKey="waitForProductPageToLoad2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex3"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct3"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> @@ -160,8 +157,7 @@ <actualResult type="variable">grabTextFromSubtotalField3</actualResult> </assertEquals> <!--Tier Price is changed in Shopping Cart and is changed on Product page if Tier Price parameters are changed in Admin--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex4"/> - <waitForPageLoad time="30" stepKey="waitForProductPageToLoa4"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex4"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct4"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> @@ -225,8 +221,7 @@ <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceAmount('2', '75')}}" stepKey="assertProductTierPriceAmountForSecondRow2"/> <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceSavePercentageAmount('1', '10')}}" stepKey="assertProductTierPriceSavePercentageAmountForFirstRow2"/> <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceSavePercentageAmount('2', '25')}}" stepKey="assertProductTierPriceSavePercentageAmountForSecondRow2"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex5"/> - <waitForPageLoad time="30" stepKey="waitForProductPageToLoad3"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex5"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct5"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> @@ -246,7 +241,7 @@ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="closeAdvancedPricingPopup"/> <waitForElementVisible selector="{{AdminProductFormSection.productPrice}}" stepKey="waitForAdminProductFormSectionProductPriceInput"/> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="200" stepKey="fillProductPrice200"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToShoppingCartPage4"/> <grabTextFrom selector="{{CheckoutCartProductSection.productSubtotalByName($$createSimpleProduct.name$$)}}" stepKey="grabTextFromSubtotalField7"/> <assertEquals message="Shopping cart should contain subtotal $4,000" stepKey="assertSubtotalField7"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml index fd8c0ba29fdfa..45284e69a54e0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml @@ -27,8 +27,7 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad time="30" stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml index 96d0c209aba34..077765bd5c15a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml @@ -31,7 +31,9 @@ <!-- Set Magento back to default configuration --> <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockFalse"/> <magentoCLI command="config:set {{CatalogInventoryItemOptionsBackordersDisable.path}} {{CatalogInventoryItemOptionsBackordersDisable.value}}" stepKey="setConfigAllowBackordersFalse"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml new file mode 100644 index 0000000000000..68e6040277247 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.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="AdminChangeProductAttributeGroupTest"> + <annotations> + <stories value="Preserving attribute value after attribute group is changed"/> + <title value="Preserving attribute value after attribute group is changed"/> + <description value="Attribute value should be preserved after changing attribute group"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-35612"/> + <useCaseId value="MC-31892"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="productAttributeText" stepKey="createProductAttribute"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <createData entity="CatalogAttributeSet" stepKey="createSecondAttributeSet"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$createAttributeSet.attribute_set_id$/" + stepKey="onAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$createProductAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$createSecondAttributeSet.attribute_set_id$/" + stepKey="onSecondAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToContentGroup"> + <argument name="group" value="Content"/> + <argument name="attribute" value="$createProductAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveSecondAttributeSet"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <deleteData createDataKey="createSecondAttributeSet" stepKey="deleteSecondAttributeSet"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + + <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + + <actionGroup ref="AdminProductPageSelectAttributeSetActionGroup" stepKey="selectAttributeSet"> + <argument name="attributeSetName" value="$createAttributeSet.attribute_set_name$"/> + </actionGroup> + <waitForText userInput="$createProductAttribute.default_frontend_label$" stepKey="seeAttributeInForm"/> + <fillField selector="{{AdminProductFormSection.attributeRequiredInput($createProductAttribute.attribute_code$)}}" + userInput="test" + stepKey="fillProductAttributeValue"/> + <actionGroup ref="AdminProductPageSelectAttributeSetActionGroup" stepKey="selectSecondAttributeSet"> + <argument name="attributeSetName" value="$createSecondAttributeSet.attribute_set_name$"/> + </actionGroup> + <actionGroup ref="ExpandAdminProductSectionActionGroup" stepKey="expandContentSection"/> + <waitForText userInput="$createProductAttribute.default_frontend_label$" stepKey="seeAttributeInSection"/> + <grabValueFrom selector="{{AdminProductContentSection.attributeInput($createProductAttribute.attribute_code$)}}" + stepKey="attributeValue"/> + <assertEquals stepKey="assertAttributeValue"> + <expectedResult type="string">test</expectedResult> + <actualResult type="variable">attributeValue</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml index 39351539d14a6..a72af673c009a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml @@ -131,7 +131,9 @@ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct1.price$$" stepKey="seeInitialPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront"> + <argument name="productSku" value="$$createConfigProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> <!-- Verify First Child Product attribute option is displayed --> @@ -146,8 +148,7 @@ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct3.price$$" stepKey="seeChildProduct3PriceInStoreFront"/> <!-- Open Product Index Page and Filter First Child product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="ApiSimpleOne"/> </actionGroup> @@ -156,8 +157,7 @@ <!-- Disable the product --> <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="disableProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!-- Open Product Store Front Page --> @@ -168,7 +168,9 @@ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront1"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct2.price$$" stepKey="seeUpdatedProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront1"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront1"> + <argument name="productSku" value="$$createConfigProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront1"/> <!-- Verify product Attribute Option1 is not displayed --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml index b3f7f0e6eb42a..0bdf19c9b8950 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml @@ -33,7 +33,9 @@ </createData> <!-- Create simple product --> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> - <magentoCLI command="indexer:reindex" arguments="catalogsearch_fulltext" stepKey="reindexCatalogSearch"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexCatalogSearch"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> <!-- Login to Admin page --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml index de8110f995606..b52d18f3c0203 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml @@ -32,16 +32,14 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create subcategory under parent category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!-- Verify Parent Category and Sub category is not visible in navigation menu --> <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml index fd8093d8d3b52..a4a42a9999a5c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml @@ -31,16 +31,14 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create subcategory under parent category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!-- Verify Parent Category and Sub category is not visible in navigation menu --> <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml index e6cbe156698e7..99fd1cf3caf3f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml @@ -32,16 +32,14 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create subcategory under parent category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!-- Verify Parent Category and Sub category is not visible in navigation menu --> <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml index a94610abf0918..c15cedadb4460 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml @@ -34,42 +34,47 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Open Product Index Page and filter the product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> <!-- Update product Advanced Inventory Setting --> - <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> - <waitForPageLoad stepKey="waitForProductToLoad"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuatityUsesDecimal"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton"/> - <waitForPageLoad stepKey="waitForProductPageToSave"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> - <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProduct"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminSetManageStockConfigActionGroup" stepKey="setManageStockConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminFillAdvancedInventoryQtyActionGroup" stepKey="fillProductQty"> + <argument name="qty" value="5"/> + </actionGroup> + <actionGroup ref="AdminSetMinAllowedQtyForProductActionGroup" stepKey="fillMiniAllowedQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetMaxAllowedQtyForProductActionGroup" stepKey="fillMaxAllowedQty"> + <argument name="qty" value="1000"/> + </actionGroup> + <actionGroup ref="AdminSetQtyUsesDecimalsConfigActionGroup" stepKey="setQtyUsesDecimalsConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSetNotifyBelowQtyValueActionGroup" stepKey="fillNotifyBelowQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetStockStatusConfigActionGroup" stepKey="selectOutOfStock"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButton"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Verify product is not visible in category store front page --> - <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> - <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> - <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> - <dontSee selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="dontSeeProductInCategoryPage"/> + <actionGroup ref="AssertStorefrontProductAbsentOnCategoryPageActionGroup" stepKey="doNotSeeProductInCategoryPage"> + <argument name="categoryUrlKey" value="$$createCategory.name$$"/> + <argument name="productName" value="{{SimpleProduct.name}}"/> + </actionGroup> <!--Verify Product In Store Front--> - <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToProductStorefrontPage"/> - <waitForPageLoad stepKey="waitForProductPageTobeLoaded"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="Out of stock" stepKey="seeProductStatusIsOutOfStock"/> + <actionGroup ref="StorefrontCheckProductStockStatus" stepKey="seeProductOnStorefront"> + <argument name="productUrlKey" value="$$createSimpleProduct.custom_attributes[url_key]$$"/> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="stockStatus" value="Out of stock"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml index e64707a895fd4..9c1ff43587a27 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml @@ -37,41 +37,53 @@ <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 0" /> </after> <!--Open Product Index Page and filter the product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> <!-- Update product Advanced Inventory Setting --> - <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> - <waitForPageLoad stepKey="waitForProductToLoad"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuantityUsesDecimal"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> - <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> - <waitForPageLoad stepKey="waitForProductPageToLoad"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> - <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> - + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProduct"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminSetManageStockConfigActionGroup" stepKey="setManageStockConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminFillAdvancedInventoryQtyActionGroup" stepKey="fillProductQty"> + <argument name="qty" value="5"/> + </actionGroup> + <actionGroup ref="AdminSetMinAllowedQtyForProductActionGroup" stepKey="fillMiniAllowedQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetMaxAllowedQtyForProductActionGroup" stepKey="fillMaxAllowedQty"> + <argument name="qty" value="1000"/> + </actionGroup> + <actionGroup ref="AdminSetQtyUsesDecimalsConfigActionGroup" stepKey="setQtyUsesDecimalsConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSetNotifyBelowQtyValueActionGroup" stepKey="fillNotifyBelowQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetStockStatusConfigActionGroup" stepKey="selectOutOfStock"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButton"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Run re-index task --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify product is visible in category front page --> - <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> - <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> - <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> - <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInCategoryPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="selectCategory"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeProductName"> + <argument name="productName" value="{{SimpleProduct.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml index 6ac71c4a7982d..3fff2c118ae6d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml @@ -100,8 +100,7 @@ <!--Open Category Page and select created category--> <comment userInput="Open Category Page and select created category" stepKey="commentOpenCategoryPage"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForPageToLoad0"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForPageToLoaded2"/> <!--Select Products--> @@ -120,8 +119,7 @@ <waitForPageLoad stepKey="waitFroPageToLoad2"/> <see selector="{{AdminProductGridFilterSection.productCount}}" userInput="30" stepKey="seeNumberOfProductsFound"/> <click selector="{{AdminCategoryProductsGridSection.productSelectAll}}" stepKey="selectSelectAll"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> <!--Open Category Store Front Page--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml index 192bab7c6d126..2cdec1405e9f9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml @@ -31,21 +31,19 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create subcategory under parent category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedCategory"> + <argument name="Category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> <!-- Verify Parent Category is visible in navigation menu and Sub category is not visible in navigation menu --> - <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> - <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnStoreNavigationBar"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCategoryOnStoreNavigationBar"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeSubCategoryOnStoreNavigation"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml index 65e67020e4532..6e607ca012ba0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml @@ -22,7 +22,7 @@ <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> </before> <after> - <actionGroup ref="GoToProductCatalogPageActionGroup" stepKey="goToProductCatalogPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteSimpleProduct"> <argument name="product" value="_defaultProduct"/> </actionGroup> @@ -49,12 +49,16 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductForm"/> <!-- Check that product was added with implicit type change --> <comment stepKey="beforeVerify" userInput="Verify Product Type Assigned Correctly"/> - <actionGroup ref="GoToProductCatalogPageActionGroup" stepKey="goToProductCatalogPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetSearch"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="searchForProduct"> <argument name="product" value="_defaultProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Virtual Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Virtual Product"/> + </actionGroup> <actionGroup ref="AssertProductInStorefrontProductPageActionGroup" stepKey="AssertProductInStorefrontProductPage"> <argument name="product" value="_defaultProduct"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml index f79072582035b..7191f1971b319 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml @@ -25,6 +25,10 @@ <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillProductForm"> <argument name="product" value="_defaultProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Simple Product"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml index 04d032511ded0..9b9f7cf468985 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml @@ -57,8 +57,7 @@ <see userInput="$$createProductAttribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup2"/> <!-- Assert attribute can be used in product creation --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> - <waitForPageLoad stepKey="waitForPageLoad3"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGrid"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml index b94c12d1d7a39..dba1ea040c825 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml @@ -27,20 +27,18 @@ <after> <!-- Delete the created category --> <actionGroup ref="DeleteMostRecentCategoryActionGroup" stepKey="getRidOfCreatedCategory"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> </after> <!-- Find the product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="SimpleTwo"/> </actionGroup> <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- Fill out the form for the new category --> <actionGroup ref="FillNewProductCategoryActionGroup" stepKey="FillNewProductCategory"> 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/AdminCreateCategoryTest/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml index b8e58eae8a98a..83404391abca9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml @@ -23,16 +23,13 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPage"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="enterCategoryName"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="enterURLKey"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> - <!-- Literal URL below, need to refactor line + StorefrontCategoryPage when support for variable URL is implemented--> - <amOnPage url="/{{SimpleSubCategory.name_lwr}}.html" stepKey="goToCategoryFrontPage"/> - <seeInTitle userInput="{{SimpleSubCategory.name}}" stepKey="assertTitle"/> - <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="assertInfo1"/> + <!--Go to storefront and verify created category on frontend--> + <actionGroup ref="CheckCategoryOnStorefrontActionGroup" stepKey="checkCreatedCategoryOnFrontend"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml index 4c1993eb803b3..3332bc66653e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml @@ -57,8 +57,7 @@ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> <!--Verify the Category Title--> @@ -72,8 +71,12 @@ </actionGroup> <!--Clear cache and reindex--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Verify Product in store front page--> <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name_lwr)}}" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForPageToBeLoaded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml index 4b0774d2307dd..e66984dda4427 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml @@ -21,10 +21,8 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> </before> <after> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> - <waitForPageLoad stepKey="waitStoreIndexPageLoad" /> <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> - <argument name="storeGroupName" value="customStore.name"/> + <argument name="storeGroupName" value="customStoreGroup.name"/> </actionGroup> <actionGroup ref="DeleteCategoryActionGroup" stepKey="deleteCreatedNewRootCategory"> <argument name="categoryEntity" value="NewRootCategory"/> @@ -37,39 +35,32 @@ <argument name="categoryEntity" value="NewRootCategory"/> </actionGroup> <!--Create subcategory--> - <scrollToTopOfPage stepKey="scrollToTopOfPage"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name)}}" stepKey="clickOnCreatedNewRootCategory"/> - <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedCategory"> + <argument name="Category" value="NewRootCategory"/> + </actionGroup> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> <argument name="categoryEntity" value="SimpleSubCategory"/> </actionGroup> <!--Create a Store--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Create a Store View--> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> - <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> - <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> - <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> <!--Go to store front page--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> <!--Verify subcategory displayed in store front page--> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectCustomStore"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSubCategoryInStoreFrontPage"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="switchToCustomStore"> + <argument name="storeName" value="{{customStoreGroup.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml index b27d9239c53e1..f61a97219903f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml @@ -30,7 +30,7 @@ <click selector="{{AdminCategoryModalSection.ok}}" stepKey="confirmDelete"/> <waitForPageLoad time="60" stepKey="waitForDeleteToFinish"/> <see selector="You deleted the category." stepKey="seeDeleteSuccess"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandToSeeAllCategories"/> <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(FirstLevelSubCat.name)}}" stepKey="dontSeeCategoryInTree"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -40,36 +40,31 @@ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="fillFirstSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFirstSubCategory"/> - <waitForPageLoad stepKey="waitForSFirstSubCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveFirstSubCategory"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <!--Create Nested Second Sub Category--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SecondLevelSubCat.name}}" stepKey="fillSecondSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSecondSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSecondSubCategory"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage1"/> <!--Create Nested Third Sub Category/>--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton2"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{ThirdLevelSubCat.name}}" stepKey="fillThirdSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveThirdSubCategory"/> - <waitForPageLoad stepKey="waitForThirdCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveThirdSubCategory"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage2"/> <!--Create Nested fourth Sub Category />--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton3"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FourthLevelSubCat.name}}" stepKey="fillFourthSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFourthSubCategory"/> - <waitForPageLoad stepKey="waitForFourthCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveFourthSubCategory"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage3"/> <!--Create Nested fifth Sub Category />--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton4"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FifthLevelCat.name}}" stepKey="fillFifthSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFifthLevelCategory"/> - <waitForPageLoad stepKey="waitForFifthCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveFifthLevelCategory"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage4"/> <amOnPage url="/{{FirstLevelSubCat.name}}/{{SecondLevelSubCat.name}}/{{ThirdLevelSubCat.name}}/{{FourthLevelSubCat.name}}/{{FifthLevelCat.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml index a7dab57173377..6d7d56861b731 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml @@ -26,20 +26,12 @@ </after> <!-- Create In active Category --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> - <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> - <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> - <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> - <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontCategoryIsChecked"/> - <!--Verify InActive Category is created--> - <seeElement selector="{{AdminCategoryContentSection.categoryInTree(_defaultCategory.name)}}" stepKey="seeCategoryInTree" /> + <actionGroup ref="AdminCreateInactiveCategoryActionGroup" stepKey="createInactiveCategory"/> + <actionGroup ref="AssertAdminCategoryIsInactiveActionGroup" stepKey="seeDisabledCategory"/> <!--Verify Category is not listed store front page--> - <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> - <waitForPageLoad stepKey="waitForPageToBeLoaded"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnStoreFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStoreFront"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeCategoryNameInMenu"> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml index d3a766be2c99f..f60312f19a7e0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml @@ -26,21 +26,11 @@ </after> <!--Create Category with not included in menu Subcategory --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> - <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIncludeInMenu"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> - <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> - <waitForPageLoad stepKey="waitForPageSaved"/> - <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> - <!--Verify Category is created/>--> - <seeElement selector="{{AdminCategoryContentSection.activeCategoryInTree(_defaultCategory.name)}}" stepKey="seeCategoryInTree" /> + <actionGroup ref="AdminCreateCategoryWithInactiveIncludeInMenuActionGroup" stepKey="createNotIncludedInMenuCategory"/> <!--Verify Category in store front page menu/>--> - <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> - <waitForPageLoad stepKey="waitForPageToBeLoaded"/> - <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnNavigation"/> + <actionGroup ref="CheckCategoryOnStorefrontActionGroup" stepKey="CheckCategoryOnStorefront"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeCategoryOnNavigation"> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml index 3273fb62e7d9c..4173254c66fc3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml @@ -59,8 +59,7 @@ <actionGroup ref="AdminAddProductToCategoryActionGroup" stepKey="addProductToCategory"> <argument name="product" value="$$simpleProduct$$"/> </actionGroup> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilterTest.xml index 0b269749c5dd6..21256342986ba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilterTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilterTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="NavigateToAndResetProductGridToDefaultViewActionGroup" stepKey="NavigateToAndResetProductGridToDefaultView"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <!--Create Default Product--> <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddDefaultProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillDefaultProductName"/> @@ -41,12 +40,10 @@ <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="scrollToSearchEngine"/> <click selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="selectSearchEngineOptimization"/> <fillField selector="{{AdminProductFormBundleSection.urlKey}}" userInput="{{SimpleProduct.urlKey}}" stepKey="fillUrlKey"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveDefaultProduct"/> - <waitForPageLoad stepKey="waitForPDefaultProductSaved"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveDefaultProduct"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="successMessageYouSavedTheProductIsShown"/> <!--Create product with grid filter Not Visible Individually--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="ProductList"/> - <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="ProductList"/> <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddFilterProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="fillProductName"/> <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{defaultSimpleProduct.sku}}" stepKey="fillProductSku"/> @@ -55,8 +52,7 @@ <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="scrollToSearchEngineOptimization"/> <click selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="selectSearchEngineOptimization1"/> <fillField selector="{{AdminProductFormBundleSection.urlKey}}" userInput="{{defaultSimpleProduct.urlKey}}" stepKey="fillUrlKey1"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> - <waitForPageLoad stepKey="waitForProductSaved"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create sub category--> @@ -72,8 +68,7 @@ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="selectDefaultProduct"/> <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton1"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectDefaultProductFromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="WaitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="successMessageYouSavedTheCategory"/> <!--Verify product with grid filter is not not visible--> <amOnPage url="{{StorefrontProductPage.url(defaultSimpleProduct.urlKey)}}" stepKey="seeOnProductPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml index 19552ddaab729..af72dba3f8051 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml @@ -29,8 +29,7 @@ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <!-- Verify subcategory created with required fields --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml index 7b2c67b205ea8..758dcee69525e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml @@ -46,8 +46,7 @@ </after> <!-- Open Product Index Page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <!-- Select Created Product--> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> @@ -94,8 +93,7 @@ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> <waitForPageLoad stepKey="waitForAttributeToSave"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> - <waitForPageLoad stepKey="waitForProductToSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="saveTheProduct"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Verify product attribute added in product form --> @@ -105,7 +103,7 @@ <seeElement selector="{{AdminProductFormSection.attributeLabelByText(ProductAttributeFrontendLabel.label)}}" stepKey="seeAttributeLabelInProductForm"/> <!--Verify Product Attribute in Attribute Form --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> <waitForPageLoad stepKey="waitForPageLoad" /> @@ -124,7 +122,9 @@ <waitForPageLoad stepKey="waitForProductToLoad1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="seeProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront"> + <argument name="productSku" value="$$createConfigProduct.sku$$"/> + </actionGroup> <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToMoreInformation"/> <see selector="{{StorefrontProductMoreInformationSection.attributeLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeLabel"/> <see selector="{{StorefrontProductMoreInformationSection.attributeValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="seeAttributeValue"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml index 37dc7de910917..4c57504b60ad7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml @@ -29,8 +29,7 @@ <!-- Generate the datetime default value --> <generateDate date="now" format="n/j/y g:i A" stepKey="generateDefaultValue"/> <!-- Create new datetime product attribute --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForPageLoadAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <actionGroup ref="CreateProductAttributeWithDatetimeFieldActionGroup" stepKey="createAttribute"> <argument name="attribute" value="DatetimeProductAttribute"/> <argument name="date" value="{$generateDefaultValue}"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml index 88b1c874caadc..b8fec6d5bc001 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="navigateToNewProductAttributePage"/> <!-- Set attribute properties --> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml index fef69edde23e8..9db8e74b6ae7a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -60,7 +60,7 @@ <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> <!-- Go to Product Attribute Grid page --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$$attribute.attribute_code$$" stepKey="fillAttrCodeField" /> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> @@ -83,7 +83,9 @@ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Go to store's advanced catalog search page --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml index f8346f5a9dd5c..500c95d1120f3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml @@ -31,20 +31,25 @@ <argument name="storeView" value="customStoreFR"/> </actionGroup> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron twice --> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron1"/> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron2"/> + <!-- Run cron --> + <magentoCron stepKey="runIndexCronJobs" groups="index"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> @@ -56,20 +61,22 @@ </after> <!-- Select created category and make category inactive--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatNotActive.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{CatNotActive.name}}" stepKey="seeUpdatedCategoryTitle"/> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveCategory"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <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/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml index 0aa89bdfd45b6..2394b41502f84 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml @@ -31,20 +31,25 @@ <argument name="storeView" value="customStoreFR"/> </actionGroup> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron twice --> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron1"/> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron2"/> + <!-- Run cron --> + <magentoCron stepKey="runIndexCronJobs" groups="index"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> @@ -56,21 +61,22 @@ </after> <!-- Select created category and make category inactive--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableActiveCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveIncludeInMenu"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <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 171d15fe6ed4f..35e53273aebf2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml @@ -31,20 +31,25 @@ <argument name="storeView" value="customStoreFR"/> </actionGroup> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron twice --> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron1"/> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron2"/> + <!-- Run cron --> + <magentoCron stepKey="runIndexCronJobs" groups="index"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> </actionGroup> @@ -56,22 +61,23 @@ </after> <!-- Select created category and disable Include In Menu option--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIcludeInMenuOption"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <!--Verify category is saved and Include In Menu Option is disabled in Category Page --> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="verifyInactiveIncludeInMenu"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <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/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml index caacfde89d1cb..7b555aa84be05 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -64,7 +64,7 @@ <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> <!-- Go to Product Attribute Grid page --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$$attribute.attribute_code$$" stepKey="fillAttrCodeField" /> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> @@ -87,7 +87,9 @@ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Go to store's advanced catalog search page --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml index e99643deed11d..52cac23574b53 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml @@ -26,7 +26,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="GoToProductCatalogPageActionGroup" stepKey="goToProductCatalogPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml index 573fc1f83a5a8..fc5fa60f754c4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml @@ -91,8 +91,7 @@ <dontSeeElement selector="{{AdminProductAttributeSetEditSection.attributesInGroup(emptyGroup.name)}}" stepKey="seeNoAttributes"/> <!-- Navigate to Catalog > Products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductPage"/> <!-- Start to create a new simple product with the custom attribute set from the preconditions --> <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml index 7fdab11d0a050..61ef389c7909e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml @@ -43,8 +43,7 @@ </after> <!-- Open Product Index Page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <!-- Select Created Product--> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> @@ -86,12 +85,13 @@ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> <waitForPageLoad stepKey="waitForAttributeToSave"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> - <waitForPageLoad stepKey="waitForProductToSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="saveTheProduct"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Run Re-Index task --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify product attribute added in product form --> <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> @@ -100,7 +100,7 @@ <seeElement selector="{{AdminProductFormSection.attributeLabelByText(ProductAttributeFrontendLabel.label)}}" stepKey="seeAttributeLabelInProductForm"/> <!--Verify Product Attribute in Attribute Form --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> <waitForPageLoad stepKey="waitForPageLoad" /> @@ -119,7 +119,9 @@ <waitForPageLoad stepKey="waitForProductToLoad1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{SimpleProduct.price}}" stepKey="seeProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProduct.sku}}" stepKey="seeProductSkuInStoreFront"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront"> + <argument name="productSku" value="{{SimpleProduct.sku}}"/> + </actionGroup> <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToMoreInformation"/> <see selector="{{StorefrontProductMoreInformationSection.attributeLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeLabel"/> <see selector="{{StorefrontProductMoreInformationSection.attributeValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="seeAttributeValue"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml index 274a560d343d8..9a9d64617f7b5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml @@ -41,9 +41,7 @@ </after> <!-- Open Product Index Page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> - + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <!-- Select Created Product--> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> <argument name="product" value="$$createSimpleProduct$$"/> @@ -74,8 +72,7 @@ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> <waitForPageLoad stepKey="waitForAttributeToSave"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> - <waitForPageLoad stepKey="waitForProductToSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="saveTheProduct"/> <!--Verify product attribute added in product form and Is Required message displayed--> <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> @@ -85,8 +82,7 @@ <!--Fill the Required field and save the product --> <fillField selector="{{AdminProductFormSection.attributeRequiredInput(newProductAttribute.attribute_code)}}" userInput="attribute" stepKey="fillTheAttributeRequiredInputField"/> <scrollToTopOfPage stepKey="scrollToTopOfProductFormPage"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct1"/> - <waitForPageLoad stepKey="waitForProductToSave1"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="saveTheProduct1"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml index c461aa8bfcf18..a9bc656870ff7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml @@ -27,7 +27,7 @@ </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> <fillField userInput="$$simpleProduct.name$$new" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> @@ -36,7 +36,7 @@ <fillField userInput="$$simpleProduct.quantity$$" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> <fillField userInput="$$simpleProduct.custom_attributes[url_key]$$" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <see userInput="The value specified in the URL Key field would generate a URL that already exists" selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="assertErrorMessage"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml index 40ca511e1f7bc..1202052c492fc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml @@ -21,8 +21,7 @@ <!--Delete all created data during the test execution and assign Default Root Category to Store--> <after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin2"/> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnPageAdminSystemStore"/> - <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="amOnPageAdminSystemStore"/> <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> <waitForPageLoad time="10" stepKey="waitForPageAdminStoresGridLoadAfterResetButton"/> <fillField selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="Main Website Store" stepKey="fillFieldOnWebsiteStore"/> @@ -58,8 +57,7 @@ <argument name="categoryEntity" value="SubCategoryWithParent"/> </actionGroup> <!--Assign new created root category to store--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnPageAdminSystemStore"/> - <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="amOnPageAdminSystemStore"/> <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> <waitForPageLoad time="10" stepKey="waitForPageAdminStoresGridLoadAfterResetButton"/> <fillField selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="Main Website Store" stepKey="fillFieldOnWebsiteStore"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml index 29c7bc6828662..8d947fba5f368 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml @@ -33,8 +33,7 @@ <click selector="{{AdminCategorySidebarActionSection.AddRootCategoryButton}}" stepKey="ClickOnAddRootButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="FillCategoryField"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="EnableCheckOption"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="ClickSaveButton"/> - <waitForPageLoad stepKey="WaitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="ClickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="AssertSuccessMessage"/> <seeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="SeeCheckBoxisSelected"/> <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="SeedFieldInput"/> 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/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml index a8cc66243d73e..9f51d6227aa1d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml @@ -22,7 +22,7 @@ <waitForPageLoad stepKey="wait1"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillName"/> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="-42" stepKey="fillPrice"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <see selector="{{AdminProductFormSection.priceFieldError}}" userInput="Please enter a number 0 or greater in this field." stepKey="seePriceValidationError"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml index f4878f2948e9d..24f87cca958ab 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml @@ -22,7 +22,7 @@ <waitForPageLoad stepKey="wait1"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillName"/> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="0" stepKey="fillPrice"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <amOnPage url="{{StorefrontProductPage.url(SimpleProduct.name)}}" stepKey="viewProduct"/> <waitForPageLoad stepKey="wait2"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$0.00" stepKey="seeZeroPrice"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml index c378ca5b2c27a..48f1f9bbb4e5c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectSimpleProduct"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickSimpleProductFromDropDownList"/> @@ -43,14 +42,12 @@ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.price}}" stepKey="fillSimpleProductPrice"/> <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.weight}}" stepKey="fillSimpleProductWeight"/> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.quantity}}" stepKey="fillSimpleProductQuantity"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductToSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search created simple product(from above step) in the grid page to verify sku masked as name and country of manufacture --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchCreatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchCreatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="fillSkuFilterFieldWithNameAndCountryOfManufactureInput" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml index 2141f44113057..13efee209a556 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml @@ -49,7 +49,9 @@ <!-- Save the product --> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Flush config cache to reset product attributes in attribute set --> - <magentoCLI command="cache:flush" arguments="config" stepKey="flushConfigCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> <reloadPage stepKey="reloadProductEditPage"/> <!-- Check default value --> <waitForElementVisible selector="{{AdminProductAttributesSection.sectionHeader}}" stepKey="waitAttributesSectionAppears"/> @@ -58,7 +60,7 @@ <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode($createDatetimeAttribute.attribute_code$)}}" stepKey="waitForSlideOutAttributes"/> <seeInField selector="{{AdminProductAttributesSection.attributeTextInputByCode($createDatetimeAttribute.attribute_code$)}}" userInput="$generateDefaultValue" stepKey="checkDefaultValue"/> <!-- Check datetime grid filter --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToAdminProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToAdminProductIndexPage"/> <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openProductFilters"/> <fillField selector="{{AdminProductGridFilterSection.inputByCodeRangeFrom($createDatetimeAttribute.attribute_code$)}}" userInput="{$generateDefaultValue}" stepKey="fillProductDatetimeFromFilter"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml index 8de84867241a8..54c3a05651c44 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml @@ -31,8 +31,12 @@ <argument name="category" value="$$createPreReqCategory$$"/> <argument name="simpleProduct" value="ProductWithUnicode"/> </actionGroup> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> <argument name="category" value="$$createPreReqCategory$$"/> <argument name="product" value="ProductWithUnicode"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml index 9d3a47cd115aa..1144804bb34ff 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> @@ -35,15 +34,13 @@ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="fillProductName"/> <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="fillProductSku"/> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductWithRequiredFields.price}}" stepKey="fillProductPrice"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved" /> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> <!-- Verify we see created virtual product(from the above step) on the product grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickSelector"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFilter"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="fillProductName1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml index 842f93b49c14a..12fa50c8eed99 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> @@ -58,12 +57,11 @@ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductOutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> @@ -72,7 +70,9 @@ <amOnPage url="{{StorefrontProductPage.url(virtualProductOutOfStock.urlKey)}}" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{virtualProductOutOfStock.sku}}"/> + </actionGroup> <!-- Verify customer see product tier price on product page --> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('1', tierPriceOnDefault.qty_0)}}" stepKey="firstTierPriceText"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml index 8bb3391b5240b..12dd30872123f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> @@ -46,7 +45,7 @@ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductCustomImportOptions.urlKey}}" stepKey="fillUrlKey"/> <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> @@ -112,13 +111,16 @@ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetSecondRow"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="fillOptionSkuForFourthDataSetSecondRow"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Verify customer see created virtual product with custom options suite and import options(from above step) on storefront page and is searchable by sku --> <amOnPage url="{{StorefrontProductPage.url(virtualProductCustomImportOptions.urlKey)}}" stepKey="goToProductPage"/> @@ -132,7 +134,9 @@ <!-- Verify we see created virtual product with custom options suite and import options on the storefront page --> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{virtualProductCustomImportOptions.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{virtualProductCustomImportOptions.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml index 5076ab2515332..378264141bf20 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml @@ -86,7 +86,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$categoryEntity.name$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductGeneralGroup.visibility}}" stepKey="seeVisibility"/> <conditionalClick selector="{{AdminProductSEOSection.sectionHeader}}" dependentSelector="{{AdminProductSEOSection.useDefaultUrl}}" visible="false" stepKey="openSearchEngineOptimizationSection"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="scrollToAdminProductSEOSection"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml index faae6a371db24..0f800419f0456 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> @@ -49,23 +48,20 @@ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductBigQty.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductBigQty.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="checkRetailCustomerTaxClass" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{virtualProductBigQty.name}}" stepKey="fillProductName1"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="virtualProductBigQty.name"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened" /> @@ -86,7 +82,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductBigQty.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -98,8 +94,12 @@ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Verify customer see created virtual product with tier price(from above step) on storefront page and is searchable by sku --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml index 3b5a8d8e753da..d644281c9fb44 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> @@ -41,19 +40,18 @@ <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{virtualProductWithoutManageStock.special_price}}" stepKey="fillSpecialPrice"/> <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductWithoutManageStock.quantity}}" stepKey="fillProductQuantity"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickAdvancedInventoryLink"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickAdvancedInventoryLink"/> <click selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" stepKey="clickManageStock"/> <checkOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="CheckUseConfigSettingsCheckBox"/> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickDoneButtonOnAdvancedInventorySection"/> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButtonOnAdvancedInventorySection"/> <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductWithoutManageStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> @@ -62,7 +60,9 @@ <amOnPage url="{{StorefrontProductPage.url(virtualProductWithoutManageStock.urlKey)}}" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForStoreFrontPageToLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductWithoutManageStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductWithoutManageStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{virtualProductWithoutManageStock.sku}}"/> + </actionGroup> <!-- Verify customer see product special price on the storefront page --> <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml index 3dfeea2c33af0..b82c6ba13550c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml @@ -45,8 +45,7 @@ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch2"/> <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> <!-- Search for the product by sku and name on the product page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToAdminProductIndex"/> - <waitForPageLoad stepKey="waitForAdminProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToAdminProductIndex"/> <actionGroup ref="FilterProductGridBySkuAndNameActionGroup" stepKey="filerProductsBySkuAndName"> <argument name="product" value="SimpleProductWithCustomAttributeSet"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml index 7748d4bf4db6f..bf0d5d99a23bb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml @@ -93,7 +93,9 @@ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="seeProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront"> + <argument name="productSku" value="$$createConfigProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="$$createConfigProductAttribute.default_value$$" stepKey="seeProductAttributeLabel"/> <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="seeProductAttributeOptions"/> @@ -116,7 +118,9 @@ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront1"/> <dontSee selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="dontSeeProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront1"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront1"> + <argument name="productSku" value="$$createConfigProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="OUT OF STOCK" stepKey="seeProductStatusInStoreFront1"/> <dontSee selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="$$createConfigProductAttribute.default_value$$" stepKey="dontSeeProductAttributeLabel"/> <dontSeeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="dontSeeProductAttributeOptions"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml index c0cbb44ebc681..30bc0315bcf13 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml @@ -45,8 +45,7 @@ </actionGroup> <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage2"/> <!--Go to the Catalog > Products page and create Simple Product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="toggleAddProductBtn"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="chooseAddSimpleProduct"/> <waitForPageLoad stepKey="waitForProductAdded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml index 22e1d7d7c5d9e..4599d0c275214 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml @@ -38,10 +38,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createSimpleProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml index e0375728f316f..7e5ee977d679b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml @@ -65,8 +65,12 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{NewWebSiteData.name}}"/> </actionGroup> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="createSubCategory" stepKey="deleteSubCategory"/> <deleteData createDataKey="createRootCategory" stepKey="deleteRootCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> @@ -74,8 +78,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Grab new store view code--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToNewWebsitePage"/> - <waitForPageLoad stepKey="waitForStoresPageLoad"/> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="navigateToNewWebsitePage"/> <fillField userInput="{{NewWebSiteData.name}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickFirstRow"/> @@ -94,8 +97,12 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct2"/> <!--Reindex and flush cache--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Switch to 'Default Store View' scope and open product page--> <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="SwitchDefaultStoreView"> <argument name="storeViewName" value="'Default Store View'"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml index 2fa91604e1776..84ada79c9ec86 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml @@ -29,8 +29,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="amOnAdminSystemStorePage"/> <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> @@ -41,7 +40,7 @@ <!--Verify Delete Root Category can not be deleted--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> <scrollToTopOfPage stepKey="scrollToTopOfPage2"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandToSeeAllCategories"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name))}}" stepKey="clickRootCategoryInTree"/> <!--Verify Delete button is not displayed--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml index 40bd3bdcfea20..4979b06a1051e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml @@ -27,16 +27,19 @@ <!--Verify Created root Category--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminCategoryBasicFieldSection.CategoryNameInput(NewRootCategory.name)}}" stepKey="seeRootCategory"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsListedInCategoriesTreeActionGroup" stepKey="seeRootCategory"> + <argument name="categoryName" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Delete Root Category--> <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> <!--Verify Root Category is not listed in backend--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories1"/> - <dontSee selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{NewRootCategory.name}}" stepKey="dontSeeRootCategory"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandTheCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup" stepKey="doNotSeeRootCategory"> + <argument name="categoryName" value="{{NewRootCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml index fe07360d6b9ca..4310c6f06219a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml @@ -33,59 +33,48 @@ </after> <!--Create a Store--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> - <see userInput="You saved the store." stepKey="seeSaveMessage"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStore.name}}"/> + <argument name="rootCategory" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Create a Store View--> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> - <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> - <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> - <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> - <see userInput="You saved the store view." stepKey="seeSaveMessage1"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="customStore"/> + </actionGroup> <!--Go To store front page--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> <!--Verify subcategory displayed in store front--> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectMainWebsite1"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeSubCategoryInStoreFront"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="selectCustomStore"> + <argument name="storeName" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Delete SubCategory--> <deleteData createDataKey="category" stepKey="deleteCategory"/> <!--Verify Sub Category is absent in backend --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories2"/> - <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="dontSeeCategoryInTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandTheCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup" stepKey="doNotSeeRootCategory"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Verify Sub Category is not present in Store Front--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad2"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryInStoreFront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeOldCategoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> <!--Verify in Category is not in Url Rewrite grid--> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> - <waitForPageLoad stepKey="waitForUrlRewritePageTopLoad"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillRequestPath"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="seeEmptyRow"/> + <actionGroup ref="AdminSearchDeletedUrlRewriteActionGroup" stepKey="searchingCategoryUrlRewrite"> + <argument name="requestPath" value="{{SimpleRootSubCategory.url_key}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml index 390002f5d9498..bdd1a4b4c70fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml @@ -37,10 +37,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createSimpleProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml index 22c6bf061f274..b7e037b323ee2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml @@ -23,8 +23,7 @@ <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttribute"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newsFromDate.attribute_code}}" stepKey="setAttributeCode"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml index 36001bd0b570a..a6cd3c8b52b23 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml @@ -48,8 +48,7 @@ <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad"/> <see selector="{{AdminProductAttributeSetEditSection.groupTree}}" userInput="$$attribute.attribute_code$$" stepKey="seeAttributeInAttributeGroupTree"/> <!--Open Product Index Page and filter the product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="SimpleProduct2"/> </actionGroup> @@ -82,8 +81,7 @@ <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="dontSeeAttributeInAttributeGroupTree"/> <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="dontSeeAttributeInUnassignedAttributeTree"/> <!--Verify Product Attribute is not present in Product Index Page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductIndexPage"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct1"> <argument name="product" value="SimpleProduct2"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml index f49e1142315eb..dcfcbd699fc6b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml @@ -38,10 +38,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.name$$)}}" stepKey="amOnVirtualProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createVirtualProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createVirtualProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createVirtualProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml index ebbfdc4d72f40..b437d5fb0c868 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml @@ -44,14 +44,12 @@ <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> <dontSeeElement selector="{{ProductAttributeWYSIWYGSection.TinyMCE4($$myProductAttributeCreation.attribute_code$$)}}" stepKey="dontSeeTinyMCE4" /> <fillField selector="{{ProductAttributeWYSIWYGSection.TextArea($$myProductAttributeCreation.attribute_code$$)}}" userInput="Text Area" stepKey="fillContentTextarea" /> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!-- Go to storefront product page, assert product content --> <amOnPage url="{{_defaultProduct.name}}.html" stepKey="navigateToProductPage"/> <waitForPageLoad stepKey="waitForPageLoad5"/> <see userInput="Text Area" stepKey="seeText2" /> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid2"/> - <waitForPageLoad stepKey="waitForPageLoad6"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid2"/> <click selector="{{AdminProductAttributeGridSection.AttributeCode($$myProductAttributeCreation.attribute_code$$)}}" stepKey="navigateToAttributeEditPage2" /> <waitForPageLoad stepKey="waitForPageLoad7" /> <seeOptionIsSelected selector="{{AttributePropertiesSection.InputType}}" userInput="Text Area" stepKey="seeTextAreaSelected" /> @@ -62,8 +60,7 @@ <dontSeeElement selector="{{StorefrontPropertiesSection.EnableWYSIWYG}}" stepKey="dontSeeWYSIWYGEnableField2" /> <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute8" /> <waitForPageLoad stepKey="waitForPageLoad8"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGrid" /> - <waitForPageLoad stepKey="waitForPageLoad9"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGrid"/> <actionGroup ref="SortByIdDescendingActionGroup" stepKey="sortByIdDescending" /> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.enabledFilters}}" visible="true" stepKey="clearAllExistingFilter"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingAfterFilterIsCleared"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml index 843221782ebd9..eb9fe693f8b3b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml @@ -38,7 +38,7 @@ <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillNewName"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterGridByName"> <argument name="product" value="SimpleProduct"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml index f6b2a74eca0f0..f10288bea36d9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml @@ -23,8 +23,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Clear product grid--> <comment userInput="Clear product grid" stepKey="commentClearProductGrid"/> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridToDefaultView"/> <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteProductIfTheyExist"/> <createData stepKey="category1" entity="SimpleSubCategory"/> @@ -37,8 +36,8 @@ </createData> </before> <after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> <click selector="{{AdminDataGridPaginationSection.previousPage}}" stepKey="clickPrevPageOrderGrid"/> <actionGroup ref="AdminDataGridDeleteCustomPerPageActionGroup" stepKey="deleteCustomAddedPerPage"> <argument name="perPage" value="ProductPerPage.productCount"/> @@ -49,8 +48,7 @@ <deleteData stepKey="deleteProduct2" createDataKey="product2"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> <actionGroup ref="AdminDataGridSelectCustomPerPageActionGroup" stepKey="select1OrderPerPage"> <argument name="perPage" value="ProductPerPage.productCount"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml index c319116bf075c..af31b7c1d5c07 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -32,8 +32,12 @@ </createData> <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml index 0214f9141b903..b7a55a90a08d3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml @@ -30,15 +30,14 @@ </createData> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> <!-- Search and select products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="api-simple-product"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml index 845ce340451d1..070c07d9feb7d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml @@ -30,8 +30,7 @@ </after> <!--Open Product Index Page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <!--Search products using keyword --> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml index cd34741b6a68c..35a3a39422185 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml @@ -44,8 +44,7 @@ </after> <!-- Search and select products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="api-simple-product"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml index e4d69e9169613..479c9e5e5bb19 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml @@ -30,15 +30,14 @@ </createData> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> <!-- Search and select products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="api-simple-product"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml index 08b2d924e2a5e..30ab17f65f3c8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml @@ -31,12 +31,11 @@ <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createProductThree" stepKey="deleteProductThree"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="AdminDeleteStoreViewActionGroup"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!-- Search and select products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="api-simple-product"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml index 54921d3fc2dda..b1dad54edcae9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml @@ -14,7 +14,7 @@ <title value="Admin should be able to mass update product statuses in store view scope"/> <description value="Admin should be able to mass update product statuses in store view scope"/> <severity value="AVERAGE"/> - <testCaseId value="MAGETWO-59361"/> + <testCaseId value="MC-28538"/> <group value="Catalog"/> <group value="Product Attributes"/> <group value="SearchEngineElasticsearch"/> @@ -22,69 +22,51 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <!--Create Website --> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> - <argument name="newWebsiteName" value="Second Website"/> - <argument name="websiteCode" value="second_website"/> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> - - <!--Create Store --> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> - <argument name="website" value="Second Website"/> - <argument name="storeGroupName" value="Second Store"/> - <argument name="storeGroupCode" value="second_store"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> </actionGroup> - - <!--Create Store view --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> - <waitForElementVisible selector="//legend[contains(., 'Store View Information')]" stepKey="waitForNewStorePageToOpen"/> - <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> - <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="1" stepKey="enableStoreViewStatus"/> - <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreView"/> - <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal"/> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning"/> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal"/> - <waitForPageLoad stepKey="waitForPageLoad2" time="180"/> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" time="150" stepKey="waitForPageReolad"/> - <see userInput="You saved the store view." stepKey="seeSavedMessage"/> <!--Create a Simple Product 1 --> <actionGroup ref="CreateSimpleProductAndAddToWebsiteActionGroup" stepKey="createSimpleProduct1"> <argument name="product" value="simpleProductForMassUpdate"/> - <argument name="website" value="Second Website"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <!--Create a Simple Product 2 --> <actionGroup ref="CreateSimpleProductAndAddToWebsiteActionGroup" stepKey="createSimpleProduct2"> <argument name="product" value="simpleProductForMassUpdate2"/> - <argument name="website" value="Second Website"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> </before> <after> <!--Delete website --> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> - <argument name="websiteName" value="Second Website"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <!--Delete Products --> - <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> - <argument name="productName" value="simpleProductForMassUpdate.name"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteProduct1"> + <argument name="sku" value="{{simpleProductForMassUpdate.sku}}"/> </actionGroup> - <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct2"> - <argument name="productName" value="simpleProductForMassUpdate2.name"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteProduct2"> + <argument name="sku" value="{{simpleProductForMassUpdate2.sku}}"/> </actionGroup> - <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> </after> <!-- Search and select products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="{{simpleProductForMassUpdate.keyword}}"/> </actionGroup> @@ -92,7 +74,7 @@ <!-- Filter to Second Store View --> <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterStoreView"> - <argument name="customStore" value="'Second Store View'"/> + <argument name="customStore" value="customStore.name"/> </actionGroup> <!-- Select Product 2 --> @@ -136,8 +118,7 @@ <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefaultSecondProductResults"/> <!--Enable the product in Default store view--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex2"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex2"/> <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckboxDefaultStoreView"/> <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckboxDefaultStoreView2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml index bf5fde3b85bba..809a015369ea9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml @@ -30,8 +30,7 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> @@ -39,8 +38,8 @@ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!--Enable Anchor for FirstLevelSubCat Category--> @@ -49,8 +48,7 @@ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting1"/> <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting1"/> <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor1"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave1"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory1"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage1"/> <!--Enable Anchor for SimpleSubCategory Category and add products to the Category--> @@ -64,13 +62,16 @@ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory2"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory2"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Open Category in store front page--> <amOnPage url="/$$createDefaultCategory.name$$/{{FirstLevelSubCat.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> @@ -92,8 +93,7 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree2"/> <!--Move SubCategory under Default Category--> <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml index 4dbbdc8f4399e..9100e6027a52f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml @@ -23,8 +23,12 @@ <field key="is_active">true</field> </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> @@ -33,23 +37,20 @@ <!--Open category page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(FirstLevelSubCat.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <!--Create second level category--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SubCategory.name}}" stepKey="addSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave1"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory1"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage1"/> <!--Create third level category under second level category--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName1"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory2"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory2"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#" /> @@ -74,8 +75,7 @@ <!--Open Category Page --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree2"/> <!--Move the third level category under first level category --> <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree($$createDefaultCategory.name$$)}}" stepKey="m0oveCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml index 116df566f2bd0..0e056e4bb7078 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml @@ -33,8 +33,7 @@ <!--Open Category page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> @@ -42,7 +41,7 @@ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <!--Create a Subcategory under _defaultCategory category--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> @@ -54,12 +53,13 @@ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory1"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!--Run re-index task --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify category displayed in store front page--> <amOnPage url="/$$createDefaultCategory.name$$/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> @@ -81,8 +81,7 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree2"/> <!--Move SubCategory under Default Category--> <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml index fd9e50928d748..654ddb4d8d872 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml @@ -33,20 +33,17 @@ <!-- Open Category Page --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <!-- Create three level deep sub Category --> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickAddSubCategoryButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="fillSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFirstLevelSubCategory"/> - <waitForPageLoad stepKey="waitForFirstLevelCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveFirstLevelSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButtonAgain"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SecondLevelSubCat.name}}" stepKey="fillSecondLevelSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSecondLevelSubCategory"/> - <waitForPageLoad stepKey="waitForSecondLevelCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSecondLevelSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSaveSuccessMessage"/> <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#" /> @@ -68,8 +65,7 @@ <!-- Move Category to another position in category tree and click ok button--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openTheAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SecondLevelSubCat.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="DragCategory"/> <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessageForOneMoreTime"/> <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml index 055f4e23cd9e7..5d257bf0648cd 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"/> @@ -63,8 +61,7 @@ <!-- Create subcategory <Sub1> of the anchored category --> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory1"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSaveSuccessMessage"/> <!-- Assign <product1> to the <Sub1> --> @@ -76,10 +73,8 @@ <fillField userInput="{{SimpleSubCategory.name}}" selector="{{AdminProductFormSection.searchCategory}}" stepKey="fillSearch"/> <waitForPageLoad stepKey="waitForSubCategory"/> <click selector="{{AdminProductFormSection.selectCategory(SimpleSubCategory.name)}}" stepKey="selectSub1Category"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickDone"/> - <waitForPageLoad stepKey="waitForApplyCategory"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="waitForSavingChanges"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickDone"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSave"/> <!-- Enable `Use Categories Path for Product URLs` on Stores -> Configuration -> Catalog -> Catalog -> Search Engine Optimization --> <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="onConfigPage"/> @@ -92,8 +87,7 @@ <see selector="{{AdminIndexManagementSection.successMessage}}" userInput="You saved the configuration." stepKey="seeMessage"/> <!-- Navigate to the Catalog > Products --> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="onCatalogProductPage"/> - <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="onCatalogProductPage"/> <!-- Click on <product1>: Product page opens--> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterProduct"> @@ -107,10 +101,8 @@ <click selector="{{AdminProductFormSection.unselectCategories(SimpleSubCategory.name)}}" stepKey="removeCategory"/> <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="openDropDown"/> <checkOption selector="{{AdminProductFormSection.selectCategory($$createSecondCategory.name$$)}}" stepKey="selectCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="pressButtonDone"/> - <waitForPageLoad stepKey="waitForApplyCategory2"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="pushButtonSave"/> - <waitForPageLoad stepKey="waitForSavingProduct"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="pressButtonDone"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="pushButtonSave"/> <!--Product is saved --> <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSuccessMessage"/> @@ -165,8 +157,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAdmin"/> <!-- Navigate to the Catalog > Products: Navigate to the Catalog>Products --> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="amOnProductPage"/> - <waitForPageLoad stepKey="waitForProductsPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="amOnProductPage"/> <!-- Click on <product1> --> <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="openSimpleProduct"> @@ -179,10 +170,8 @@ <fillField userInput="{$grabNameSubCategory}" selector="{{AdminProductFormSection.searchCategory}}" stepKey="fillSearchField"/> <waitForPageLoad stepKey="waitForSearchSubCategory"/> <click selector="{{AdminProductFormSection.selectCategory({$grabNameSubCategory})}}" stepKey="selectSubCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickButtonDone"/> - <waitForPageLoad stepKey="waitForCategoryApply"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickButtonSave"/> - <waitForPageLoad stepKey="waitForSaveChanges"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickButtonDone"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickButtonSave"/> <!-- Product is saved successfully --> <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSaveMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml index c1cfcf7ebe10f..4c076c9a495fc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml @@ -15,45 +15,38 @@ <title value="Use Default Value checkboxes should be checked for new website scope"/> <description value="Use Default Value checkboxes for product attribute should be checked for new website scope"/> <severity value="BLOCKER"/> - <testCaseId value="MAGETWO-92454"/> + <testCaseId value="MC-25783"/> <group value="Catalog"/> </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + </before> + <after> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> - <argument name="websiteName" value="Second Website"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> - <argument name="newWebsiteName" value="Second Website"/> - <argument name="websiteCode" value="second_website"/> - </actionGroup> - <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> - <argument name="website" value="Second Website"/> - <argument name="storeGroupName" value="Second Store"/> - <argument name="storeGroupCode" value="second_store"/> - </actionGroup> - - <!--Create Store view --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> - <waitForElementVisible selector="//legend[contains(., 'Store View Information')]" stepKey="waitForNewStorePageToOpen"/> - <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> - <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="1" stepKey="enableStoreViewStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickStoreViewSaveButton"/> - <waitForElementVisible selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" stepKey="waitForAcceptNewStoreViewCreationModal" /> - <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="AcceptNewStoreViewCreation"/> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReload"/> - <see userInput="You saved the store view." stepKey="seeSaveMessage"/> <!--Create a Simple Product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> - <waitForPageLoad stepKey="waitForProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGrid"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillProductName"/> @@ -63,17 +56,17 @@ <!-- Add product to second website and save the product --> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsites"/> - <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> <waitForLoadingMaskToDisappear stepKey="waitForProductPageSave"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> <!-- switch to the second store view --> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStore.name)}}" stepKey="chooseStoreView"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> + <see userInput="{{customStore.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> <!-- Check if Use Default Value checkboxes are checked --> <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatusUseDefault}}" stepKey="seeProductStatusCheckboxChecked"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml index 659521ed9e467..94d7ea14b096c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml @@ -96,8 +96,7 @@ </after> <!--Open Product Index Page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <!--Select SimpleProduct --> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml index 1c536df7c2efb..8e728fc6e1f27 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml @@ -35,8 +35,12 @@ </actionGroup> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProductFirst" stepKey="deleteFirstSimpleProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml index 9536ee030cdf8..d677eda5b0920 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml @@ -94,8 +94,7 @@ <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductGridFilters"/> <!--Sort by custom attribute DESC using grabbed value--> <conditionalClick selector="{{AdminProductGridSection.columnHeader($$createDropdownAttribute.attribute[frontend_labels][0][label]$$)}}" dependentSelector="{{AdminProductGridSection.columnHeader($$createDropdownAttribute.attribute[frontend_labels][0][label]$$)}}" visible="true" stepKey="ascendSortByCustomAttribute"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml index d47730a99308b..449d201393206 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml @@ -26,8 +26,7 @@ <deleteData createDataKey="createSimpleProductWithDate" stepKey="deleteProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttribute"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="Set Product as New from Date" stepKey="setAttributeLabel"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromGrid"/> @@ -38,16 +37,14 @@ <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> <waitForPageLoad stepKey="waitForSaveAttribute"/> <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad time="30" stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <click selector="{{AdminProductGridFilterSection.columnsDropdown}}" stepKey="openColumnsdropDown1"/> <checkOption selector="{{AdminProductGridFilterSection.viewColumnOption('Set Product as New from Date')}}" stepKey="showProductAsNewColumn"/> <click selector="{{AdminProductGridFilterSection.columnsDropdown}}" stepKey="closeColumnsDropdown1"/> <seeElement selector="{{AdminProductGridSection.columnHeader('Set Product as New from Date')}}" stepKey="seeNewFromDateColumn"/> <waitForPageLoad stepKey="waitforFiltersToApply"/> <actionGroup ref="FilterProductGridBySetNewFromDateActionGroup" stepKey="filterProductGridToCheckSetAsNewColumn"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnFirstRowProductGrid"/> - <waitForPageLoad stepKey="waitForProductEditPageToLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnFirstRowProductGrid"/> <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveAndCloseProductForm"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="expandFilters"/> <seeInField selector="{{AdminProductGridFilterSection.newFromDateFilter}}" userInput="05/16/2018" stepKey="checkForNewFromDate"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml new file mode 100644 index 0000000000000..bfa80c2e24b48 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminProductGridUrlFilterApplierTest"> + <annotations> + <features value="Catalog"/> + <stories value="Filter product using GET URL parameter"/> + <title value="Verify that filter is applied on product grid when filters parameter is set on url"/> + <description value="Accessing product grid url with filters parameter"/> + <severity value="MAJOR"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> + <group value="product"/> + </annotations> + + <before> + <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> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + + <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="SKU: $createSimpleProduct.sku$" stepKey="seeProductNameFilter"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml index ae63158990b96..081eceede0b35 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml @@ -23,8 +23,7 @@ </before> <after> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttribute"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid1"/> <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="Enable Product" stepKey="setAttributeLabel1"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid1"/> @@ -37,8 +36,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttribute"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="Enable Product" stepKey="setAttributeLabel"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> @@ -48,8 +46,7 @@ <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> <waitForPageLoad stepKey="waitForAttributeToSave"/> <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad time="30" stepKey="waitForProductGridPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickOnAddSimpleProduct"/> <waitForPageLoad stepKey="waitForProductEditToLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml index 9d82edd0fb50c..6cbf03a02f3b0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml @@ -29,12 +29,20 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductForm"/> <!--Assert simple product on Admin product page grid--> <comment userInput="Assert simple product in Admin product page grid" stepKey="commentAssertProductOnAdmin"/> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogSimpleProductPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogSimpleProductPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterSimpleProductGridBySku"> <argument name="sku" value="$$createProduct.sku$$"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeSimpleProductNameInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeSimpleProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeSimpleProductNameInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeSimpleProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Simple Product"/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearSimpleProductFilters"/> <!--Assert simple product on storefront--> <comment userInput="Assert simple product on storefront" stepKey="commentAssertSimpleProductOnStorefront"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml index 12d654508d7d7..2311369db48f6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml @@ -49,12 +49,20 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveDownloadableProductForm"/> <!--Assert downloadable product on Admin product page grid--> <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertDownloadableProductOnAdmin"/> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySku"> <argument name="sku" value="$$createProduct.sku$$"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeDownloadableProductNameInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Downloadable Product" stepKey="seeDownloadableProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeDownloadableProductNameInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeDownloadableProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Downloadable Product"/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearDownloadableProductFilters"/> <!--Assert downloadable product on storefront--> <comment userInput="Assert downloadable product on storefront" stepKey="commentAssertDownloadableProductOnStorefront"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml index 96ee795998459..99fe4dd0c135d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml @@ -26,7 +26,7 @@ </before> <after> <deleteData createDataKey="createProduct" stepKey="deleteProductWithOptions"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml index 00eaa623e2bca..521256cf57dd5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml @@ -22,12 +22,11 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateSimpleProduct"> <argument name="product" value="SimpleProduct3"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml index 6cc1b256e5ec9..4a544b60f15b6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml @@ -22,12 +22,11 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="defaultVirtualProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml index 6cd76c4cc06b8..1707fda9e3edb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml @@ -26,53 +26,56 @@ <requiredEntity createDataKey="category"/> </createData> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> - <argument name="newWebsiteName" value="FirstWebSite"/> - <argument name="websiteCode" value="FirstWebSiteCode"/> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore" after="createWebsite"> - <argument name="website" value="FirstWebSite"/> - <argument name="storeGroupName" value="NewStore"/> - <argument name="storeGroupCode" value="Base1"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> </actionGroup> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView" after="createNewStore"> - <argument name="StoreGroup" value="staticFirstStoreGroup"/> - <argument name="customStore" value="staticStore"/> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> </actionGroup> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite" after="createCustomStoreView"> - <argument name="newWebsiteName" value="SecondWebSite"/> - <argument name="websiteCode" value="SecondWebSiteCode"/> + <argument name="newWebsiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{secondCustomWebsite.code}}"/> </actionGroup> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStore" after="createSecondWebsite"> - <argument name="website" value="SecondWebSite"/> - <argument name="storeGroupName" value="SecondStore"/> - <argument name="storeGroupCode" value="Base2"/> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> </actionGroup> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView2" after="createSecondStore"> - <argument name="StoreGroup" value="staticStoreGroup"/> - <argument name="customStore" value="staticSecondStore"/> + <argument name="StoreGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> </actionGroup> </before> <after> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> - <argument name="websiteName" value="FirstWebSite"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> - <argument name="websiteName" value="SecondWebSite"/> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> </actionGroup> <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> <deleteData createDataKey="product" stepKey="deleteFirstProduct"/> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <!--Open created product--> @@ -90,15 +93,14 @@ </actionGroup> <!--"Product in Websites": select both Websites--> <actionGroup ref="ProductSetWebsiteActionGroup" stepKey="ProductSetWebsite1"> - <argument name="website" value="FirstWebSite"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="ProductSetWebsiteActionGroup" stepKey="ProductSetWebsite2"> - <argument name="website" value="SecondWebSite"/> + <argument name="website" value="{{secondCustomWebsite.name}}"/> </actionGroup> <!--Go to "Catalog" -> "Products". Open created product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoaded"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductPage"/> <click selector="{{AdminProductGridSection.productGridNameProduct($$product.name$$)}}" stepKey="openCreatedProduct"/> <waitForPageLoad stepKey="waitForCreatedProductOpened"/> @@ -110,7 +112,7 @@ <!--Switch to "Store view 1"--> <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="selectStoreView"> - <argument name="storeViewName" value="Store View"/> + <argument name="storeViewName" value="{{customStore.name}}"/> </actionGroup> <!-- Assert product first image not in admin product form --> @@ -120,7 +122,7 @@ <!--Switch to "Store view 2"--> <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="selectSecondStoreView"> - <argument name="storeViewName" value="Second Store View"/> + <argument name="storeViewName" value="{{SecondStoreUnique.name}}"/> </actionGroup> <!-- Verify that Image 1 is deleted from the Second Store View list --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml index 2444165fa1b39..394e16b8412a5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml @@ -39,7 +39,7 @@ <expectedResult type="string">rgb(226, 38, 38)</expectedResult> </assertEquals> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndexPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="addProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="addSimpleProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml index 9819890ed3751..de116b26d1414 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml @@ -37,8 +37,7 @@ </after> <!-- Go to the first product edit page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> <argument name="product" value="$$firstProduct$$"/> @@ -106,7 +105,7 @@ <dontSeeElement selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="dontSeeErrorPng"/> <!-- Save the first product and go to the storefront --> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <amOnPage url="$$firstProduct.name$$.html" stepKey="goToStorefront"/> <waitForPageLoad stepKey="waitForStorefront"/> @@ -123,8 +122,7 @@ <seeElement selector=".products-grid img[src*='placeholder/small_image.jpg']" stepKey="seePlaceholder"/> <!-- Go to the second product edit page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex2"/> - <waitForPageLoad stepKey="wait2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex2"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku2"> <argument name="product" value="$$secondProduct$$"/> @@ -143,12 +141,15 @@ <!-- Save the second product --> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Go to the admin grid and see the uploaded image --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex3"/> - <waitForPageLoad stepKey="wait3"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex3"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid3"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku3"> <argument name="product" value="$$secondProduct$$"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml index ec82bdcf5bc94..e1c97b205d4fa 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml @@ -33,8 +33,7 @@ </after> <!-- Go to the product edit page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> <argument name="product" value="$$product$$"/> @@ -85,7 +84,7 @@ <waitForPageLoad stepKey="waitForHide3"/> <!-- Save the product with all 3 images --> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!-- Go to the product page and see the Base image --> <amOnPage url="$$product.name$$.html" stepKey="goToProductPage"/> @@ -98,8 +97,7 @@ <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc('/adobe-small')}}" stepKey="seeThumb"/> <!-- Go to the admin grid and see the Thumbnail image --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex2"/> - <waitForPageLoad stepKey="wait2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex2"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku2"> <argument name="product" value="$$product$$"/> @@ -107,8 +105,7 @@ <seeElement selector="{{AdminProductGridSection.productThumbnailBySrc('/adobe-thumb')}}" stepKey="seeBaseInGrid"/> <!-- Go to the product edit page again --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex3"/> - <waitForPageLoad stepKey="wait5"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex3"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid3"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku3"> <argument name="product" value="$$product$$"/> @@ -120,11 +117,10 @@ <click selector="{{AdminProductImagesSection.nthRemoveImageBtn('1')}}" stepKey="removeImage1"/> <click selector="{{AdminProductImagesSection.nthRemoveImageBtn('2')}}" stepKey="removeImage2"/> <click selector="{{AdminProductImagesSection.nthRemoveImageBtn('3')}}" stepKey="removeImage3"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct2"/> <!-- Check admin grid for placeholder --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex4"/> - <waitForPageLoad stepKey="wait6"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex4"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid4"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku4"> <argument name="product" value="$$product$$"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml index 51a91a17ff41a..fc18531eca350 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml @@ -34,8 +34,7 @@ </after> <!-- Create product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> @@ -51,7 +50,7 @@ <fillField selector="{{AdminProductContentSection.shortDescriptionTextArea}}" userInput="This is the short description" stepKey="fillShortDescription"/> <!--save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Edit content--> @@ -61,7 +60,7 @@ <fillField selector="{{AdminProductContentSection.shortDescriptionTextArea}}" userInput="EDIT ~ This is the short description ~ EDIT" stepKey="editShortDescription"/> <!--save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAfterEdit"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAfterEdit"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <!--Checking content admin--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml index 534924e0f70c9..3c90de572988c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml @@ -35,7 +35,7 @@ <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> <argument name="product" value="SimpleProduct3"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct0" stepKey="deleteSimpleProduct0"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> @@ -46,8 +46,7 @@ </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="SimpleProduct3"/> </actionGroup> @@ -61,7 +60,7 @@ </actionGroup> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Add another related product--> @@ -73,7 +72,7 @@ <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.removeRelatedProduct($$simpleProduct0.sku$$)}}" stepKey="removeRelatedProduct"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAfterEdit"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAfterEdit"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <!--See related product in admin--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml index 71e827a64ae2d..73aeed3af4fb0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml @@ -31,7 +31,9 @@ <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterEnableWebUrlOptions"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableWebUrlOptions"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -40,17 +42,20 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <actionGroup ref="GoToProductCatalogPageActionGroup" stepKey="goToProductCatalogPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Assign Custom Website to Simple Product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> - <waitForPageLoad stepKey="waitForCatalogProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGrid"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="assignCustomWebsiteToProduct"> @@ -62,13 +67,11 @@ <uncheckOption selector="{{ProductInWebsitesSection.website(_defaultWebsite.name)}}" stepKey="deselectMainWebsite"/> <checkOption selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectWebsite"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> - <waitForLoadingMaskToDisappear stepKey="waitForProductPageToSaveAgain"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> <!--Navigate To Product Grid To Check Website Sorting--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGridToSortByWebsite"/> - <waitForPageLoad stepKey="waitForCatalogProductGridLoaded"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGridToSortByWebsite"/> <!--Sorting works (By Websites) ASC--> <click selector="{{AdminProductGridSection.columnHeader('Websites')}}" stepKey="clickWebsitesHeaderToSortAsc"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml index e0e517defdeac..e562faf523929 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml @@ -33,8 +33,7 @@ </after> <!--Open Store Page --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="amOnAdminSystemStorePage"/> <!--Create Custom Store --> <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> @@ -51,12 +50,11 @@ <!--Update Category--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> - <waitForPageLoad stepKey="waitForCateforyToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveUpdatedCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <waitForPageLoad stepKey="waitForPageToLoad1"/> <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml index a4ba859714982..21f2b622c2ebd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml @@ -30,11 +30,10 @@ <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Update category and make category inactive--> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontCategoryIsChecked"/> @@ -47,7 +46,7 @@ <!--Verify Inactive Category in category page --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree1"/> <seeElement selector="{{AdminCategoryContentSection.categoryInTree(_defaultCategory.name)}}" stepKey="assertCategoryInTree" /> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory1"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle1" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml index 0ca8e74c4e59e..f4d464455491b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml @@ -32,16 +32,12 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <!--Open store page --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <!--Create Custom Store --> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStore.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> <!--Create Store View--> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> @@ -50,32 +46,40 @@ </actionGroup> <!--Verify created SubCAtegory is present on Store Front --> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="seeCustomStore"> + <argument name="storeName" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goToCategoryPage"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryInStoreFront"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <!--Update Category--> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTreeUnderRoot(SimpleRootSubCategory.name)}}" stepKey="clickOnSubcategoryIsUndeRootCategory"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> - <waitForPageLoad stepKey="waitForCateforyToSave"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandCategoryTree"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminChangeCategoryNameActionGroup" stepKey="updateCategoryName"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <!--Verify the Category is not present in Store Front--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> - <waitForPageLoad stepKey="waitForPageToLoaded2"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="dontSeeCatergoryInStoreFront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeOldCategoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Verify the Updated Category is present in Store Front--> - <amOnPage url="/{{NewRootCategory.name}}/{{_defaultCategory.name}}.html" stepKey="seeTheUpdatedCategoryInStoreFront"/> - <waitForPageLoad stepKey="waitForPageToLoaded3"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeUpdatedCatergoryInStoreFront"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeUpdatedCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml index 87d7f91431dc3..4389bf4bd6383 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml @@ -46,7 +46,7 @@ <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyDefaultValueCheckbox}}" stepKey="uncheckUseDefaultUrlKey"/> <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{_defaultCategory.name_lwr}}-hattest" stepKey="enterURLKey"/> <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckRedirect1"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterFirstSeoUpdate"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategoryAfterFirstSeoUpdate"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> <amOnPage url="" stepKey="goToStorefront"/> <waitForPageLoad stepKey="waitForFrontendLoad"/> @@ -64,7 +64,7 @@ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection2"/> <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{_defaultCategory.name_lwr}}" stepKey="enterOriginalURLKey"/> <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckRedirect2"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterOriginalSeoKey"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategoryAfterOriginalSeoKey"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterOriginalSeoKey"/> <amOnPage url="" stepKey="goToStorefrontAfterOriginalSeoKey"/> <waitForPageLoad stepKey="waitForFrontendLoadAfterOriginalSeoKey"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml index 6a12b991bd225..3bbe8722d8bfc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml @@ -32,16 +32,12 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <!--Open Store Page --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <!--Create Custom Store --> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStore.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> <!--Create Store View--> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> @@ -50,34 +46,37 @@ </actionGroup> <!--Verify Category in Store View--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> - <waitForPageLoad stepKey="waitForSystemStorePage1"/> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> - <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="selectCategory"/> - <waitForPageLoad stepKey="waitForProductToLoad"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="switchToCustomStore"> + <argument name="storeName" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="selectCategory"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> <!--Update URL Key--> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCategory1"/> - <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSearchEngineOptimization"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> - <clearField selector="{{AdminCategorySEOSection.UrlKeyInput}}" stepKey="clearUrlKeyField"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="newurlkey" stepKey="enterURLKey"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterFirstSeoUpdate"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedSubCategory"> + <argument name="Category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="ChangeSeoUrlKeyActionGroup" stepKey="changeSeoUrlKey"> + <argument name="value" value="newurlkey"/> + </actionGroup> <!--Open Category Store Front Page--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> - <waitForPageLoad stepKey="waitForSystemStorePage3"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCategoryOnNavigation1"/> - <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="selectCategory2"/> - <waitForPageLoad stepKey="waitForProductToLoad1"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategory"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Verify Updated URLKey is present--> - <seeInCurrentUrl stepKey="verifyUpdatedUrlKey" url="newurlkey.html"/> + <actionGroup ref="StorefrontAssertProperUrlIsShownActionGroup" stepKey="seeUpdatedUrlkey"> + <argument name="urlPath" value="newurlkey.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml index db6cfce167bce..373a14c6bb0e7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml @@ -31,7 +31,7 @@ <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Update Category name,description, urlKey, meta title and disable Include in Menu--> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="fillCategoryName"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> @@ -44,8 +44,7 @@ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillUpdatedUrlKey"/> <fillField selector="{{AdminCategorySEOSection.MetaTitleInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="fillUpdatedMetaTitle"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <!--Open UrlRewrite Page--> @@ -65,7 +64,7 @@ <!--Verify Updated fields in Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree1"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCreatedCategory1"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml new file mode 100644 index 0000000000000..051495b257012 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.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="AdminUpdateCategoryWithProductsDefaultSortingTest"> + <annotations> + <features value="Catalog"/> + <stories value="Update categories"/> + <title value="Update category, sort products by default sorting"/> + <description value="Login as admin, update category and sort products"/> + <testCaseId value="MC-25667"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct" /> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + + <!--Open Category Page--> + <actionGroup ref="GoToAdminCategoryPageByIdActionGroup" stepKey="goToAdminCategoryPage"> + <argument name="id" value="$createCategory.id$"/> + </actionGroup> + + <!--Update Product Display Setting--> + <waitForElementVisible selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" stepKey="waitForDisplaySettingsSection"/> + <conditionalClick selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" dependentSelector="{{AdminCategoryDisplaySettingsSection.displayMode}}" visible="false" stepKey="openDisplaySettingsSection"/> + <waitForElementVisible selector="{{CategoryDisplaySettingsSection.productListCheckBox}}" stepKey="waitForAvailableProductListCheckbox"/> + <click selector="{{CategoryDisplaySettingsSection.productListCheckBox}}" stepKey="enableTheAvailableProductList"/> + <selectOption selector="{{CategoryDisplaySettingsSection.productList}}" parameterArray="['Product Name', 'Price']" stepKey="selectPrice"/> + <waitForElementVisible selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" stepKey="waitForDefaultProductList"/> + <click selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" stepKey="enableTheDefaultProductList"/> + <selectOption selector="{{CategoryDisplaySettingsSection.defaultProductList}}" userInput="name" stepKey="selectProductName"/> + + <!--Add Products in Category--> + <actionGroup ref="AdminCategoryAssignProductActionGroup" stepKey="assignSimpleProductToCategory"> + <argument name="productSku" value="$simpleProduct.sku$"/> + </actionGroup> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategory"/> + + <!--Verify Category Title--> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryNamePageTitle" /> + + <!--Verify Category in store front page--> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="openStorefrontCategoryPage"/> + + <!--Verify Product in Category--> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="assertSimpleProductOnCategoryPage"> + <argument name="productName" value="$simpleProduct.name$"/> + </actionGroup> + + <!--Verify product name and sku on Store Front--> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="assertProductOnStorefrontProductPage"> + <argument name="product" value="$simpleProduct$"/> + </actionGroup> + </test> +</tests> + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml index 9b827550a6817..f82294ece6478 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml @@ -7,15 +7,18 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminUpdateCategoryWithProductsTest"> + <test name="AdminUpdateCategoryWithProductsTest" deprecated="Use AdminUpdateCategoryWithProductsDefaultSortingTest instead"> <annotations> <stories value="Update categories"/> - <title value="Update category, sort products by default sorting"/> + <title value="DEPRECATED. Update category, sort products by default sorting"/> <description value="Login as admin, update category and sort products"/> <testCaseId value="MC-6059"/> <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <skip> + <issueId value="DEPRECATED">Use AdminUpdateCategoryWithProductsDefaultSortingTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> @@ -30,7 +33,7 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> @@ -55,8 +58,7 @@ <waitForPageLoad stepKey="waitFroPageToLoad1"/> <scrollTo selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="scrollToTableRow"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProduct1FromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> @@ -64,8 +66,12 @@ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Verify Category in store front page--> <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="seeDefaultProductPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml index 1950b385c4a68..208b588493112 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml @@ -32,20 +32,25 @@ <argument name="storeView" value="customStoreFR"/> </actionGroup> <!--Run full reindex and clear caches --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> + <!-- Run cron --> + <magentoCron stepKey="runAllCronJobs"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> </actionGroup> @@ -57,9 +62,11 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Select Created Category--> - <magentoCLI command="indexer:reindex" stepKey="reindexBeforeFlow"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexBeforeFlow"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForTheCategoryPageToLoaded"/> <!--Add Products in Category--> @@ -73,15 +80,16 @@ <waitForPageLoad stepKey="waitFroPageToLoad1"/> <scrollTo selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="scrollToTableRow"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!--Open Index Management Page and verify flat categoryIndex status--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToLoad"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <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 1214ba879f211..a688dea47a0c4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml @@ -58,19 +58,20 @@ <dontSee selector="{{StorefrontHeaderSection.NavigationCategoryByName(CatNotIncludeInMenu.name)}}" stepKey="dontSeeCategoryOnNavigation"/> <!-- Select created category and enable Include In Menu option--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatNotIncludeInMenu.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="enableIncludeInMenuOption"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <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 490f8dbdc4f81..27a834833ed76 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml @@ -45,7 +45,9 @@ <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData stepKey="deleteCategory" createDataKey="createCategory" /> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> @@ -57,7 +59,7 @@ </after> <!-- Select Created Category--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForPageToLoaded"/> <!--Update Category Name and Description --> @@ -65,15 +67,16 @@ <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent"/> <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent"/> <fillField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="fillUpdatedDescription"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToLoad"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <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"/> @@ -90,7 +93,7 @@ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnNavigation1"/> <!-- Verify Updated Category Name and description on Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree1"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectUpdatedCategory"/> <waitForPageLoad stepKey="waitForUpdatedCategoryPageToLoad"/> <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedSubCategoryName"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml index 6edffb923d540..5f7cecfde188a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -42,8 +42,7 @@ </after> <!-- Search default simple product in grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -64,8 +63,7 @@ <!-- Update default simple product with name --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductDataOverriding.name}}" stepKey="fillSimpleProductName"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickButtonSave"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml index e954de90ef542..8a05ed9d64b8b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -42,8 +42,7 @@ </after> <!-- Search default simple product in grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -62,8 +61,7 @@ <!-- Update default simple product with price --> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductDataOverriding.price}}" stepKey="fillSimpleProductPrice"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickButtonSave"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml index f5b0fb8054dc1..300b312612253 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml @@ -27,8 +27,12 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> <!--TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush full_page" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> @@ -40,8 +44,7 @@ </after> <!-- Search default simple product in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -74,19 +77,17 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductTierPrice300InStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -125,7 +126,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductTierPrice300InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="seeProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSku"> + <argument name="productSku" value="{{simpleProductTierPrice300InStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductTierPrice300InStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml index d20594461173b..a9630aba467c6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml @@ -34,8 +34,7 @@ </after> <!-- Search default simple product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -53,15 +52,13 @@ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductDisabled.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickEnableProductLabelToDisableProduct"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductDisabled.name}}" stepKey="fillSimpleProductNameInNameFilter"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml index 5fa7acbeb8de9..aa1b1ae702914 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml @@ -38,8 +38,7 @@ </after> <!-- Search default simple product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -52,11 +51,10 @@ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="fillSimpleProductPrice"/> <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{simpleProductEnabledFlat.productTaxClass}}" stepKey="selectProductTaxClass"/> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductEnabledFlat.quantity}}" stepKey="fillSimpleProductQuantity"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPage"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickAdvancedInventoryLink"/> <conditionalClick selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" dependentSelector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" visible="true" stepKey="checkUseConfigSettingsCheckBox"/> <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="No" stepKey="selectManageStock"/> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickDoneButtonOnAdvancedInventorySection"/> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButtonOnAdvancedInventorySection"/> <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductEnabledFlat.status}}" stepKey="selectStockStatusInStock"/> <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductEnabledFlat.weight}}" stepKey="fillSimpleProductWeight"/> <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductEnabledFlat.weightSelect}}" stepKey="selectProductWeight"/> @@ -67,20 +65,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductEnabledFlat.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductEnabledFlat.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -95,8 +91,7 @@ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="seeSimpleProductPrice"/> <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{simpleProductEnabledFlat.productTaxClass}}" stepKey="seeProductTaxClass"/> <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductEnabledFlat.quantity}}" stepKey="seeSimpleProductQuantity"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickTheAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPageLoad"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickTheAdvancedInventoryLink"/> <see selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="No" stepKey="seeManageStock"/> <click selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryCloseButton}}" stepKey="clickDoneButtonOnAdvancedInventory"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductEnabledFlat.status}}" stepKey="seeSimpleProductStockStatus"/> @@ -119,7 +114,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeSimpleProductSkuOnStoreFrontPage"> + <argument name="productSku" value="{{simpleProductEnabledFlat.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductEnabledFlat.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml index 4b21d1337e9b7..86fac835ce44d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,20 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductNotVisibleIndividually.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductNotVisibleIndividually.urlKey}}" stepKey="fillSimpleProductUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="fillSimpleProductNameInNameFilter"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml index 4256f93ea41d1..af3861e4e0b64 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml @@ -34,8 +34,7 @@ </after> <!--Search default simple product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -46,18 +45,16 @@ <scrollTo selector="{{AdminProductFormSection.productStockStatus}}" stepKey="scroll"/> <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> <click selector="{{AdminProductFormSection.unselectCategories($$initialCategoryEntity.name$$)}}" stepKey="unselectCategories"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategory"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategory"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!--Search default simple product in the grid page --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="OpenCategoryCatalogPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$initialCategoryEntity.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="clickAdminCategoryProductSection"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml index 58db163bed720..320edba5feeff 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,20 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice245InStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="fillSimpleProductUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -95,7 +92,9 @@ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="seeUrlKey"/> <!--Run re-index task --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify customer see updated simple product link on category page --> <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> @@ -107,7 +106,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice245InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeSimpleProductSkuOnStoreFrontPage"> + <argument name="productSku" value="{{simpleProductRegularPrice245InStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductRegularPrice245InStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml index 5e9a48f659d6b..77c3e7548a3cf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,20 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice32501InStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -95,7 +92,9 @@ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="seeUrlKey"/> <!--Run re-index task --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify customer see updated simple product link on category page --> <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> @@ -107,7 +106,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice32501InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="seeProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSku"> + <argument name="productSku" value="{{simpleProductRegularPrice32501InStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductRegularPrice32501InStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml index 3d37b54dfa439..39dab0b08915c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,20 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice325InStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice325InStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -104,7 +101,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice325InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="seeProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSku"> + <argument name="productSku" value="{{simpleProductRegularPrice325InStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductRegularPrice325InStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml index 855a2b1d9b0cc..670030d1d98ea 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,7 +57,7 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPriceCustomOptions.urlKey}}" stepKey="fillUrlKey"/> <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> @@ -76,15 +75,13 @@ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price_type}}" stepKey="selectOptionPriceType"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_sku}}" stepKey="fillOptionSku"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!--Verify customer see success message--> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!--Search updated simple product(from above step) in the grid page--> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -124,7 +121,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeSimpleProductSkuOnStoreFrontPage"> + <argument name="productSku" value="{{simpleProductRegularPriceCustomOptions.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductRegularPriceCustomOptions.storefrontStatus}}</expectedResult> @@ -150,9 +149,7 @@ <!-- Verify added Product in cart --> <selectOption selector="{{StorefrontProductPageSection.customOptionDropDown}}" userInput="{{simpleProductCustomizableOption.option_0_title}} +$98.00" stepKey="selectCustomOption"/> <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1" stepKey="fillProductQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> - <waitForPageLoad stepKey="waitForProductToAddInCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeYouAddedSimpleprod4ToYourShoppingCartSuccessSaveMessage"/> <seeElement selector="{{StorefrontMinicartSection.quantity(1)}}" stepKey="seeAddedProductQuantityInCart"/> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml index af836efcf6be6..441bc9b8f8005 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,19 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32503OutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -102,7 +100,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeSimpleProductSkuOnStoreFrontPage"> + <argument name="productSku" value="{{simpleProductRegularPrice32503OutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductRegularPrice32503OutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml index 5221510fd4dce..616c38e326a62 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml @@ -43,8 +43,7 @@ <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!-- Open 3rd Level category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createThreeLevelNestedCategories.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> @@ -55,8 +54,7 @@ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="updatedurl" stepKey="updateUrlKey"/> <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckPermanentRedirectCheckBox"/> <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> - <waitForPageLoad stepKey="waitForCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveUpdatedCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!-- Get Category Id --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml index 505ca583da3f4..9d4bc5f184c9c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml @@ -41,8 +41,7 @@ <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!-- Open 3rd Level category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createThreeLevelNestedCategories.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> @@ -53,8 +52,7 @@ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="updateredirecturl" stepKey="updateUrlKey"/> <checkOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="checkPermanentRedirectCheckBox"/> <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> - <waitForPageLoad stepKey="waitForCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveUpdatedCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!-- Get Category ID --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml index 595f9bcd489ec..f4375ad499dfd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -70,19 +68,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -110,7 +107,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -140,7 +137,9 @@ <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductRegularPrice.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> <assertEquals stepKey="assertTierPriceTextOnProductPage"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml index 458d02d61426d..0cf0d22094cb6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml @@ -34,12 +34,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -57,7 +55,7 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPriceInStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPriceInStock.urlKey}}" stepKey="fillUrlKey"/> @@ -120,14 +118,13 @@ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetSecondRow"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="fillOptionSkuForFourthDataSetSecondRow"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -149,7 +146,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPriceInStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -219,7 +216,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductRegularPriceInStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductRegularPriceInStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml index 6d6ff0b3b1b89..7f3df4be87bb9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -56,14 +54,13 @@ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -88,7 +85,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductRegularPrice5OutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductRegularPrice5OutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml index d5ae971d87695..343326547254a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickclearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -59,19 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -92,7 +89,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -108,7 +105,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductRegularPrice5OutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductRegularPrice5OutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml index 314df67d43d00..0d6a1c87c62fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml @@ -35,12 +35,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -54,14 +52,13 @@ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -86,7 +83,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductRegularPrice99OutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductRegularPrice99OutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml index d0f4fc8882e3f..f423cd6c77807 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -65,19 +63,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPrice.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPrice.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -101,7 +98,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <see selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPrice.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -125,7 +122,9 @@ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPrice.urlKey)}}" stepKey="goToProductStorefrontPage"/> <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductSpecialPrice.sku}}"/> + </actionGroup> <!-- Verify customer see virtual product special price on the storefront page --> <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml index 2234d6f338b62..78a15ee7eb195 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -64,19 +62,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product with special price(out of stock) in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -99,7 +96,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <see selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -110,7 +107,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductSpecialPriceOutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductSpecialPriceOutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml index ab5d23f0f875e..79a1bf96671d9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml @@ -97,7 +97,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$categoryEntity.name$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductTierPriceInStock.visibility}}" stepKey="seeVisibility"/> <conditionalClick selector="{{AdminProductSEOSection.sectionHeader}}" dependentSelector="{{AdminProductSEOSection.useDefaultUrl}}" visible="false" stepKey="openSearchEngineOptimizationSection"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml index 8f0861fe33371..f64e628c0edb9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -70,19 +68,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductWithTierPriceInStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductWithTierPriceInStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -110,7 +107,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductWithTierPriceInStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -126,7 +123,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductWithTierPriceInStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductWithTierPriceInStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml index f7f5385381590..6e835f2e5e98b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -70,19 +68,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualTierPriceOutOfStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualTierPriceOutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -110,7 +107,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualTierPriceOutOfStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -126,7 +123,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualTierPriceOutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualTierPriceOutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml index 9146ee4d4d579..914e72d51e92a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml @@ -25,7 +25,7 @@ <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="GoToProductCatalogPageActionGroup" stepKey="goToProductCatalogPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="VerifyProductTypeOrder" stepKey="verifyProductTypeOrder"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index 3eaae60d789f5..55d697e35deba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -110,7 +110,9 @@ <actionGroup ref="ClearProductsFilterActionGroup" stepKey="ClearProductsFilterActionGroup"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Edit customer info--> @@ -333,8 +335,12 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!--Do reindex and flush cache--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHint.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml similarity index 100% rename from app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHint.xml rename to app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml index 437532b9baebf..26ff1bc45be9d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml @@ -39,7 +39,7 @@ <generateDate date="now" format="m/j/Y" stepKey="generateDefaultDate"/> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute as TextField, with code and default value.--> <actionGroup ref="CreateProductAttributeWithDateFieldActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownTest.xml index 580a5bd4939bb..61787dcff0b91 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownTest.xml @@ -33,7 +33,7 @@ </after> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute as TextField, with code and default value.--> <actionGroup ref="CreateProductAttributeActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml index e24bf0d7b1115..73c8bafba0625 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml @@ -33,7 +33,7 @@ </after> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute as TextField, with code and default value.--> <actionGroup ref="CreateProductAttributeActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml index 0a84d9af3c918..9952f6a4a85fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml @@ -32,7 +32,7 @@ </after> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute as TextField, with code and default value.--> <actionGroup ref="CreateProductAttributeActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml index 97eff20b2d560..760cd5e0e488a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml @@ -33,7 +33,7 @@ </after> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute with Price--> <actionGroup ref="CreateProductAttributeActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml index c0cff7b0b2bc9..ea94fc58400a6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml @@ -33,7 +33,7 @@ </after> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute as TextField, with code and default value.--> <actionGroup ref="CreateProductAttributeWithTextFieldActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml index ce9ff3af18607..ac855cdbf94af 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml @@ -56,8 +56,7 @@ <argument name="parentCategory" value="$$createNewRootCategoryA.name$$"/> </actionGroup> <!-- Change root category for Main Website Store. --> - <amOnPage stepKey="s1" url="{{AdminSystemStorePage.url}}"/> - <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="s1"/> <click stepKey="s2" selector="{{AdminStoresGridSection.resetButton}}"/> <waitForPageLoad stepKey="waitForPageAdminStoresGridLoadAfterResetButton" time="10"/> <fillField stepKey="s4" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="Main Website Store"/> @@ -75,7 +74,9 @@ <!-- @TODO: Uncomment commented below code after MQE-903 is fixed --> <!-- Perform cli reindex. --> - <!--<magentoCLI command="indexer:reindex" stepKey="magentoCli"/>--> + <!--<actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex">--> + <!-- <argument name="indices" value=""/>--> + <!--</actionGroup>--> <!-- Delete Default Root Category. --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPageAfterCLIReindexCommand"/> @@ -152,7 +153,7 @@ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree('$$createNewRootCategoryA.name$$')}}" stepKey="clickOnNewRootCategoryA"/> <waitForPageLoad stepKey="waitForPageNewRootCategoryALoad" /> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="Default Category" stepKey="enterCategoryNameAsDefaultCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryDefaultCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategoryDefaultCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaveDefaultCategory"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml index f6ede46578f33..e679402740398 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml @@ -24,14 +24,18 @@ <comment userInput="Create category, flush cache and log in" stepKey="createCategoryAndLogIn"/> <createData entity="SimpleSubCategory" stepKey="simpleCategory"/> <actionGroup ref="AdminLoginActionGroup" stepKey="logInAsAdmin"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete category and log out --> <comment userInput="Delete category and log out" stepKey="deleteCategoryAndLogOut"/> <deleteData createDataKey="simpleCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logOutFromAdmin"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Navigate to category details page --> <comment userInput="Navigate to category details page" stepKey="navigateToAdminCategoryPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml index 5c3f79694e79a..110b4167cfc80 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml @@ -29,8 +29,7 @@ <!--Admin creates product--> <!--Create Simple Product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageSimple"/> - <waitForPageLoad time="30" stepKey="waitForProductPageLoadSimple"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageSimple"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateSimpleProduct"> <argument name="product" value="SimpleProduct"/> @@ -57,8 +56,7 @@ </actionGroup> <!--Create Virtual Product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageVirtual"/> - <waitForPageLoad time="30" stepKey="waitForProductPageLoadVirtual"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageVirtual"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateVirtualProduct"> <argument name="product" value="VirtualProduct"/> </actionGroup> @@ -73,8 +71,7 @@ <!--Admin uses product grid--> <!--Start with default view--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageGrid"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageGrid"/> <!--Search by keyword--> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> @@ -82,7 +79,11 @@ <argument name="keyword" value="SimpleProduct.name"/> </actionGroup> <seeNumberOfElements selector="{{AdminProductGridSection.productGridRows}}" userInput="1" stepKey="seeOnlyOneProductInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="{{SimpleProduct.name}}" stepKey="seeOnlySimpleProductInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeOnlySimpleProductInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="{{SimpleProduct.name}}"/> + </actionGroup> <!--Paging works--> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="setProductGridToDefaultPagination"/> @@ -108,7 +109,11 @@ <argument name="product" value="GroupedProduct"/> </actionGroup> <seeNumberOfElements selector="{{AdminProductGridSection.productGridRows}}" userInput="1" stepKey="seeOneMatchingSkuInProductGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1','SKU')}}" userInput="{{GroupedProduct.sku}}" stepKey="seeProductInFilteredGridSku"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductInFilteredGridSku"> + <argument name="row" value="1"/> + <argument name="column" value="SKU"/> + <argument name="value" value="{{GroupedProduct.sku}}"/> + </actionGroup> <!--Filter by price--> <actionGroup ref="FilterProductGridByPriceRangeActionGroup" stepKey="filterProductGridByPrice"> <argument name="filter" value="PriceFilterRange"/> @@ -197,7 +202,11 @@ <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridToCheckWeightColumn"> <argument name="product" value="SimpleProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1','Weight')}}" userInput="{{SimpleProduct.weight}}" stepKey="seeCorrectProductWeightInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeCorrectProductWeightInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Weight"/> + <argument name="value" value="{{SimpleProduct.weight}}"/> + </actionGroup> <!--END Admin uses product grid--> <!--Admin creates category--> @@ -218,7 +227,7 @@ <!--Admin moves category--> <comment userInput="Admin moves category." stepKey="adminMovesCategoryComment" before="onCategoryPageToMoveCategory"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="onCategoryPageToMoveCategory"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandTree"/> <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryTreeRoot}}" stepKey="dragAndDropCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml index 441c9cd5eab8b..ff68bba78cae8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml @@ -57,7 +57,9 @@ </after> <!--Re-index--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Step 1: User browses catalog --> <comment userInput="Start of browsing catalog" stepKey="startOfBrowsingCatalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml index 9ee56c02c7710..845299b3e33bb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml @@ -23,8 +23,7 @@ <!-- A Cms page containing the New Products Widget gets created here via extends --> <!-- Create a Simple Product to appear in the widget --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="fillProductSku"/> @@ -32,7 +31,7 @@ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQuantity"/> <fillField selector="{{AdminProductFormSection.setProductAsNewFrom}}" userInput="01/1/2000" stepKey="fillProductNewFrom"/> <fillField selector="{{AdminProductFormSection.setProductAsNewTo}}" userInput="01/1/2099" stepKey="fillProductNewTo"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here via merge --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml index a4e0d8708eb49..9f49b2abcea7a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml @@ -25,8 +25,7 @@ <!-- Create a Virtual Product to appear in the widget --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="toggleAddProductButton"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickAddVirtualProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> @@ -35,7 +34,7 @@ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQuantity"/> <fillField selector="{{AdminProductFormSection.setProductAsNewFrom}}" userInput="01/1/2000" stepKey="fillProductNewFrom"/> <fillField selector="{{AdminProductFormSection.setProductAsNewTo}}" userInput="01/1/2099" stepKey="fillProductNewTo"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here, via merge --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml index 9b5fa25085e1a..7fd752d7df98d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml @@ -37,23 +37,28 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront2"/> - <waitForPageLoad stepKey="waitForCategoryStorefront"/> - <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="dontSeeCreatedProduct"/> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="onCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandAll"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$simpleSubCategory.name$$)}}" stepKey="clickOnCreatedSimpleSubCategoryBeforeDelete"/> - <waitForPageLoad stepKey="AdminCategoryEditPageLoad"/> - <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="EnableCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AssertStorefrontProductAbsentOnCategoryPageActionGroup" stepKey="doNotSeeProductOnCategoryPage"> + <argument name="categoryUrlKey" value="$$createCategory.name$$"/> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedSubCategory"> + <argument name="Category" value="$$simpleSubCategory$$"/> + </actionGroup> + <actionGroup ref="AdminEnableCategoryActionGroup" stepKey="enableCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="seeSuccessMessage"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> - <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront"/> - <waitForPageLoad stepKey="waitForCategoryStorefrontPage"/> - <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="seeCreatedProduct"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openEnabledCategory"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeCreatedProduct"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml index b206a33ebde88..2ff5f0cadcfe7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml @@ -15,51 +15,40 @@ <title value="You should be able to save a product with custom options assigned to a different website"/> <description value="Custom Options should not be split when saving the product after assigning to a different website"/> <severity value="BLOCKER"/> - <testCaseId value="MAGETWO-91436"/> + <testCaseId value="MC-25687"/> <group value="product"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <!--Create new website --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> - <argument name="newWebsiteName" value="Second Website"/> - <argument name="websiteCode" value="second_website"/> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> - - <!--Create new Store Group --> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> - <argument name="website" value="Second Website"/> - <argument name="storeGroupName" value="Second Store"/> - <argument name="storeGroupCode" value="second_store"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> </actionGroup> - <!--Create Store view --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForAdminSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> - <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> - <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption userInput="1" selector="{{AdminNewStoreSection.statusDropdown}}" stepKey="enableStoreViewStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickStoreViewSaveButton"/> - <waitForElementVisible selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" stepKey="waitForAcceptNewStoreViewCreationModal" /> - <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="AcceptNewStoreViewCreation"/> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReolad"/> - <see userInput="You saved the store view." stepKey="seeSaveMessage" /> + <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> </before> + <after> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> - <argument name="websiteName" value="Second Website"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> - <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> <!--Create a Simple Product with Custom Options --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> - <waitForPageLoad stepKey="waitForCatalogProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGrid"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> @@ -88,19 +77,17 @@ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '2')}}" userInput="7" stepKey="fillOptionValuePrice3"/> <!--Save the product with custom options --> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitProductPageSave"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeProductSavedMessage"/> <!-- Add this product to second website --> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsitesSection1"/> - <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> <waitForLoadingMaskToDisappear stepKey="waitForProductPagetoSaveAgain"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection2"/> <seeNumberOfElements selector=".admin__dynamic-rows[data-index='values'] tr.data-row" userInput="3" stepKey="see4RowsOfOptions"/> - </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml index 7b2e004495fea..f6292a3a96c40 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml @@ -23,8 +23,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateSimpleProduct"> <argument name="product" value="SimpleProduct3"/> </actionGroup> @@ -37,7 +36,7 @@ <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> <argument name="product" value="SimpleProduct3"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!-- opens the custom option panel and clicks add options --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml index e109dcb0deea5..cde7b14614f8e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml @@ -114,8 +114,12 @@ </createData> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> - <magentoCLI command="indexer:reindex" arguments="catalogsearch_fulltext" stepKey="performReindex"/> - <magentoCLI command="cache:clean" arguments="full_page" stepKey="cleanFullPageCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="performReindex"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanFullPageCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> 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/StoreFrontRecentlyViewedAtStoreLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml index 489be97a9927a..e1b5aca6382e9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml @@ -74,8 +74,12 @@ </actionGroup> <!-- Logout Admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterDeletion"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDeletion"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Create widget for recently viewed products--> <actionGroup ref="AdminEditCMSPageContentActionGroup" stepKey="clearRecentlyViewedWidgetsFromCMSContentBefore"> @@ -95,7 +99,9 @@ <argument name="buttonToShowSection2" value="3"/> </actionGroup> <!-- Warm up cache --> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterWidgetCreated"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterWidgetCreated"> + <argument name="tags" value=""/> + </actionGroup> <!-- Navigate to product 3 on store front --> <amOnPage url="{{StorefrontProductPage.url($createSimpleProduct2.name$)}}" stepKey="goToStoreOneProductPageTwo"/> <amOnPage url="{{StorefrontProductPage.url($createSimpleProduct3.name$)}}" stepKey="goToStoreOneProductPageThree"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml index bc93b3e6e3c45..0117493906de1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml @@ -66,8 +66,12 @@ <!-- Logout Admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterDeletion"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDeletion"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Create widget for recently viewed products--> @@ -88,7 +92,9 @@ <argument name="buttonToShowSection2" value="3"/> </actionGroup> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterWidgetCreated"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterWidgetCreated"> + <argument name="tags" value=""/> + </actionGroup> <!-- Navigate to product 3 on store front --> <amOnPage url="{{StorefrontProductPage.url($createSimpleProduct2.name$)}}" stepKey="goToStore1ProductPage2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml index 8955f43e1b335..3ff477070cc30 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml @@ -44,7 +44,9 @@ <!-- Set the category filter to be present on the category page layered navigation --> <magentoCLI command="config:set {{EnableCategoryFilterOnCategoryPageConfigData.path}} {{EnableCategoryFilterOnCategoryPageConfigData.value}}" stepKey="setCategoryFilterVisibleOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="clearCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml index 7900a712e0664..a2316efb3a743 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml @@ -44,7 +44,9 @@ <!-- Set the category filter to NOT be present on the category page layered navigation --> <magentoCLI command="config:set {{DisableCategoryFilterOnCategoryPageConfigData.path}} {{DisableCategoryFilterOnCategoryPageConfigData.value}}" stepKey="hideCategoryFilterOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="clearCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml index b13c3827c6727..a73bd5a533ad0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml @@ -188,8 +188,12 @@ <seeInField selector="{{AdminCatalogStorefrontConfigSection.productsPerPageDefaultValue}}" userInput="12" stepKey="seeDefaultValueProductPerPage"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Open storefront on the category page --> <comment userInput="Open storefront on the category page" stepKey="commentOpenStorefront"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml new file mode 100644 index 0000000000000..507e4ae14e83c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckNoAppearDefaultOptionConfigurableProductTest"> + <annotations> + <stories value="Configurable Product"/> + <title value="Check for Configurable Product the default option doesn't appear."/> + <description value="Check for Configurable Product the default option doesn't appear on the list options product when an option use."/> + <testCaseId value="MC-35074"/> + <severity value="CRITICAL"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteAttribute"> + <argument name="productAttributeLabel" value="{{colorProductAttribute.default_label}}" /> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <actionGroup ref="AdminFillBasicValueConfigurableProductActionGroup" stepKey="fillBasicValue"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup" stepKey="createOptions"/> + <actionGroup ref="AdminGotoSelectValueAttributePageActionGroup" stepKey="gotoSelectValuePage"> + <argument name="defaultLabelAttribute" value="{{colorProductAttribute.default_label}}"/> + </actionGroup> + <actionGroup ref="AdminSelectValueFromAttributeActionGroup" stepKey="selectColorProductAttribute2"> + <argument name="option" value="colorProductAttribute2"/> + </actionGroup> + <actionGroup ref="AdminSelectValueFromAttributeActionGroup" stepKey="selectColorProductAttribute3"> + <argument name="option" value="colorProductAttribute3"/> + </actionGroup> + <actionGroup ref="AdminSetQuantityToEachSkusConfigurableProductActionGroup" stepKey="saveConfigurable"/> + <grabValueFrom selector="{{NewProductPageSection.sku}}" stepKey="grabSkuProduct"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="expandOption"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="attributeDefaultLabel" value="{{colorProductAttribute.default_label}}"/> + </actionGroup> + <dontSeeElement selector="{{LayeredNavigationSection.filterOptionContent(colorProductAttribute.default_label,colorProductAttribute1.name)}}" stepKey="dontSeeCaptchaField"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteConfigurableProduct"> + <argument name="sku" value="$grabSkuProduct"/> + </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/StorefrontOnlyXProductLeftForSimpleProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml new file mode 100644 index 0000000000000..dc608a7f12dd3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontOnlyXProductLeftForSimpleProductsTest"> + <annotations> + <features value="Catalog"/> + <title value="See Only * Left block"/> + <stories value="See Only * Left on product page if Only X left Threshold was set"/> + <description value="See Only * Left on product page if Only X left Threshold was set"/> + <testCaseId value="MC-35235"/> + <severity value="MINOR"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="config:set {{CatalogInventoryOptionsOnlyXleftThreshold.path}} 10000" stepKey="setStockThresholdQty"/> + <magentoCLI command="cache:flush config" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set {{CatalogInventoryOptionsOnlyXleftThreshold.path}} {{CatalogInventoryOptionsOnlyXleftThreshold.value}}" stepKey="removedStockThresholdQty"/> + </after> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <seeElement selector="{{StorefrontProductPageSection.onlyProductsLeft}}" stepKey="seeOnlyLeftBlock"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml index bcd5d7b851db3..67ca04a0a4594 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml @@ -26,8 +26,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Create product via admin--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToProductCreatePage"> <argument name="product" value="SimpleProductNameWithDoubleQuote"/> </actionGroup> @@ -41,7 +40,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Check product in category listing--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryPage"/> @@ -52,7 +53,9 @@ <click selector="{{StorefrontCategoryProductSection.ProductTitleByName(SimpleProductNameWithDoubleQuote.name)}}" stepKey="clickProductToGoProductPage"/> <waitForPageLoad stepKey="waitForProductDisplayPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProductNameWithDoubleQuote.name}}" stepKey="seeCorrectName"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProductNameWithDoubleQuote.sku}}" stepKey="seeCorrectSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku"> + <argument name="productSku" value="{{SimpleProductNameWithDoubleQuote.sku}}"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="${{SimpleProductNameWithDoubleQuote.price}}" stepKey="seeCorrectPrice"/> <seeElement selector="{{StorefrontProductInfoMainSection.productImageSrc(ProductImage.fileName)}}" stepKey="seeCorrectImage"/> <see selector="{{StorefrontProductInfoMainSection.stock}}" userInput="In Stock" stepKey="seeInStock"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml index bd2c22c90318a..2156178ea88d0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml @@ -33,7 +33,9 @@ </after> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Check product in category listing--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategoryOne.name$$)}}" stepKey="navigateToCategoryPage"/> @@ -46,7 +48,9 @@ <waitForPageLoad stepKey="waitForProductDisplayPageLoad2"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{productWithHTMLEntityOne.name}}" stepKey="seeCorrectName"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{productWithHTMLEntityOne.sku}}" stepKey="seeCorrectSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku"> + <argument name="productSku" value="{{productWithHTMLEntityOne.sku}}"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="${{productWithHTMLEntityOne.price}}" stepKey="seeCorrectPrice"/> <!--Veriy the breadcrumbs on Product Display page--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml index 767e0c88b7af2..2080aee933aad 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml @@ -67,7 +67,7 @@ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersGridFilter"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> @@ -75,8 +75,7 @@ <!-- Open Product Grid, Filter product and open --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> <argument name="product" value="_defaultProduct"/> @@ -103,7 +102,7 @@ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '1')}}" userInput="option2" stepKey="fillOptionValueTitle2"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '1')}}" userInput="50" stepKey="fillOptionValuePrice2"/> <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType('Custom Options 1', '1')}}" userInput="percent" stepKey="clickSelectPriceType"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton1"/> <!-- Switcher to Store FR--> <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToStoreFR"> @@ -127,7 +126,7 @@ <click selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="clickHiddenRequireMessage"/> <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="uncheckUseDefaultOptionValueTitle2"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '1')}}" userInput="FR option2" stepKey="fillOptionValueTitle4"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton2"/> <!-- Login Customer Storefront --> @@ -181,9 +180,7 @@ <!--Select shipping method--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!--Select payment method--> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> <!-- Place Order --> @@ -258,9 +255,7 @@ <!--Select shipping method--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod2"/> - <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton2"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext2"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext2"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext2"/> <!--Select payment method--> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod2"/> @@ -272,8 +267,7 @@ <!-- Open Product Grid, Filter product and open --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage1"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad15"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage1"/> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions1"> <argument name="product" value="_defaultProduct"/> @@ -304,7 +298,7 @@ <waitForPageLoad time="30" stepKey="waitForPageLoad19"/> <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="checkUseDefaultOptionValueTitle2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton3"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton3"/> <!--Go to Product Page--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml index 09b596f298e0f..631d1d50077e9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -115,9 +115,7 @@ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="1:00 AM" stepKey="seeProductOptionTimeInput" /> <!--Select shipping method--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!--Select payment method--> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> <!-- Place Order --> @@ -154,7 +152,7 @@ <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorder"/> <actionGroup ref="AdminCheckoutSelectCheckMoneyOrderBillingMethodActionGroup" stepKey="selectBillingMethod"/> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="trySubmitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="trySubmitOrder" /> <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField1" /> <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml index 95e48e63419d3..aac76999636b0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml @@ -79,9 +79,7 @@ <!--Select shipping method--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml index c8872425552be..78fbed1aef3a9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml @@ -61,7 +61,9 @@ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> <deleteData createDataKey="defaultCategory2" stepKey="deleteCategory2"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> </test> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml new file mode 100644 index 0000000000000..914ac3444db22 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontRemoveProductFromCompareSidebarTest"> + <annotations> + <title value="Verify that the product isn't removed on clicking the product name"/> + <stories value="Verify that the product isn't removed on clicking the product name"/> + <description value="Verify that the product isn't removed on clicking the product name, but it's redirected to product page"/> + <features value="Catalog"/> + <severity value="MINOR"/> + <group value="Catalog"/> + <testCaseId value="MC-35068"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="SimpleProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + </after> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="category" value="$$defaultCategory$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addProductToCompareList"> + <argument name="productVar" value="$$simpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontClickOnProductFromSidebarCompareListActionGroup" stepKey="clickOnComparingProductLink"> + <argument name="product" value="$$simpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductUrlActionGroup" stepKey="checkProductPageUrl"> + <argument name="productUrl" value="$$simpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + </test> +</tests> 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/StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest.xml new file mode 100644 index 0000000000000..14001ecbb52af --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest.xml @@ -0,0 +1,234 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Categories Indexer"/> + <title value="Verify Category Product and Product Category partial reindex"/> + <description value="Verify that Merchant Developer can use console commands to perform partial reindex for Category Products, Product Categories, and Catalog Search"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11386"/> + <useCaseId value="MAGETWO-88184"/> + <group value="catalog"/> + <group value="indexer"/> + </annotations> + <before> + <!-- Change "Category Products", "Product Categories" and "Catalog Search" indexers to "Update by Schedule" mode --> + <magentoCLI command="indexer:set-mode" arguments="schedule catalog_category_product catalog_product_category catalogsearch_fulltext" stepKey="setIndexerMode"/> + + <!-- Create categories K, L, M, N with different nesting in the tree and Anchor = Yes/No--> + <!-- Category K is an anchor category --> + <createData entity="_defaultCategory" stepKey="categoryK"/> + <!-- Category L is a non-anchor subcategory of category K --> + <createData entity="SubCategoryNonAnchor" stepKey="categoryL"> + <requiredEntity createDataKey="categoryK"/> + </createData> + <!-- Category M is a subcategory of category L --> + <createData entity="SubCategoryWithParent" stepKey="categoryM"> + <requiredEntity createDataKey="categoryL"/> + </createData> + <!-- Category N is a subcategory of category K --> + <createData entity="SubCategoryWithParent" stepKey="categoryN"> + <requiredEntity createDataKey="categoryK"/> + </createData> + + <!-- Create different Products with different settings, assign to categories: --> + <!-- Product A in 0 categories, i.e. not assigned to any category --> + <createData entity="SimpleProduct2" stepKey="productA"/> + <!-- Product B in 1 category M --> + <createData entity="SimpleProduct3" stepKey="productB"> + <requiredEntity createDataKey="categoryM"/> + </createData> + <!-- Product C in 2 categories M and N --> + <createData entity="SimpleProduct2" stepKey="productC"/> + <createData entity="AssignProductToCategory" stepKey="assignCategoryMToProductC"> + <requiredEntity createDataKey="categoryM"/> + <requiredEntity createDataKey="productC"/> + </createData> + <createData entity="AssignProductToCategory" stepKey="assignCategoryNToProductC"> + <requiredEntity createDataKey="categoryN"/> + <requiredEntity createDataKey="productC"/> + </createData> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </before> + <after> + <!-- Change indexers to "Update on Save" mode --> + <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setRealtimeMode"/> + + <!-- Delete data --> + <deleteData createDataKey="productA" stepKey="deleteProductA"/> + <deleteData createDataKey="productB" stepKey="deleteProductB"/> + <deleteData createDataKey="productC" stepKey="deleteProductC"/> + <deleteData createDataKey="categoryN" stepKey="deleteCategoryN"/> + <deleteData createDataKey="categoryM" stepKey="deleteCategoryM"/> + <deleteData createDataKey="categoryL" stepKey="deleteCategoryL"/> + <deleteData createDataKey="categoryK" stepKey="deleteCategoryK"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + + <!-- Open categories K, L, M, N on Storefront --> + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$)}}" stepKey="onCategoryK"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBOnCategoryK"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryK"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnCategoryK"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$)}}" stepKey="onCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" stepKey="seeMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProducts"/> + + <!-- Category M contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$/$categoryM.custom_attributes[url_key]$)}}" stepKey="onCategoryM"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBOnCategoryM"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryM"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnCategoryM"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryN.custom_attributes[url_key]$)}}" stepKey="onCategoryN"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryN"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnCategoryN"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductBOnCategoryN"/> + + <!-- Assign category K to Product A --> + <createData entity="AssignProductToCategory" stepKey="assignCategoryKToProductA"> + <requiredEntity createDataKey="categoryK"/> + <requiredEntity createDataKey="productA"/> + </createData> + + <!-- Unassign category M from Product B --> + <deleteData url="/V1/categories/$categoryM.id$/products/$productB.sku$" stepKey="unassignCategoryMFromProductB"/> + + <!-- Assign category L to Product C --> + <createData entity="AssignProductToCategory" stepKey="assignCategoryLToProductC"> + <requiredEntity createDataKey="categoryL"/> + <requiredEntity createDataKey="productC"/> + </createData> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assignments are not applied yet --> + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$)}}" stepKey="amOnCategoryK"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBCategoryK"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryK"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductACategoryN"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$)}}" stepKey="amOnCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" stepKey="seeEmptyMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProduct"/> + + <!-- Category M contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$/$categoryM.custom_attributes[url_key]$)}}" stepKey="amOnCategoryM"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBCategoryM"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryM"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAInCategoryM"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryN.custom_attributes[url_key]$)}}" stepKey="amOnCategoryN"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductInCategoryN"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAInCategoryN"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductBInCategoryN"/> + + <!-- Run cron --> + <magentoCron groups="index" stepKey="runCronIndex"/> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assignments are applied --> + <!-- Category K contains only Products A, C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$)}}" stepKey="storefrontCategoryK"/> + <see userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductAOnCategoryK"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryKWithProductC"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryKWithProductB"/> + + <!-- Category L contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$)}}" stepKey="storefrontCategoryL"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryLWithProductC"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryLWithProductA"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryLWithProductB"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$/$categoryM.custom_attributes[url_key]$)}}" stepKey="storefrontCategoryM"/> + <waitForPageLoad stepKey="waitForStorefrontCategoryM"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMAndProductC"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMAndProductA"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMAndProductB"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryN.custom_attributes[url_key]$)}}" stepKey="storefrontCategoryN"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCAndCategoryN"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAAndCategoryN"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductBAndCategoryN"/> + + <!-- Remove Product A assignment for category K --> + <deleteData url="/V1/categories/$categoryK.id$/products/$productA.sku$" stepKey="unassignCategoryKFromProductA"/> + + <!-- Remove Product C assignment for category L --> + <deleteData url="/V1/categories/$categoryL.id$/products/$productC.sku$" stepKey="unassignCategoryLFromProductC"/> + + <!-- Add Product B assignment for category N --> + <createData entity="AssignProductToCategory" stepKey="assignCategoryNToProductB"> + <requiredEntity createDataKey="categoryN"/> + <requiredEntity createDataKey="productB"/> + </createData> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assignments are not applied yet --> + <!-- Category K contains only Products A, C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$)}}" stepKey="onStorefrontCategoryK"/> + <see userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductAWithCategoryK"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductB"/> + + <!-- Category L contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$)}}" stepKey="onStorefrontCategoryL"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryLAndProductC"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryLAndProductA"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryLAndProductB"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$/$categoryM.custom_attributes[url_key]$)}}" stepKey="onStorefrontCategoryM"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMWithProductC"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMWithProductA"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMWithProductB"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryN.custom_attributes[url_key]$)}}" stepKey="onStorefrontCategoryN"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productCOnCategoryN"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnTheCategoryN"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductBOnTheCategoryN"/> + + <!-- Run Cron once to reindex product changes --> + <magentoCron groups="index" stepKey="runCronIndex2"/> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assignments are applied --> + + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$)}}" stepKey="onFrontendCategoryK"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productBOnCategoryK"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productCOnCategoryK"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnTheCategoryK"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$)}}" stepKey="onFrontendCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" stepKey="noProductsMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductsOnCategoryL"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$/$categoryM.custom_attributes[url_key]$)}}" stepKey="onFrontendCategoryM"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMPageAndProductC"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMPageAndProductA"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMPageAndProductB"/> + + <!-- Category N contains only Products B and C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryN.custom_attributes[url_key]$)}}" stepKey="onFrontendCategoryN"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBAndCategoryN"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryN"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAWithCategoryN"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml index 26cebae318cd9..9c68c08064081 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml @@ -37,11 +37,11 @@ stepKey="clickOpenProductForEdit"/> <waitForPageLoad time="30" stepKey="waitForProductEditOpen"/> <!--Step2. Open *Advanced Inventory* pop-up (Click on *Advanced Inventory* link). Set *Qty Uses Decimals* to *Yes*. Click on button *Done* --> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="scrollToQtyUsesDecimalsDropBox"/> <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="clickOnQtyUsesDecimalsDropBox"/> <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimalsOptions('1')}}" stepKey="chooseYesOnQtyUsesDecimalsDropBox"/> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton"/> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton"/> <!--Step3. Open *Advanced Pricing* pop-up (Click on *Advanced Pricing* link). Click on *Add* button. Fill *0.5* in *Quantity*--> <scrollTo selector="{{AdminProductFormSection.productName}}" stepKey="scrollToProductName"/> <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingLink1"/> @@ -50,7 +50,7 @@ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="0.5" stepKey="fillProductTierPriceQty"/> <!--Step4. Close *Advanced Pricing* (Click on button *Done*). Save *prod1* (Click on button *Save*)--> <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickOnDoneButton2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <!--The code should be uncommented after fix MAGETWO-96016--> <!--<click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingLink2"/>--> @@ -58,8 +58,7 @@ <!--<click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickOnCloseButton"/>--> <!--Step5. Open *Advanced Inventory* pop-up. Set *Enable Qty Increments* to *Yes*. Fill *.5* in *Qty Increments*--> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink2"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink2"/> <scrollTo selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" stepKey="scrollToEnableQtyIncrements"/> <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsUseConfigSettings}}" stepKey="clickOnEnableQtyIncrementsUseConfigSettingsCheckbox"/> <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" stepKey="clickOnEnableQtyIncrements"/> @@ -69,8 +68,8 @@ <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" stepKey="scrollToQtyIncrements"/> <fillField selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" userInput=".5" stepKey="fillQtyIncrements"/> <!--Step6. Close *Advanced Inventory* (Click on button *Done*). Save *prod1* (Click on button *Save*) --> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton3"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton3"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton2"/> <!--Step7. Open *Customer view* (Go to *Store Front*). Open *prod1* page (Find via search and click on product name) --> <amOnPage url="{{StorefrontHomePage.url}}$$createPreReqSimpleProduct.custom_attributes[url_key]$$.html" stepKey="amOnProductPage"/> <!--Step8. Fill *1.5* in *Qty*. Click on button *Add to Cart*--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml index b4514c9b53736..ce04b377300f8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml @@ -8,16 +8,19 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="VerifyCategoryProductAndProductCategoryPartialReindexTest"> + <test name="VerifyCategoryProductAndProductCategoryPartialReindexTest" deprecated="Use StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest instead."> <annotations> <features value="Catalog"/> <stories value="Product Categories Indexer"/> - <title value="Verify Category Product and Product Category partial reindex"/> + <title value="DEPRECATED. Verify Category Product and Product Category partial reindex"/> <description value="Verify that Merchant Developer can use console commands to perform partial reindex for Category Products, Product Categories, and Catalog Search"/> <severity value="BLOCKER"/> <testCaseId value="MC-11386"/> <group value="catalog"/> <group value="indexer"/> + <skip> + <issueId value="DEPRECATED">Use StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest instead.</issueId> + </skip> </annotations> <before> <!-- Change "Category Products" and "Product Categories" indexers to "Update by Schedule" mode --> @@ -55,14 +58,16 @@ <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 --> <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setRealtimeMode"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Delete data --> <deleteData createDataKey="productA" stepKey="deleteProductA"/> @@ -103,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$$"/> @@ -142,9 +149,10 @@ <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="60"/> + + <wait stepKey="waitAfterRunMagentoCron" time="90"/> <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> <!-- Category K contains only Products A, C --> @@ -208,9 +216,10 @@ <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="60"/> + + <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/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml index a15081e0cbda3..d7e4f97ed0bc2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml @@ -37,7 +37,7 @@ <seeElement selector="{{TinyMCESection.InsertImageBtn}}" stepKey="insertImage"/> <dontSee selector="{{TinyMCESection.InsertWidgetBtn}}" stepKey="insertWidget" /> <dontSee selector="{{TinyMCESection.InsertVariableBtn}}" stepKey="insertVariable" /> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCatalog"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCatalog"/> <!-- Go to storefront product page, assert product content --> <amOnPage url="/{{SimpleSubCategory.name_lwr}}.html" stepKey="goToCategoryFrontPage"/> <waitForPageLoad stepKey="waitForPageLoad2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml index 1e4adedfc168d..cffc4af6fcbbd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml @@ -44,13 +44,13 @@ <see selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" userInput="Insert Image..." stepKey="seeInsertImage1"/> <dontSee selector="{{TinyMCESection.InsertWidgetBtn}}" stepKey="insertWidget1" /> <dontSee selector="{{TinyMCESection.InsertVariableBtn}}" stepKey="insertVariable1" /> - <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="scrollToDesShowHideBtn2" /> + <scrollTo selector="{{ProductShortDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToDesShowHideBtn2" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="clickShowHideBtn2" /> <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" stepKey="waitForInsertImage2" /> <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" userInput="Insert Image..." stepKey="seeInsertImage2"/> <dontSee selector="{{TinyMCESection.InsertWidgetBtn}}" stepKey="insertWidget2" /> <dontSee selector="{{TinyMCESection.InsertVariableBtn}}" stepKey="insertVariable2" /> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!-- Go to storefront product page, assert product content --> <amOnPage url="{{_defaultProduct.name}}.html" stepKey="navigateToProductPage"/> <waitForPageLoad stepKey="waitForPageLoad2"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php index d1b01db75927c..157f641335497 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php @@ -235,7 +235,7 @@ public function testGetAddToCartPostParams() ->willReturn(true); $this->cartHelperMock->expects($this->any()) ->method('getAddUrl') - ->with($this->productMock, []) + ->with($this->productMock, ['_escape' => false]) ->willReturn($url); $this->productMock->expects($this->once()) ->method('getEntityId') diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php index ca35d49113f41..681cef8489796 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php @@ -7,10 +7,12 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; +use Magento\Backend\Model\Session; use Magento\Backend\Model\View\Result\Redirect as ResultRedirect; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Catalog\Model\Product\Attribute\Frontend\Inputtype\Presentation; use Magento\Catalog\Model\Product\AttributeSet\Build; use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; @@ -31,63 +33,64 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class SaveTest extends AttributeTest { /** * @var BuildFactory|MockObject */ - protected $buildFactoryMock; + private $buildFactoryMock; /** * @var FilterManager|MockObject */ - protected $filterManagerMock; + private $filterManagerMock; /** * @var ProductHelper|MockObject */ - protected $productHelperMock; + private $productHelperMock; /** * @var AttributeFactory|MockObject */ - protected $attributeFactoryMock; + private $attributeFactoryMock; /** * @var ValidatorFactory|MockObject */ - protected $validatorFactoryMock; + private $validatorFactoryMock; /** * @var CollectionFactory|MockObject */ - protected $groupCollectionFactoryMock; + private $groupCollectionFactoryMock; /** * @var LayoutFactory|MockObject */ - protected $layoutFactoryMock; + private $layoutFactoryMock; /** * @var ResultRedirect|MockObject */ - protected $redirectMock; + private $redirectMock; /** - * @var AttributeSet|MockObject + * @var AttributeSetInterface|MockObject */ - protected $attributeSetMock; + private $attributeSetMock; /** * @var Build|MockObject */ - protected $builderMock; + private $builderMock; /** * @var InputTypeValidator|MockObject */ - protected $inputTypeValidatorMock; + private $inputTypeValidatorMock; /** * @var FormData|MockObject @@ -104,19 +107,34 @@ class SaveTest extends AttributeTest */ private $attributeCodeValidatorMock; + /** + * @var Presentation|MockObject + */ + private $presentationMock; + + /** + * @var Session|MockObject + */ + + private $sessionMock; + protected function setUp(): void { parent::setUp(); + $this->filterManagerMock = $this->createMock(FilterManager::class); + $this->productHelperMock = $this->createMock(ProductHelper::class); + $this->attributeSetMock = $this->createMock(AttributeSetInterface::class); + $this->builderMock = $this->createMock(Build::class); + $this->inputTypeValidatorMock = $this->createMock(InputTypeValidator::class); + $this->formDataSerializerMock = $this->createMock(FormData::class); + $this->attributeCodeValidatorMock = $this->createMock(AttributeCodeValidator::class); + $this->presentationMock = $this->createMock(Presentation::class); + $this->sessionMock = $this->createMock(Session::class); + $this->layoutFactoryMock = $this->createMock(LayoutFactory::class); $this->buildFactoryMock = $this->getMockBuilder(BuildFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->filterManagerMock = $this->getMockBuilder(FilterManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productHelperMock = $this->getMockBuilder(ProductHelper::class) - ->disableOriginalConstructor() - ->getMock(); $this->attributeFactoryMock = $this->getMockBuilder(AttributeFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() @@ -129,32 +147,23 @@ protected function setUp(): void ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->layoutFactoryMock = $this->getMockBuilder(LayoutFactory::class) - ->disableOriginalConstructor() - ->getMock(); $this->redirectMock = $this->getMockBuilder(ResultRedirect::class) ->setMethods(['setData', 'setPath']) ->disableOriginalConstructor() ->getMock(); - $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->builderMock = $this->getMockBuilder(Build::class) - ->disableOriginalConstructor() - ->getMock(); - $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) - ->disableOriginalConstructor() - ->getMock(); - $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class) - ->disableOriginalConstructor() - ->getMock(); $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) - ->setMethods(['getId', 'get']) - ->getMockForAbstractClass(); - + ->setMethods( + [ + 'getId', + 'get', + 'getBackendTypeByInput', + 'getDefaultValueByInput', + 'getBackendType', + 'getFrontendClass', + 'addData', + 'save' + ] + )->getMockForAbstractClass(); $this->buildFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->builderMock); @@ -167,7 +176,7 @@ protected function setUp(): void } /** - * {@inheritdoc} + * @inheritdoc */ protected function getModel() { @@ -184,7 +193,9 @@ protected function getModel() 'groupCollectionFactory' => $this->groupCollectionFactoryMock, 'layoutFactory' => $this->layoutFactoryMock, 'formDataSerializer' => $this->formDataSerializerMock, - 'attributeCodeValidator' => $this->attributeCodeValidatorMock + 'attributeCodeValidator' => $this->attributeCodeValidatorMock, + 'presentation' => $this->presentationMock, + '_session' => $this->sessionMock ]); } @@ -214,6 +225,67 @@ public function testExecuteWithEmptyData() $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); } + public function testExecuteSaveFrontendClass() + { + $data = [ + 'frontend_input' => 'test_frontend_input', + ]; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['isAjax', null, null], + ['serialized_options', '[]', ''], + ['set', null, 1], + ['attribute_code', null, 'test_attribute_code'], + ]); + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with('') + ->willReturn([]); + $this->requestMock->expects($this->once()) + ->method('getPostValue') + ->willReturn($data); + $this->inputTypeValidatorMock->expects($this->any()) + ->method('isValid') + ->with($data['frontend_input']) + ->willReturn(true); + $this->presentationMock->expects($this->once()) + ->method('convertPresentationDataToInputType') + ->willReturn($data); + $this->productHelperMock->expects($this->once()) + ->method('getAttributeSourceModelByInputType') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productHelperMock->expects($this->once()) + ->method('getAttributeBackendModelByInputType') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productAttributeMock->expects($this->once()) + ->method('getBackendTypeByInput') + ->with($data['frontend_input']) + ->willReturnSelf('test_backend_type'); + $this->productAttributeMock->expects($this->once()) + ->method('getDefaultValueByInput') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productAttributeMock->expects($this->once()) + ->method('getBackendType') + ->willReturn('static'); + $this->productAttributeMock->expects($this->once()) + ->method('getFrontendClass') + ->willReturn('static'); + $this->resultFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->redirectMock); + $this->redirectMock->expects($this->any()) + ->method('setPath') + ->willReturnSelf(); + + $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); + } + public function testExecute() { $data = [ diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php index 122089332f89b..371dac9a64b89 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php @@ -153,7 +153,7 @@ public function testExecute() ->willReturnMap( [ [Attribute::class, [], $this->attributeMock], - [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] + [AttributeSet::class, [], $this->attributeSetMock] ] ); $this->attributeMock->expects($this->once()) @@ -188,6 +188,69 @@ public function testExecute() $this->assertInstanceOf(ResultJson::class, $this->getModel()->execute()); } + /** + * Test that editing existing attribute loads attribute by id + * + * @return void + * @throws NotFoundException + */ + public function testExecuteEditExisting(): void + { + $serializedOptions = '{"key":"value"}'; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['frontend_label', null, 'test_frontend_label'], + ['attribute_id', null, 10], + ['attribute_code', null, 'test_attribute_code'], + ['new_attribute_set_name', null, 'test_attribute_set_name'], + ['serialized_options', '[]', $serializedOptions], + ] + ); + $this->objectManagerMock->expects($this->exactly(2)) + ->method('create') + ->willReturnMap( + [ + [Attribute::class, [], $this->attributeMock], + [AttributeSet::class, [], $this->attributeSetMock] + ] + ); + $this->attributeMock->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $this->attributeMock->expects($this->once()) + ->method('getAttributeCode') + ->willReturn('test_attribute_code'); + + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('test_attribute_code') + ->willReturn(true); + + $this->requestMock->expects($this->once()) + ->method('has') + ->with('new_attribute_set_name') + ->willReturn(true); + $this->attributeSetMock->expects($this->once()) + ->method('setEntityTypeId') + ->willReturnSelf(); + $this->attributeSetMock->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $this->attributeSetMock->expects($this->once()) + ->method('getId') + ->willReturn(false); + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->resultJson); + $this->resultJson->expects($this->once()) + ->method('setJsonData') + ->willReturnSelf(); + + $this->assertInstanceOf(ResultJson::class, $this->getModel()->execute()); + } + /** * @dataProvider provideUniqueData * @param array $options @@ -605,7 +668,7 @@ public function testExecuteWithOptionsDataError() ->willReturnMap( [ [Attribute::class, [], $this->attributeMock], - [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] + [AttributeSet::class, [], $this->attributeSetMock] ] ); 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/Test/Unit/Model/Indexer/Category/Flat/Plugin/StoreGroupTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/StoreGroupTest.php index 7dba4ddfd3d8c..cb4fd12e8d6f2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/StoreGroupTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/StoreGroupTest.php @@ -11,7 +11,6 @@ use Magento\Catalog\Model\Indexer\Category\Flat\State; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\Group as GroupModel; use Magento\Store\Model\ResourceModel\Group; use PHPUnit\Framework\MockObject\MockObject; @@ -22,32 +21,32 @@ class StoreGroupTest extends TestCase /** * @var MockObject|IndexerInterface */ - protected $indexerMock; + private $indexerMock; /** * @var MockObject|State */ - protected $stateMock; + private $stateMock; /** * @var StoreGroup */ - protected $model; + private $model; /** * @var MockObject|Group */ - protected $subjectMock; + private $subjectMock; /** * @var IndexerRegistry|MockObject */ - protected $indexerRegistryMock; + private $indexerRegistryMock; /** * @var MockObject|GroupModel */ - protected $groupMock; + private $groupMock; protected function setUp(): void { @@ -70,14 +69,10 @@ protected function setUp(): void $this->indexerRegistryMock = $this->createPartialMock(IndexerRegistry::class, ['get']); - $this->model = (new ObjectManager($this)) - ->getObject( - StoreGroup::class, - ['indexerRegistry' => $this->indexerRegistryMock, 'state' => $this->stateMock] - ); + $this->model = new StoreGroup($this->indexerRegistryMock, $this->stateMock); } - public function testBeforeAndAfterSave() + public function testAfterSave(): void { $this->stateMock->expects($this->once())->method('isFlatEnabled')->willReturn(true); $this->indexerMock->expects($this->once())->method('invalidate'); @@ -90,14 +85,14 @@ public function testBeforeAndAfterSave() ->with('root_category_id') ->willReturn(true); $this->groupMock->expects($this->once())->method('isObjectNew')->willReturn(false); - $this->model->beforeSave($this->subjectMock, $this->groupMock); + $this->assertSame( $this->subjectMock, $this->model->afterSave($this->subjectMock, $this->subjectMock, $this->groupMock) ); } - public function testBeforeAndAfterSaveNotNew() + public function testAfterSaveNotNew(): void { $this->stateMock->expects($this->never())->method('isFlatEnabled'); $this->groupMock->expects($this->once()) @@ -105,7 +100,7 @@ public function testBeforeAndAfterSaveNotNew() ->with('root_category_id') ->willReturn(true); $this->groupMock->expects($this->once())->method('isObjectNew')->willReturn(true); - $this->model->beforeSave($this->subjectMock, $this->groupMock); + $this->assertSame( $this->subjectMock, $this->model->afterSave($this->subjectMock, $this->subjectMock, $this->groupMock) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/StoreViewTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/StoreViewTest.php index 6f39cc9a7b220..d818af8f1233c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/StoreViewTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/StoreViewTest.php @@ -12,6 +12,7 @@ use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Store\Model\ResourceModel\Store; +use Magento\Store\Model\Store as StoreModel; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -20,27 +21,27 @@ class StoreViewTest extends TestCase /** * @var MockObject|IndexerInterface */ - protected $indexerMock; + private $indexerMock; /** * @var MockObject|State */ - protected $stateMock; + private $stateMock; /** * @var StoreView */ - protected $model; + private $model; /** * @var IndexerRegistry|MockObject */ - protected $indexerRegistryMock; + private $indexerRegistryMock; /** - * @var MockObject + * @var Store|MockObject */ - protected $subjectMock; + private $subjectMock; protected function setUp(): void { @@ -65,50 +66,51 @@ protected function setUp(): void $this->model = new StoreView($this->indexerRegistryMock, $this->stateMock); } - public function testBeforeAndAfterSaveNewObject() + public function testAfterSaveNewObject(): void { $this->mockConfigFlatEnabled(); $this->mockIndexerMethods(); $storeMock = $this->createPartialMock( - \Magento\Store\Model\Store::class, + StoreModel::class, ['isObjectNew', 'dataHasChangedFor'] ); $storeMock->expects($this->once())->method('isObjectNew')->willReturn(true); - $this->model->beforeSave($this->subjectMock, $storeMock); + $this->assertSame( $this->subjectMock, $this->model->afterSave($this->subjectMock, $this->subjectMock, $storeMock) ); } - public function testBeforeAndAfterSaveHasChanged() + public function testAfterSaveHasChanged(): void { $storeMock = $this->createPartialMock( - \Magento\Store\Model\Store::class, + StoreModel::class, ['isObjectNew', 'dataHasChangedFor'] ); - $this->model->beforeSave($this->subjectMock, $storeMock); + $this->assertSame( $this->subjectMock, $this->model->afterSave($this->subjectMock, $this->subjectMock, $storeMock) ); } - public function testBeforeAndAfterSaveNoNeed() + public function testAfterSaveNoNeed(): void { $this->mockConfigFlatEnabledNever(); + $storeMock = $this->createPartialMock( - \Magento\Store\Model\Store::class, + StoreModel::class, ['isObjectNew', 'dataHasChangedFor'] ); - $this->model->beforeSave($this->subjectMock, $storeMock); + $this->assertSame( $this->subjectMock, $this->model->afterSave($this->subjectMock, $this->subjectMock, $storeMock) ); } - protected function mockIndexerMethods() + private function mockIndexerMethods(): void { $this->indexerMock->expects($this->once())->method('invalidate'); $this->indexerRegistryMock->expects($this->once()) @@ -117,12 +119,12 @@ protected function mockIndexerMethods() ->willReturn($this->indexerMock); } - protected function mockConfigFlatEnabled() + private function mockConfigFlatEnabled(): void { $this->stateMock->expects($this->once())->method('isFlatEnabled')->willReturn(true); } - protected function mockConfigFlatEnabledNever() + private function mockConfigFlatEnabledNever(): void { $this->stateMock->expects($this->never())->method('isFlatEnabled'); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php index 500f7f714e159..f7064b7e1117c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php @@ -9,9 +9,9 @@ use Magento\Catalog\Model\Indexer\Category\Product; use Magento\Catalog\Model\Indexer\Category\Product\Plugin\StoreGroup; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\Group as GroupModel; use Magento\Store\Model\ResourceModel\Group; use PHPUnit\Framework\MockObject\MockObject; @@ -22,7 +22,7 @@ class StoreGroupTest extends TestCase /** * @var GroupModel|MockObject */ - private $groupMock; + private $groupModelMock; /** * @var MockObject|IndexerInterface @@ -32,13 +32,18 @@ class StoreGroupTest extends TestCase /** * @var MockObject|Group */ - private $subject; + private $subjectMock; /** * @var IndexerRegistry|MockObject */ private $indexerRegistryMock; + /** + * @var TableMaintainer|MockObject + */ + private $tableMaintainerMock; + /** * @var StoreGroup */ @@ -46,7 +51,7 @@ class StoreGroupTest extends TestCase protected function setUp(): void { - $this->groupMock = $this->createPartialMock( + $this->groupModelMock = $this->createPartialMock( GroupModel::class, ['dataHasChangedFor', 'isObjectNew'] ); @@ -59,44 +64,48 @@ protected function setUp(): void true, ['getId', 'getState'] ); - $this->subject = $this->createMock(Group::class); + $this->subjectMock = $this->createMock(Group::class); $this->indexerRegistryMock = $this->createPartialMock(IndexerRegistry::class, ['get']); + $this->tableMaintainerMock = $this->createMock(TableMaintainer::class); - $this->model = (new ObjectManager($this)) - ->getObject(StoreGroup::class, ['indexerRegistry' => $this->indexerRegistryMock]); + $this->model = new StoreGroup($this->indexerRegistryMock, $this->tableMaintainerMock); } /** * @param array $valueMap * @dataProvider changedDataProvider */ - public function testBeforeAndAfterSave($valueMap) + public function testAfterSave(array $valueMap): void { $this->mockIndexerMethods(); - $this->groupMock->expects($this->exactly(2))->method('dataHasChangedFor')->willReturnMap($valueMap); - $this->groupMock->expects($this->once())->method('isObjectNew')->willReturn(false); + $this->groupModelMock->expects($this->exactly(2))->method('dataHasChangedFor')->willReturnMap($valueMap); + $this->groupModelMock->expects($this->once())->method('isObjectNew')->willReturn(false); - $this->model->beforeSave($this->subject, $this->groupMock); - $this->assertSame($this->subject, $this->model->afterSave($this->subject, $this->subject, $this->groupMock)); + $this->assertSame( + $this->subjectMock, + $this->model->afterSave($this->subjectMock, $this->subjectMock, $this->groupModelMock) + ); } /** * @param array $valueMap * @dataProvider changedDataProvider */ - public function testBeforeAndAfterSaveNotNew($valueMap) + public function testAfterSaveNotNew(array $valueMap): void { - $this->groupMock->expects($this->exactly(2))->method('dataHasChangedFor')->willReturnMap($valueMap); - $this->groupMock->expects($this->once())->method('isObjectNew')->willReturn(true); + $this->groupModelMock->expects($this->exactly(2))->method('dataHasChangedFor')->willReturnMap($valueMap); + $this->groupModelMock->expects($this->once())->method('isObjectNew')->willReturn(true); - $this->model->beforeSave($this->subject, $this->groupMock); - $this->assertSame($this->subject, $this->model->afterSave($this->subject, $this->subject, $this->groupMock)); + $this->assertSame( + $this->subjectMock, + $this->model->afterSave($this->subjectMock, $this->subjectMock, $this->groupModelMock) + ); } /** * @return array */ - public function changedDataProvider() + public function changedDataProvider(): array { return [ [ @@ -106,18 +115,20 @@ public function changedDataProvider() ]; } - public function testBeforeAndAfterSaveWithoutChanges() + public function testAfterSaveWithoutChanges(): void { - $this->groupMock->expects($this->exactly(2)) + $this->groupModelMock->expects($this->exactly(2)) ->method('dataHasChangedFor') ->willReturnMap([['root_category_id', false], ['website_id', false]]); - $this->groupMock->expects($this->never())->method('isObjectNew'); + $this->groupModelMock->expects($this->never())->method('isObjectNew'); - $this->model->beforeSave($this->subject, $this->groupMock); - $this->assertSame($this->subject, $this->model->afterSave($this->subject, $this->subject, $this->groupMock)); + $this->assertSame( + $this->subjectMock, + $this->model->afterSave($this->subjectMock, $this->subjectMock, $this->groupModelMock) + ); } - private function mockIndexerMethods() + private function mockIndexerMethods(): void { $this->indexerMock->expects($this->once())->method('invalidate'); $this->indexerRegistryMock->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php index 3108934d50f5a..4ecc2404153ba 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php @@ -27,27 +27,27 @@ class StoreViewTest extends TestCase /** * @var MockObject|IndexerInterface */ - protected $indexerMock; + private $indexerMock; /** * @var StoreView */ - protected $model; + private $model; /** * @var IndexerRegistry|MockObject */ - protected $indexerRegistryMock; + private $indexerRegistryMock; /** * @var TableMaintainer|MockObject */ - protected $tableMaintainer; + private $tableMaintainerMock; /** - * @var MockObject + * @var Group|MockObject */ - protected $subject; + private $subjectMock; protected function setUp(): void { @@ -60,7 +60,7 @@ protected function setUp(): void true, ['getId', 'getState'] ); - $this->subject = $this->createMock(Group::class); + $this->subjectMock = $this->createMock(Group::class); $this->indexerRegistryMock = $this->createPartialMock(IndexerRegistry::class, ['get']); $this->storeMock = $this->createPartialMock( Store::class, @@ -70,47 +70,56 @@ protected function setUp(): void 'dataHasChangedFor' ] ); - $this->tableMaintainer = $this->createPartialMock( + $this->tableMaintainerMock = $this->createPartialMock( TableMaintainer::class, [ 'createTablesForStore' ] ); - $this->model = new StoreView($this->indexerRegistryMock, $this->tableMaintainer); + $this->model = new StoreView($this->indexerRegistryMock, $this->tableMaintainerMock); } - public function testAroundSaveNewObject() + public function testAfterSaveNewObject(): void { $this->mockIndexerMethods(); $this->storeMock->expects($this->atLeastOnce())->method('isObjectNew')->willReturn(true); $this->storeMock->expects($this->atLeastOnce())->method('getId')->willReturn(1); - $this->model->beforeSave($this->subject, $this->storeMock); - $this->assertSame($this->subject, $this->model->afterSave($this->subject, $this->subject, $this->storeMock)); + + $this->assertSame( + $this->subjectMock, + $this->model->afterSave($this->subjectMock, $this->subjectMock, $this->storeMock) + ); } - public function testAroundSaveHasChanged() + public function testAfterSaveHasChanged(): void { $this->mockIndexerMethods(); $this->storeMock->expects($this->once()) ->method('dataHasChangedFor') ->with('group_id') ->willReturn(true); - $this->model->beforeSave($this->subject, $this->storeMock); - $this->assertSame($this->subject, $this->model->afterSave($this->subject, $this->subject, $this->storeMock)); + + $this->assertSame( + $this->subjectMock, + $this->model->afterSave($this->subjectMock, $this->subjectMock, $this->storeMock) + ); } - public function testAroundSaveNoNeed() + public function testAfterSaveNoNeed(): void { $this->storeMock->expects($this->once()) ->method('dataHasChangedFor') ->with('group_id') ->willReturn(false); - $this->model->beforeSave($this->subject, $this->storeMock); - $this->assertSame($this->subject, $this->model->afterSave($this->subject, $this->subject, $this->storeMock)); + + $this->assertSame( + $this->subjectMock, + $this->model->afterSave($this->subjectMock, $this->subjectMock, $this->storeMock) + ); } - private function mockIndexerMethods() + private function mockIndexerMethods(): void { $this->indexerMock->expects($this->once())->method('invalidate'); $this->indexerRegistryMock->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Plugin/StoreViewTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Plugin/StoreViewTest.php index 0d5a3bb93a6bd..4b2c803c66744 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Plugin/StoreViewTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Plugin/StoreViewTest.php @@ -11,50 +11,81 @@ use Magento\Catalog\Model\Indexer\Product\Eav\Processor; use Magento\Framework\Model\AbstractModel; use Magento\Store\Model\ResourceModel\Store; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class StoreViewTest extends TestCase { /** - * @param array $data - * @dataProvider beforeSaveDataProvider + * @var Processor|MockObject + */ + private $eavProcessorMock; + /** + * @var Store|MockObject */ - public function testBeforeSave(array $data) + private $subjectMock; + /** + * @var AbstractModel|MockObject + */ + private $objectMock; + + /** + * @var StoreView + */ + private $storeViewPlugin; + + protected function setUp(): void { - $eavProcessorMock = $this->getMockBuilder(Processor::class) + $this->eavProcessorMock = $this->getMockBuilder(Processor::class) ->disableOriginalConstructor() ->getMock(); - $matcher = $data['matcher']; - $eavProcessorMock->expects($this->$matcher()) - ->method('markIndexerAsInvalid'); - $subjectMock = $this->getMockBuilder(Store::class) + $this->subjectMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->getMock(); - $objectMock = $this->getMockBuilder(AbstractModel::class) + $this->objectMock = $this->getMockBuilder(AbstractModel::class) ->disableOriginalConstructor() ->setMethods(['getId', 'dataHasChangedFor', 'getIsActive']) ->getMock(); - $objectMock->expects($this->any()) + + $this->storeViewPlugin = new StoreView($this->eavProcessorMock); + } + + /** + * @param array $data + * @dataProvider beforeSaveDataProvider + */ + public function testAfterSave(array $data): void + { + $matcher = $data['matcher']; + + $this->eavProcessorMock->expects($this->$matcher()) + ->method('markIndexerAsInvalid'); + + $this->objectMock->expects($this->any()) ->method('getId') ->willReturn($data['object_id']); - $objectMock->expects($this->any()) + + $this->objectMock->expects($this->any()) ->method('dataHasChangedFor') ->with('group_id') ->willReturn($data['has_group_id_changed']); - $objectMock->expects($this->any()) + + $this->objectMock->expects($this->any()) ->method('getIsActive') ->willReturn($data['is_active']); - $model = new StoreView($eavProcessorMock); - $model->beforeSave($subjectMock, $objectMock); + $this->assertSame( + $this->subjectMock, + $this->storeViewPlugin->afterSave($this->subjectMock, $this->subjectMock, $this->objectMock) + ); } /** * @return array */ - public function beforeSaveDataProvider() + public function beforeSaveDataProvider(): array { return [ [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Plugin/StoreGroupTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Plugin/StoreGroupTest.php index 5500081d5e11d..adabfab1560dd 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Plugin/StoreGroupTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Plugin/StoreGroupTest.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Indexer\Product\Flat\Plugin\StoreGroup; use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Store\Model\Group as StoreGroupModel; use Magento\Store\Model\ResourceModel\Group; use Magento\Store\Model\Store; use PHPUnit\Framework\MockObject\MockObject; @@ -19,17 +20,22 @@ class StoreGroupTest extends TestCase /** * @var Processor|MockObject */ - protected $processorMock; + private $processorMock; /** * @var Store|MockObject */ - protected $storeGroupMock; + private $storeGroupMock; /** * @var MockObject */ - protected $subjectMock; + private $subjectMock; + + /** + * @var StoreGroup + */ + private $storeGroupPlugin; protected function setUp(): void { @@ -40,9 +46,11 @@ protected function setUp(): void $this->subjectMock = $this->createMock(Group::class); $this->storeGroupMock = $this->createPartialMock( - \Magento\Store\Model\Group::class, + StoreGroupModel::class, ['getId', 'dataHasChangedFor'] ); + + $this->storeGroupPlugin = new StoreGroup($this->processorMock); } /** @@ -50,14 +58,16 @@ protected function setUp(): void * @param int|null $storeId * @dataProvider storeGroupDataProvider */ - public function testBeforeSave($matcherMethod, $storeId) + public function testAfterSave(string $matcherMethod, ?int $storeId): void { $this->processorMock->expects($this->{$matcherMethod}())->method('markIndexerAsInvalid'); $this->storeGroupMock->expects($this->once())->method('getId')->willReturn($storeId); - $model = new StoreGroup($this->processorMock); - $model->beforeSave($this->subjectMock, $this->storeGroupMock); + $this->assertSame( + $this->subjectMock, + $this->storeGroupPlugin->afterSave($this->subjectMock, $this->subjectMock, $this->storeGroupMock) + ); } /** @@ -65,30 +75,25 @@ public function testBeforeSave($matcherMethod, $storeId) * @param bool $websiteChanged * @dataProvider storeGroupWebsiteDataProvider */ - public function testChangedWebsiteBeforeSave($matcherMethod, $websiteChanged) + public function testAfterSaveChangedWebsite(string $matcherMethod, bool $websiteChanged): void { $this->processorMock->expects($this->{$matcherMethod}())->method('markIndexerAsInvalid'); $this->storeGroupMock->expects($this->once())->method('getId')->willReturn(1); - $this->storeGroupMock->expects( - $this->once() - )->method( - 'dataHasChangedFor' - )->with( - 'root_category_id' - )->willReturn( - $websiteChanged - ); + $this->storeGroupMock->expects($this->once())->method('dataHasChangedFor') + ->with('root_category_id')->willReturn($websiteChanged); - $model = new StoreGroup($this->processorMock); - $model->beforeSave($this->subjectMock, $this->storeGroupMock); + $this->assertSame( + $this->subjectMock, + $this->storeGroupPlugin->afterSave($this->subjectMock, $this->subjectMock, $this->storeGroupMock) + ); } /** * @return array */ - public function storeGroupWebsiteDataProvider() + public function storeGroupWebsiteDataProvider(): array { return [['once', true], ['never', false]]; } @@ -96,7 +101,7 @@ public function storeGroupWebsiteDataProvider() /** * @return array */ - public function storeGroupDataProvider() + public function storeGroupDataProvider(): array { return [['once', null], ['never', 1]]; } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Plugin/StoreTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Plugin/StoreTest.php index 1b74039fa944c..219149a57d361 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Plugin/StoreTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Plugin/StoreTest.php @@ -7,7 +7,9 @@ namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Flat\Plugin; +use Magento\Catalog\Model\Indexer\Product\Flat\Plugin\Store as StorePlugin; use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Store\Model\ResourceModel\Store as StoreResourceModel; use Magento\Store\Model\Store; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -17,17 +19,22 @@ class StoreTest extends TestCase /** * @var Processor|MockObject */ - protected $processorMock; + private $processorMock; /** * @var Store|MockObject */ - protected $storeMock; + private $storeMock; /** * @var MockObject */ - protected $subjectMock; + private $subjectMock; + + /** + * @var StorePlugin + */ + private $storePlugin; protected function setUp(): void { @@ -36,11 +43,13 @@ protected function setUp(): void ['markIndexerAsInvalid'] ); - $this->subjectMock = $this->createMock(\Magento\Store\Model\ResourceModel\Store::class); + $this->subjectMock = $this->createMock(StoreResourceModel::class); $this->storeMock = $this->createPartialMock( Store::class, ['getId', 'dataHasChangedFor'] ); + + $this->storePlugin = new StorePlugin($this->processorMock); } /** @@ -48,14 +57,16 @@ protected function setUp(): void * @param int|null $storeId * @dataProvider storeDataProvider */ - public function testBeforeSave($matcherMethod, $storeId) + public function testAfterSave(string $matcherMethod, ?int $storeId): void { $this->processorMock->expects($this->{$matcherMethod}())->method('markIndexerAsInvalid'); $this->storeMock->expects($this->once())->method('getId')->willReturn($storeId); - $model = new \Magento\Catalog\Model\Indexer\Product\Flat\Plugin\Store($this->processorMock); - $model->beforeSave($this->subjectMock, $this->storeMock); + $this->assertSame( + $this->subjectMock, + $this->storePlugin->afterSave($this->subjectMock, $this->subjectMock, $this->storeMock) + ); } /** @@ -63,30 +74,25 @@ public function testBeforeSave($matcherMethod, $storeId) * @param bool $storeGroupChanged * @dataProvider storeGroupDataProvider */ - public function testBeforeSaveSwitchStoreGroup($matcherMethod, $storeGroupChanged) + public function testAfterSaveSwitchStoreGroup(string $matcherMethod, bool $storeGroupChanged): void { $this->processorMock->expects($this->{$matcherMethod}())->method('markIndexerAsInvalid'); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); - $this->storeMock->expects( - $this->once() - )->method( - 'dataHasChangedFor' - )->with( - 'group_id' - )->willReturn( - $storeGroupChanged - ); + $this->storeMock->expects($this->once())->method('dataHasChangedFor') + ->with('group_id')->willReturn($storeGroupChanged); - $model = new \Magento\Catalog\Model\Indexer\Product\Flat\Plugin\Store($this->processorMock); - $model->beforeSave($this->subjectMock, $this->storeMock); + $this->assertSame( + $this->subjectMock, + $this->storePlugin->afterSave($this->subjectMock, $this->subjectMock, $this->storeMock) + ); } /** * @return array */ - public function storeGroupDataProvider() + public function storeGroupDataProvider(): array { return [['once', true], ['never', false]]; } @@ -94,7 +100,7 @@ public function storeGroupDataProvider() /** * @return array */ - public function storeDataProvider() + public function storeDataProvider(): array { return [['once', null], ['never', 1]]; } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/OptionManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/OptionManagementTest.php index edbbaebd0576b..05bd3ec2a3d33 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/OptionManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/OptionManagementTest.php @@ -10,10 +10,14 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Model\Product\Attribute\OptionManagement; use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Eav\Api\AttributeOptionUpdateInterface; use Magento\Eav\Api\Data\AttributeOptionInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Class to test management of attribute options + */ class OptionManagementTest extends TestCase { /** @@ -22,18 +26,28 @@ class OptionManagementTest extends TestCase protected $model; /** - * @var MockObject + * @var AttributeOptionManagementInterface|MockObject */ protected $eavOptionManagementMock; + /** + * @var AttributeOptionUpdateInterface|MockObject + */ + private $eavOptionUpdateMock; + protected function setUp(): void { $this->eavOptionManagementMock = $this->getMockForAbstractClass(AttributeOptionManagementInterface::class); + $this->eavOptionUpdateMock = $this->getMockForAbstractClass(AttributeOptionUpdateInterface::class); $this->model = new OptionManagement( - $this->eavOptionManagementMock + $this->eavOptionManagementMock, + $this->eavOptionUpdateMock ); } + /** + * Test to Retrieve list of attribute options + */ public function testGetItems() { $attributeCode = 10; @@ -44,6 +58,9 @@ public function testGetItems() $this->assertEquals([], $this->model->getItems($attributeCode)); } + /** + * Test to Add option to attribute + */ public function testAdd() { $attributeCode = 42; @@ -56,6 +73,9 @@ public function testAdd() $this->assertTrue($this->model->add($attributeCode, $optionMock)); } + /** + * Test to delete attribute option + */ public function testDelete() { $attributeCode = 'atrCde'; @@ -68,6 +88,9 @@ public function testDelete() $this->assertTrue($this->model->delete($attributeCode, $optionId)); } + /** + * Test to delete attribute option with invalid option id + */ public function testDeleteWithInvalidOption() { $this->expectException('Magento\Framework\Exception\InputException'); @@ -77,4 +100,24 @@ public function testDeleteWithInvalidOption() $this->eavOptionManagementMock->expects($this->never())->method('delete'); $this->model->delete($attributeCode, $optionId); } + + /** + * Test to update attribute option + */ + public function testUpdate() + { + $attributeCode = 'atrCde'; + $optionId = 10; + $optionMock = $this->getMockForAbstractClass(AttributeOptionInterface::class); + + $this->eavOptionUpdateMock->expects($this->once()) + ->method('update') + ->with( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeCode, + $optionId, + $optionMock + )->willReturn(true); + $this->assertTrue($this->model->update($attributeCode, $optionId, $optionMock)); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Type/FileTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Type/FileTest.php index 539489f18f404..0e6fb8ececf03 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Type/FileTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Type/FileTest.php @@ -24,6 +24,8 @@ use PHPUnit\Framework\TestCase; /** + * Test file option type + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FileTest extends TestCase @@ -142,6 +144,14 @@ protected function getFileObject() ); } + public function testGetFormattedOptionValueWithUnserializedValue() + { + $fileObject = $this->getFileObject(); + + $value = 'some unserialized value, 1, 2.test'; + $this->assertEquals($value, $fileObject->getFormattedOptionValue($value)); + } + public function testGetCustomizedView() { $fileObject = $this->getFileObject(); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php index e03ea8c79cc8a..e46884d1637da 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php @@ -12,13 +12,11 @@ use Magento\Catalog\Model\Product\Option\Value; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; use Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator; use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\PriceInfoInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** @@ -36,11 +34,6 @@ class ValueTest extends TestCase */ private $customOptionPriceCalculatorMock; - /** - * @var CalculateCustomOptionCatalogRule|MockObject - */ - private $CalculateCustomOptionCatalogRule; - protected function setUp(): void { $mockedResource = $this->getMockedResource(); @@ -50,10 +43,6 @@ protected function setUp(): void CustomOptionPriceCalculator::class ); - $this->CalculateCustomOptionCatalogRule = $this->createMock( - CalculateCustomOptionCatalogRule::class - ); - $helper = new ObjectManager($this); $this->model = $helper->getObject( Value::class, @@ -61,7 +50,6 @@ protected function setUp(): void 'resource' => $mockedResource, 'valueCollectionFactory' => $mockedCollectionFactory, 'customOptionPriceCalculator' => $this->customOptionPriceCalculatorMock, - 'CalculateCustomOptionCatalogRule' => $this->CalculateCustomOptionCatalogRule ] ); $this->model->setOption($this->getMockedOption()); @@ -89,8 +77,8 @@ public function testGetPrice() $this->assertEquals($price, $this->model->getPrice(false)); $percentPrice = 100.0; - $this->CalculateCustomOptionCatalogRule->expects($this->atLeastOnce()) - ->method('execute') + $this->customOptionPriceCalculatorMock->expects($this->atLeastOnce()) + ->method('getOptionPriceByPriceCode') ->willReturn($percentPrice); $this->assertEquals($percentPrice, $this->model->getPrice(true)); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php index d326f5fb19520..8aa29ca6688a3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php @@ -142,6 +142,7 @@ public function testGetList($configValue, $customerGroupId, $groupData, $expecte ->method('getValue') ->with('catalog/price/scope', ScopeInterface::SCOPE_WEBSITE) ->willReturn($configValue); + $priceMock = null; if ($expected) { $priceMock = $this->getMockForAbstractClass(ProductTierPriceInterface::class); $priceMock->expects($this->once()) @@ -203,7 +204,9 @@ public function testSuccessDeleteTierPrice() ->method('getValue') ->with('catalog/price/scope', ScopeInterface::SCOPE_WEBSITE) ->willReturn(0); - $this->priceModifierMock->expects($this->once())->method('removeTierPrice')->with($this->productMock, 4, 5, 0); + $this->priceModifierMock->expects($this->once()) + ->method('removeTierPrice') + ->with($this->productMock, 4, 5, 0); $this->assertTrue($this->service->remove('product_sku', 4, 5, 0)); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php index aa90f1d8924a2..129873a067d97 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php @@ -48,12 +48,20 @@ use PHPUnit\Framework\TestCase; /** + * Test for \Magento\Catalog\Model\ProductRepository. + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ class ProductRepositoryTest extends TestCase { + private const STUB_STORE_ID = 1; + private const STUB_STORE_ID_GLOBAL = 0; + private const STUB_PRODUCT_ID = 100; + private const STUB_PRODUCT_NAME = 'name'; + private const STUB_PRODUCT_SKU = 'sku'; + /** * @var Product|MockObject */ @@ -298,6 +306,7 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods([]) ->getMockForAbstractClass(); + $storeMock->method('getId')->willReturn(self::STUB_STORE_ID); $storeMock->expects($this->any())->method('getWebsiteId')->willReturn('1'); $storeMock->expects($this->any())->method('getCode')->willReturn(Store::ADMIN_CODE); $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeMock); @@ -351,6 +360,72 @@ function ($value) { $this->objectManager->setBackwardCompatibleProperty($this->model, 'mediaProcessor', $mediaProcessor); } + /** + * Test save product with global store id + * + * @param array $productData + * @return void + * @dataProvider getProductData + */ + public function testSaveForAllStoreViewScope(array $productData): void + { + $this->productFactory->method('create')->willReturn($this->product); + $this->product->method('getSku')->willReturn($productData['sku']); + $this->extensibleDataObjectConverter + ->expects($this->once()) + ->method('toNestedArray') + ->willReturn($productData); + $this->resourceModel->method('getIdBySku')->willReturn(self::STUB_PRODUCT_ID); + $this->resourceModel->expects($this->once())->method('validate')->willReturn(true); + $this->product->expects($this->at(14))->method('setData') + ->with('store_id', $productData['store_id']); + + $this->model->save($this->product); + } + + /** + * Product data provider + * + * @return array + */ + public function getProductData(): array + { + return [ + [ + [ + 'sku' => self::STUB_PRODUCT_SKU, + 'name' => self::STUB_PRODUCT_NAME, + 'store_id' => self::STUB_STORE_ID_GLOBAL, + ], + ], + ]; + } + + /** + * Test save product without store + * + * @return void + */ + public function testSaveWithoutStoreId(): void + { + $this->productFactory->method('create')->willReturn($this->product); + $this->product->method('getSku')->willReturn($this->productData['sku']); + $this->extensibleDataObjectConverter + ->expects($this->once()) + ->method('toNestedArray') + ->willReturn($this->productData); + $this->resourceModel->method('getIdBySku')->willReturn(self::STUB_PRODUCT_ID); + $this->resourceModel->expects($this->once())->method('validate')->willReturn(true); + $this->product->expects($this->at(15))->method('setData') + ->with('store_id', self::STUB_STORE_ID); + + $this->model->save($this->product); + } + + /** + * @expectedException \Magento\Framework\Exception\NoSuchEntityException + * @expectedExceptionMessage The product that was requested doesn't exist. Verify the product and try again. + */ public function testGetAbsentProduct() { $this->expectException('Magento\Framework\Exception\NoSuchEntityException'); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php new file mode 100644 index 0000000000000..c73e02fb7ecbf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\ResourceModel\Category\AggregateCount; +use Magento\Catalog\Model\ResourceModel\Category as ResourceCategory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Aggregate count model test + */ +class AggregateCountTest extends TestCase +{ + + /** + * @var AggregateCount + */ + protected $aggregateCount; + + /** + * @var ObjectManagerHelper + */ + protected $objectManagerHelper; + + /** + * @var Category|MockObject + */ + protected $categoryMock; + + /** + * @var ResourceCategory|MockObject + */ + protected $resourceCategoryMock; + + /** + * @var AdapterInterface|MockObject + */ + protected $connectionMock; + + /** + * {@inheritdoc} + */ + public function setUp(): void + { + $this->categoryMock = $this->createMock(Category::class); + $this->resourceCategoryMock = $this->createMock(ResourceCategory::class); + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class) + ->getMockForAbstractClass(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->aggregateCount = $this->objectManagerHelper->getObject(AggregateCount::class); + } + + /** + * @return void + */ + public function testProcessDelete(): void + { + $parentIds = 3; + $table = 'catalog_category_entity'; + + $this->categoryMock->expects($this->once()) + ->method('getResource') + ->willReturn($this->resourceCategoryMock); + $this->categoryMock->expects($this->once()) + ->method('getParentIds') + ->willReturn($parentIds); + $this->resourceCategoryMock->expects($this->any()) + ->method('getEntityTable') + ->willReturn($table); + $this->resourceCategoryMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $this->connectionMock->expects($this->once()) + ->method('update') + ->with( + $table, + ['children_count' => new \Zend_Db_Expr('children_count - 1')], + ['entity_id IN(?)' => $parentIds] + ); + $this->aggregateCount->processDelete($this->categoryMock); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CalculateCustomOptionCatalogRuleTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CalculateCustomOptionCatalogRuleTest.php deleted file mode 100644 index 894408048b536..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CalculateCustomOptionCatalogRuleTest.php +++ /dev/null @@ -1,266 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Pricing\Price; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\PriceModifier\Composite as PriceModifier; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; -use Magento\Catalog\Pricing\Price\RegularPrice; -use Magento\Catalog\Pricing\Price\SpecialPrice; -use Magento\CatalogRule\Pricing\Price\CatalogRulePrice; -use Magento\Directory\Model\PriceCurrency; -use Magento\Framework\Pricing\PriceInfo\Base; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Test for CalculateCustomOptionCatalogRule class. - */ -class CalculateCustomOptionCatalogRuleTest extends TestCase -{ - /** - * @var Product|MockObject - */ - private $saleableItemMock; - - /** - * @var RegularPrice|MockObject - */ - private $regularPriceMock; - - /** - * @var SpecialPrice|MockObject - */ - private $specialPriceMock; - - /** - * @var CatalogRulePrice|MockObject - */ - private $catalogRulePriceMock; - - /** - * @var PriceModifier|MockObject - */ - private $priceModifierMock; - - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $objectManager = new ObjectManager($this); - $this->saleableItemMock = $this->createMock(Product::class); - $this->regularPriceMock = $this->createMock(RegularPrice::class); - $this->specialPriceMock = $this->createMock(SpecialPrice::class); - $this->catalogRulePriceMock = $this->createMock(CatalogRulePrice::class); - $priceInfoMock = $this->createMock(Base::class); - $this->saleableItemMock->expects($this->any()) - ->method('getPriceInfo') - ->willReturn($priceInfoMock); - $this->regularPriceMock->expects($this->any()) - ->method('getPriceCode') - ->willReturn(RegularPrice::PRICE_CODE); - $this->specialPriceMock->expects($this->any()) - ->method('getPriceCode') - ->willReturn(SpecialPrice::PRICE_CODE); - $this->catalogRulePriceMock->expects($this->any()) - ->method('getPriceCode') - ->willReturn(CatalogRulePrice::PRICE_CODE); - $priceInfoMock->expects($this->any()) - ->method('getPrices') - ->willReturn( - [ - 'regular_price' => $this->regularPriceMock, - 'special_price' => $this->specialPriceMock, - 'catalog_rule_price' => $this->catalogRulePriceMock - ] - ); - $priceInfoMock->expects($this->any()) - ->method('getPrice') - ->willReturnMap( - [ - ['regular_price', $this->regularPriceMock], - ['special_price', $this->specialPriceMock], - ['catalog_rule_price', $this->catalogRulePriceMock], - ] - ); - $priceCurrencyMock = $this->createMock(PriceCurrency::class); - $priceCurrencyMock->expects($this->any()) - ->method('convertAndRound') - ->willReturnArgument(0); - $this->priceModifierMock = $this->createMock(PriceModifier::class); - - $this->calculateCustomOptionCatalogRule = $objectManager->getObject( - CalculateCustomOptionCatalogRule::class, - [ - 'priceCurrency' => $priceCurrencyMock, - 'priceModifier' => $this->priceModifierMock, - ] - ); - } - - /** - * Tests correct option price calculation with different catalog rules and special prices combination. - * - * @dataProvider executeDataProvider - * @param array $prices - * @param float $catalogRulePriceModifier - * @param float $optionPriceValue - * @param bool $isPercent - * @param float $expectedResult - */ - public function testExecute( - array $prices, - float $catalogRulePriceModifier, - float $optionPriceValue, - bool $isPercent, - float $expectedResult - ) { - $this->regularPriceMock->expects($this->any()) - ->method('getValue') - ->willReturn($prices['regularPriceValue']); - $this->specialPriceMock->expects($this->any()) - ->method('getValue') - ->willReturn($prices['specialPriceValue']); - $this->priceModifierMock->expects($this->any()) - ->method('modifyPrice') - ->willReturnCallback( - function ($price) use ($catalogRulePriceModifier) { - return $price * $catalogRulePriceModifier; - } - ); - - $finalPrice = $this->calculateCustomOptionCatalogRule->execute( - $this->saleableItemMock, - $optionPriceValue, - $isPercent - ); - - $this->assertSame($expectedResult, $finalPrice); - } - - /** - * Data provider for testExecute. - * - * "Active" means this price type has biggest discount, so other prices doesn't count. - * - * @return array - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function executeDataProvider(): array - { - return [ - 'No special price, no catalog price rules, fixed option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 1000, - ], - 'catalogRulePriceModifier' => 1.0, - 'optionPriceValue' => 100.0, - 'isPercent' => false, - 'expectedResult' => 100.0 - ], - 'No special price, no catalog price rules, percent option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 1000, - ], - 'catalogRulePriceModifier' => 1.0, - 'optionPriceValue' => 100.0, - 'isPercent' => true, - 'expectedResult' => 1000.0 - ], - 'No special price, catalog price rule set, fixed option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 1000, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => false, - 'expectedResult' => 90.0 - ], - 'No special price, catalog price rule set, percent option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 1000, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => true, - 'expectedResult' => 900.0 - ], - 'Special price set, no catalog price rule, fixed option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 900, - ], - 'catalogRulePriceModifier' => 1.0, - 'optionPriceValue' => 100.0, - 'isPercent' => false, - 'expectedResult' => 100.0 - ], - 'Special price set, no catalog price rule, percent option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 900, - ], - 'catalogRulePriceModifier' => 1.0, - 'optionPriceValue' => 100.0, - 'isPercent' => true, - 'expectedResult' => 900.0 - ], - 'Special price set and active, catalog price rule set, fixed option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 800, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => false, - 'expectedResult' => 100.0 - ], - 'Special price set and active, catalog price rule set, percent option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 800, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => true, - 'expectedResult' => 800.0 - ], - 'Special price set, catalog price rule set and active, fixed option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 950, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => false, - 'expectedResult' => 90.0 - ], - 'Special price set, catalog price rule set and active, percent option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 950, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => true, - 'expectedResult' => 900.0 - ], - ]; - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index 254d893d24584..17318d4207841 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -7,13 +7,16 @@ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; -use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Categories; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Framework\AuthorizationInterface; use Magento\Framework\DB\Helper as DbHelper; use Magento\Framework\UrlInterface; use Magento\Store\Model\Store; +use Magento\Backend\Model\Auth\Session; +use Magento\Authorization\Model\Role; +use Magento\User\Model\User; use PHPUnit\Framework\MockObject\MockObject; /** @@ -51,6 +54,11 @@ class CategoriesTest extends AbstractModifierTest */ private $authorizationMock; + /** + * @var Session|MockObject + */ + private $sessionMock; + protected function setUp(): void { parent::setUp(); @@ -72,7 +80,10 @@ protected function setUp(): void $this->authorizationMock = $this->getMockBuilder(AuthorizationInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - + $this->sessionMock = $this->getMockBuilder(Session::class) + ->setMethods(['getUser']) + ->disableOriginalConstructor() + ->getMock(); $this->categoryCollectionFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->categoryCollectionMock); @@ -88,6 +99,26 @@ protected function setUp(): void $this->categoryCollectionMock->expects($this->any()) ->method('getIterator') ->willReturn(new \ArrayIterator([])); + + $roleAdmin = $this->getMockBuilder(Role::class) + ->setMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $roleAdmin->expects($this->any()) + ->method('getId') + ->willReturn(0); + + $userAdmin = $this->getMockBuilder(User::class) + ->setMethods(['getRole']) + ->disableOriginalConstructor() + ->getMock(); + $userAdmin->expects($this->any()) + ->method('getRole') + ->willReturn($roleAdmin); + + $this->sessionMock->expects($this->any()) + ->method('getUser') + ->willReturn($userAdmin); } /** @@ -101,11 +132,28 @@ protected function createModel() 'locator' => $this->locatorMock, 'categoryCollectionFactory' => $this->categoryCollectionFactoryMock, 'arrayManager' => $this->arrayManagerMock, - 'authorization' => $this->authorizationMock + 'authorization' => $this->authorizationMock, + 'session' => $this->sessionMock ] ); } + /** + * @param object $object + * @param string $method + * @param array $args + * @return mixed + * @throws \ReflectionException + */ + private function invokeMethod($object, $method, $args = []) + { + $class = new \ReflectionClass(Categories::class); + $method = $class->getMethod($method); + $method->setAccessible(true); + + return $method->invokeArgs($object, $args); + } + public function testModifyData() { $this->assertSame([], $this->getModel()->modifyData([])); @@ -176,4 +224,44 @@ public function modifyMetaLockedDataProvider() { return [[true], [false]]; } + + /** + * Asserts that a user with an ACL role ID of 0 and a user with an ACL role ID of 1 do not have the same cache IDs + * Assumes a store ID of 0 + * + * @throws \ReflectionException + */ + public function testAclCacheIds() + { + $categoriesAdmin = $this->createModel(); + $cacheIdAdmin = $this->invokeMethod($categoriesAdmin, 'getCategoriesTreeCacheId', [0]); + + $roleAclUser = $this->getMockBuilder(Role::class) + ->disableOriginalConstructor() + ->getMock(); + $roleAclUser->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $userAclUser = $this->getMockBuilder(User::class) + ->disableOriginalConstructor() + ->getMock(); + $userAclUser->expects($this->any()) + ->method('getRole') + ->will($this->returnValue($roleAclUser)); + + $this->sessionMock = $this->getMockBuilder(Session::class) + ->setMethods(['getUser']) + ->disableOriginalConstructor() + ->getMock(); + + $this->sessionMock->expects($this->any()) + ->method('getUser') + ->will($this->returnValue($userAclUser)); + + $categoriesAclUser = $this->createModel(); + $cacheIdAclUser = $this->invokeMethod($categoriesAclUser, 'getCategoriesTreeCacheId', [0]); + + $this->assertNotEquals($cacheIdAdmin, $cacheIdAclUser); + } } diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php index c80b2663d1f69..6b85ade0995a0 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php @@ -109,6 +109,7 @@ public function prepare() * Apply sorting. * * @return void + * @since 103.0.2 */ protected function applySorting() { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php index 1f154d3204454..9b328e9bcc199 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php @@ -54,7 +54,6 @@ public function __construct( /** * {@inheritdoc} - * @since 101.0.0 */ public function modifyData(array $data) { @@ -63,7 +62,6 @@ public function modifyData(array $data) /** * {@inheritdoc} - * @since 101.0.0 */ public function modifyMeta(array $meta) { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index 7608173c8edfc..73d29819c1153 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php @@ -18,12 +18,14 @@ use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; use Magento\Framework\AuthorizationInterface; +use Magento\Backend\Model\Auth\Session; /** * Data provider for categories field of product page * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 101.0.0 */ class Categories extends AbstractModifier @@ -48,7 +50,7 @@ class Categories extends AbstractModifier /** * @var array - * @deprecated 101.0.3 + * @deprecated 101.0.0 * @since 101.0.0 */ protected $categoriesTrees = []; @@ -86,6 +88,11 @@ class Categories extends AbstractModifier */ private $authorization; + /** + * @var Session + */ + private $session; + /** * @param LocatorInterface $locator * @param CategoryCollectionFactory $categoryCollectionFactory @@ -94,6 +101,7 @@ class Categories extends AbstractModifier * @param ArrayManager $arrayManager * @param SerializerInterface $serializer * @param AuthorizationInterface $authorization + * @param Session $session */ public function __construct( LocatorInterface $locator, @@ -102,7 +110,8 @@ public function __construct( UrlInterface $urlBuilder, ArrayManager $arrayManager, SerializerInterface $serializer = null, - AuthorizationInterface $authorization = null + AuthorizationInterface $authorization = null, + Session $session = null ) { $this->locator = $locator; $this->categoryCollectionFactory = $categoryCollectionFactory; @@ -111,6 +120,7 @@ public function __construct( $this->arrayManager = $arrayManager; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); $this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class); + $this->session = $session ?: ObjectManager::getInstance()->get(Session::class); } /** @@ -370,10 +380,16 @@ protected function getCategoriesTree($filter = null) * @param string $filter * @return string */ - private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string + private function getCategoriesTreeCacheId(int $storeId, string $filter = ''): string { + if ($this->session->getUser() !== null) { + return self::CATEGORY_TREE_ID + . '_' . (string)$storeId + . '_' . $this->session->getUser()->getAclRole() + . '_' . $filter; + } return self::CATEGORY_TREE_ID - . '_' . (string) $storeId + . '_' . (string)$storeId . '_' . $filter; } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index 0295e778f2b9b..dd757841410e2 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -40,7 +40,7 @@ use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory; /** - * Data provider for eav attributes on product page + * Class Eav data provider for product editing form * * @api * @@ -791,7 +791,9 @@ private function getAttributeDefaultValue(ProductAttributeInterface $attribute) \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $this->storeManager->getStore() ); - $attribute->setDefaultValue($defaultValue); + if ($defaultValue !== null) { + $attribute->setDefaultValue($defaultValue); + } } return $attribute->getDefaultValue(); } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php index 453be0c1a1582..a0935de84627a 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php @@ -50,7 +50,6 @@ private function extractLayoutUpdate(ProductInterface $product) /** * @inheritdoc - * @since 101.1.0 */ public function modifyData(array $data) { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php index c64d3e2e4effb..e9e8229e581ba 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php @@ -18,7 +18,7 @@ * Tier prices modifier adds price type option to tier prices. * * @api - * @since 101.1.0 + * @since 102.0.0 */ class TierPrice extends AbstractModifier { @@ -46,7 +46,7 @@ public function __construct( /** * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ public function modifyData(array $data) { @@ -56,7 +56,7 @@ public function modifyData(array $data) /** * Add tier price info to meta array. * - * @since 101.1.0 + * @since 102.0.0 * @param array $meta * @return array */ diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php index d8f76c40e8fad..2324ca27ffaaf 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php @@ -54,7 +54,7 @@ class Image implements ProductRenderCollectorInterface /** * @var DesignInterface - * @deprecated 2.3.0 DesignLoader is used for design theme loading + * @deprecated 103.0.1 DesignLoader is used for design theme loading */ private $design; diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php index a518afc576d61..b5d2bad1d348e 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php @@ -130,6 +130,7 @@ public function addFilter(\Magento\Framework\Api\Filter $filter) /** * @inheritdoc + * @since 103.0.0 */ public function getMeta() { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php index 3f16e0a6617da..3ea21223816c1 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php @@ -12,7 +12,7 @@ * Allows to collect absolutely different product render information from different modules * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ProductRenderCollectorInterface { @@ -22,7 +22,7 @@ interface ProductRenderCollectorInterface * @param ProductInterface $product * @param ProductRenderInterface $productRender * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function collect(ProductInterface $product, ProductRenderInterface $productRender); } 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 1c97c920266df..ddd66a5bf04bd 100644 --- a/app/code/Magento/Catalog/etc/db_schema.xml +++ b/app/code/Magento/Catalog/etc/db_schema.xml @@ -138,6 +138,11 @@ <index referenceId="CATALOG_PRODUCT_ENTITY_INT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> + <index referenceId="CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID_STORE_ID_VALUE" indexType="btree"> + <column name="attribute_id"/> + <column name="store_id"/> + <column name="value"/> + </index> </table> <table name="catalog_product_entity_text" resource="default" engine="innodb" comment="Catalog Product Text Attribute Backend Table"> @@ -149,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> @@ -403,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/etc/db_schema_whitelist.json b/app/code/Magento/Catalog/etc/db_schema_whitelist.json index d4bd6927d4345..f4cda73c371d0 100644 --- a/app/code/Magento/Catalog/etc/db_schema_whitelist.json +++ b/app/code/Magento/Catalog/etc/db_schema_whitelist.json @@ -69,7 +69,8 @@ }, "index": { "CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID": true, - "CATALOG_PRODUCT_ENTITY_INT_STORE_ID": true + "CATALOG_PRODUCT_ENTITY_INT_STORE_ID": true, + "CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID_STORE_ID_VALUE": true }, "constraint": { "PRIMARY": true, diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 5a7a3135b4bfe..97a787c87bfa8 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -35,6 +35,7 @@ <preference for="Magento\Catalog\Api\Data\ProductAttributeTypeInterface" type="Magento\Catalog\Model\Product\Attribute\Type" /> <preference for="Magento\Catalog\Api\ProductAttributeGroupRepositoryInterface" type="Magento\Catalog\Model\ProductAttributeGroupRepository" /> <preference for="Magento\Catalog\Api\ProductAttributeOptionManagementInterface" type="Magento\Catalog\Model\Product\Attribute\OptionManagement" /> + <preference for="Magento\Catalog\Api\ProductAttributeOptionUpdateInterface" type="Magento\Catalog\Model\Product\Attribute\OptionManagement" /> <preference for="Magento\Catalog\Api\ProductLinkRepositoryInterface" type="Magento\Catalog\Model\ProductLink\Repository" /> <preference for="Magento\Catalog\Api\Data\ProductAttributeSearchResultsInterface" type="Magento\Catalog\Model\ProductAttributeSearchResults" /> <preference for="Magento\Catalog\Api\Data\CategoryAttributeSearchResultsInterface" type="Magento\Catalog\Model\CategoryAttributeSearchResults" /> diff --git a/app/code/Magento/Catalog/etc/frontend/di.xml b/app/code/Magento/Catalog/etc/frontend/di.xml index ee9c5b29da894..9c7c3e04b0772 100644 --- a/app/code/Magento/Catalog/etc/frontend/di.xml +++ b/app/code/Magento/Catalog/etc/frontend/di.xml @@ -13,11 +13,6 @@ </argument> </arguments> </virtualType> - <type name="Magento\Catalog\Model\ResourceModel\Category\Collection"> - <arguments> - <argument name="fetchStrategy" xsi:type="object">Magento\Catalog\Model\ResourceModel\Category\Collection\FetchStrategy</argument> - </arguments> - </type> <type name="Magento\Catalog\Model\Indexer\AbstractFlatState"> <arguments> <argument name="isAvailable" xsi:type="boolean">true</argument> @@ -120,4 +115,13 @@ <plugin name="catalog_app_action_dispatch_controller_context_plugin" type="Magento\Catalog\Plugin\Framework\App\Action\ContextPlugin" /> </type> + <type name="\Magento\PageCache\Model\PageCacheTagsPreprocessorComposite"> + <arguments> + <argument name="preprocessors" xsi:type="array"> + <item name="catalog_product_view" xsi:type="array"> + <item name="product_not_found" xsi:type="object">Magento\Catalog\Model\ProductNotFoundPageCacheTags</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/webapi.xml b/app/code/Magento/Catalog/etc/webapi.xml index 3f82175ab02eb..5e799cd9f426d 100644 --- a/app/code/Magento/Catalog/etc/webapi.xml +++ b/app/code/Magento/Catalog/etc/webapi.xml @@ -183,6 +183,12 @@ <resource ref="Magento_Catalog::attributes_attributes" /> </resources> </route> + <route url="/V1/products/attributes/:attributeCode/options/:optionId" method="PUT"> + <service class="Magento\Catalog\Api\ProductAttributeOptionUpdateInterface" method="update" /> + <resources> + <resource ref="Magento_Catalog::attributes_attributes" /> + </resources> + </route> <route url="/V1/products/attributes/:attributeCode/options/:optionId" method="DELETE"> <service class="Magento\Catalog\Api\ProductAttributeOptionManagementInterface" method="delete" /> <resources> diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_index.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_index.xml index c503196cc8647..89f34a21415d3 100644 --- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_index.xml +++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_index.xml @@ -21,6 +21,7 @@ <referenceContainer name="content"> <uiComponent name="product_listing"/> <block class="Magento\Catalog\Block\Adminhtml\Product" name="products_list"/> + <block class="Magento\Backend\Block\Template" template="Magento_Catalog::product/grid/url_filter_applier.phtml" name="product_list_url_filter_applier" /> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml index c77b66733afc4..c19f140687bbc 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml @@ -6,13 +6,16 @@ /** * @var $block \Magento\Catalog\Block\Adminhtml\Category\Edit + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div data-id="information-dialog-category" class="messages" style="display: none;"> +<div data-id="information-dialog-category" class="messages"> <div class="message message-notice"> <div><?= $block->escapeHtml(__('This operation can take a long time')) ?></div> </div> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'div[data-id="information-dialog-category"]') ?> + <script type="text/x-magento-init"> { "*": { diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml index af7aec12a57ed..e52b43b1c3d24 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml @@ -5,8 +5,9 @@ */ /** @var \Magento\Catalog\Block\Adminhtml\Category\AssignProducts $block */ - /** @var \Magento\Catalog\Block\Adminhtml\Category\Tab\Product $blockGrid */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $blockGrid = $block->getBlockGrid(); $gridJsObjectName = $blockGrid->getJsObjectName(); ?> @@ -23,6 +24,4 @@ $gridJsObjectName = $blockGrid->getJsObjectName(); } </script> <!-- @todo remove when "UI components" will support such initialization --> -<script> - require('mage/apply/main').apply(); -</script> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], "require('mage/apply/main').apply();", false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml index d6340330df8ea..0cd00e88f4350 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml @@ -5,36 +5,48 @@ */ /** @var $block \Magento\Catalog\Block\Adminhtml\Category\Tree */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="categories-side-col"> <div class="sidebar-actions"> - <?php if ($block->getRoot()) :?> + <?php if ($block->getRoot()):?> <?= $block->getAddRootButtonHtml() ?><br/> <?= $block->getAddSubButtonHtml() ?> <?php endif; ?> </div> <div class="tree-actions"> - <?php if ($block->getRoot()) :?> - <?php //echo $block->getCollapseButtonHtml() ?> - <?php //echo $block->getExpandButtonHtml() ?> - <a href="#" - onclick="tree.collapseTree(); return false;"><?= $block->escapeHtml(__('Collapse All')) ?></a> - <span class="separator">|</span> <a href="#" - onclick="tree.expandTree(); return false;"><?= $block->escapeHtml(__('Expand All')) ?></a> + <?php if ($block->getRoot()):?> + <a id="colapseAll" href="#"><?= $block->escapeHtml(__('Collapse All')) ?></a> + <span class="separator">|</span> + <a id="expandAll" href="#"><?= $block->escapeHtml(__('Expand All')) ?></a> <?php endif; ?> </div> - <?php if ($block->getRoot()) :?> + <?php if ($block->getRoot()):?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'tree.collapseTree(); event.preventDefault();', + '#colapseAll' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'tree.expandTree();event.preventDefault();', + '#expandAll' + ) ?> <div class="tree-holder"> <div id="tree-div" class="tree-wrapper"></div> </div> </div> - <div data-id="information-dialog-tree" class="messages" style="display: none;"> + <div data-id="information-dialog-tree" class="messages"> <div class="message message-notice"> <div><?= $block->escapeHtml(__('This operation can take a long time')) ?></div> </div> </div> - <script> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display: none;', + 'div[data-id="information-dialog-tree"]' + ) ?> + <?php $scriptString = <<<script var tree; require([ "jquery", @@ -171,7 +183,7 @@ if (!this.collapsed) { this.collapsed = true; - this.loader.dataUrl = '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl(false))) ?>'; + this.loader.dataUrl = '{$block->escapeJs($block->getLoadTreeUrl(false))}'; this.request(this.loader.dataUrl, false); } }, @@ -180,7 +192,7 @@ this.expandAll(); if (this.collapsed) { this.collapsed = false; - this.loader.dataUrl = '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl(true))) ?>'; + this.loader.dataUrl = '{$block->escapeJs($block->getLoadTreeUrl(true))}'; this.request(this.loader.dataUrl, false); } }, @@ -215,7 +227,9 @@ if (tree && switcherParams) { var url; if (switcherParams.useConfirm) { - if (!confirm("<?= $block->escapeJs(__('Please confirm site switching. All data that hasn\'t been saved will be lost.')) ?>")) { + if (!confirm("{$block->escapeJs(__( + 'Please confirm site switching. All data that hasn\'t been saved will be lost.' +))}")) { return false; } } @@ -258,7 +272,7 @@ } }); } else { - var baseUrl = '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>'; + var baseUrl = '{$block->escapeJs($block->getEditUrl())}'; var urlExt = switcherParams.scopeParams + 'id/' + tree.currentNodeId + '/'; url = parseSidUrl(baseUrl, urlExt); setLocation(url); @@ -295,18 +309,22 @@ if (scopeParams) { url = url + scopeParams; } - <?php if ($block->isClearEdit()) :?> +script; + if ($block->isClearEdit()): + $scriptString .= <<<script if (selectedNode) { url = url + 'id/' + config.parameters.category_id; } - <?php endif;?> +script; +endif; + $scriptString .= <<<script //updateContent(url); //commented since ajax requests replaced with http ones to load a category jQuery('#tree-div').find('.x-tree-node-el').first().remove(); } jQuery(function () { categoryLoader = new Ext.tree.TreeLoader({ - dataUrl: '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl())) ?>' + dataUrl: '{$block->escapeJs($block->getLoadTreeUrl())}' }); categoryLoader.processResponse = function (response, parent, callback) { @@ -388,31 +406,32 @@ enableDD: true, containerScroll: true, selModel: new Ext.tree.CheckNodeMultiSelectionModel(), - rootVisible: '<?= (bool)$block->getRoot()->getIsVisible() ?>', - useAjax: <?= $block->escapeJs($block->getUseAjax()) ?>, - switchTreeUrl: '<?= $block->escapeJs($block->escapeUrl($block->getSwitchTreeUrl())) ?>', - editUrl: '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>', - currentNodeId: <?= (int)$block->getCategoryId() ?>, - baseUrl: '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>' +script; + $scriptString .= ' + rootVisible: \'' . ($block->getRoot()->getIsVisible() ? 'true' : 'false') . '\', + useAjax: ' . $block->escapeJs($block->getUseAjax()) . ', + switchTreeUrl: \'' . $block->escapeJs($block->escapeUrl($block->getSwitchTreeUrl())) .'\', + editUrl: \'' . $block->escapeJs($block->escapeUrl($block->getEditUrl())) .'\', + currentNodeId: ' . (int)$block->getCategoryId() . ', + baseUrl: \'' . $block->escapeJs($block->escapeUrl($block->getEditUrl())) . '\' }; defaultLoadTreeParams = { parameters: { - text: <?= /* @noEscape */ json_encode(htmlentities($block->getRoot()->getName())) ?>, + text: ' . /* @noEscape */ json_encode(htmlentities($block->getRoot()->getName())) . ', draggable: false, - allowDrop: <?php if ($block->getRoot()->getIsVisible()) :?>true<?php else :?>false<?php endif; ?>, - id: <?= (int)$block->getRoot()->getId() ?>, - expanded: <?= (int)$block->getIsWasExpanded() ?>, - store_id: <?= (int)$block->getStore()->getId() ?>, - category_id: <?= (int)$block->getCategoryId() ?>, - parent: <?= (int)$block->getRequest()->getParam('parent') ?> + allowDrop: ' . ($block->getRoot()->getIsVisible() ? 'true' : 'false') . ', + id: ' . (int)$block->getRoot()->getId() . ', + expanded: ' . (int)$block->getIsWasExpanded() . ', + store_id: ' . (int)$block->getStore()->getId() . ', + category_id: ' . (int)$block->getCategoryId() . ', + parent: ' . (int)$block->getRequest()->getParam('parent') . ' }, - data: <?= /* @noEscape */ $block->getTreeJson() ?> - }; - - reRenderTree(); - }); - + data: ' . /* @noEscape */ $block->getTreeJson() . ' + }; + reRenderTree(); + });' . PHP_EOL; + $scriptString .= <<<script function addNew(url, isRoot) { if (isRoot) { tree.currentNodeId = tree.root.id; @@ -485,7 +504,7 @@ click: function () { (function ($) { $.ajax({ - url: '<?= $block->escapeJs($block->escapeUrl($block->getMoveUrl())) ?>', + url: '{$block->escapeJs($block->getMoveUrl())}', method: 'POST', data: registry.get('pd'), showLoader: true @@ -521,5 +540,7 @@ window.addNew = addNew; }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml index e24d676974b01..6c92ddcf36243 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_divId = 'tree' . $block->getId() ?> @@ -10,14 +12,20 @@ <!--[if IE]> <script id="ie-deferred-loader" defer="defer" src="//:"></script> <![endif]--> -<script> -require(['jquery', "prototype", "extjs/ext-tree-checkbox"], function(jQuery){ +<?php +$isUseMassaction = $block->getUseMassaction() ? 1 : 0; +$isAnchorOnly = $block->getIsAnchorOnly() ? 1 : 0; +$intCategoryId = (int)$block->getCategoryId(); +$intRootId = (int) $block->getRoot()->getId(); +$scriptString = <<<script -var tree<?= $block->escapeJs($block->getId()) ?>; +require(['jquery', 'prototype', 'extjs/ext-tree-checkbox'], function(jQuery){ -var useMassaction = <?= $block->getUseMassaction() ? 1 : 0 ?>; +var tree{$block->escapeJs($block->getId())}; -var isAnchorOnly = <?= $block->getIsAnchorOnly() ? 1 : 0 ?>; +var useMassaction = {$isUseMassaction}; + +var isAnchorOnly = {$isAnchorOnly}; Ext.tree.TreePanel.Enhanced = function(el, config) { @@ -41,9 +49,13 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { this.setRootNode(root); if (firstLoad) { - <?php if ($block->getNodeClickListener()) :?> - this.addListener('click', <?= /* @noEscape */ $block->getNodeClickListener() ?>.createDelegate(this)); - <?php endif; ?> + +script; +if ($block->getNodeClickListener()): + $scriptString .= 'this.addListener(\'click\', ' . /* @noEscape */ $block->getNodeClickListener() . + '.createDelegate(this));' . PHP_EOL; +endif; +$scriptString .= <<<script } this.loader.buildCategoryTree(root, data); @@ -55,10 +67,14 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { jQuery(function() { - var emptyNodeAdded = <?= ($block->getWithEmptyNode() ? 'false' : 'true') ?>; + +script; + $scriptString .= 'var emptyNodeAdded = ' . ($block->getWithEmptyNode() ? 'false' : 'true') . ';' . PHP_EOL; + +$scriptString .= <<<script var categoryLoader = new Ext.tree.TreeLoader({ - dataUrl: '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl())) ?>' + dataUrl: '{$block->escapeJs($block->escapeUrl($block->getLoadTreeUrl()))}' }); categoryLoader.buildCategoryTree = function(parent, config) @@ -77,7 +93,7 @@ jQuery(function() // Add empty node to reset category filter if(!emptyNodeAdded) { var empty = Object.clone(_node); - empty.text = '<?= $block->escapeJs(__('None')) ?>'; + empty.text = '{$block->escapeJs(__('None'))}'; empty.children = []; empty.id = 'none'; empty.path = '1/none'; @@ -148,39 +164,41 @@ jQuery(function() }; categoryLoader.on("beforeload", function(treeLoader, node) { - $('<?= $block->escapeJs($_divId) ?>').fire('category:beforeLoad', {treeLoader:treeLoader}); + $('{$block->escapeJs($_divId)}').fire('category:beforeLoad', {treeLoader:treeLoader}); treeLoader.baseParams.id = node.attributes.id; }); - tree<?= $block->escapeJs($block->getId()) ?> = new Ext.tree.TreePanel.Enhanced('<?= $block->escapeJs($_divId) ?>', { + tree{$block->escapeJs($block->getId())} = new Ext.tree.TreePanel.Enhanced('{$block->escapeJs($_divId)}', { animate: false, loader: categoryLoader, enableDD: false, containerScroll: true, rootVisible: false, useAjax: true, - currentNodeId: <?= (int) $block->getCategoryId() ?>, + currentNodeId: {$intCategoryId}, addNodeTo: false }); if (useMassaction) { - tree<?= $block->escapeJs($block->getId()) ?>.on('check', function(node) { - $('<?= $block->escapeJs($_divId) ?>').fire('node:changed', {node:node}); - }, tree<?= $block->escapeJs($block->getId()) ?>); + tree{$block->escapeJs($block->getId())}.on('check', function(node) { + $('{$block->escapeJs($_divId)}').fire('node:changed', {node:node}); + }, tree{$block->escapeJs($block->getId())}); } // set the root node var parameters = { text: 'Psw', draggable: false, - id: <?= (int) $block->getRoot()->getId() ?>, + id: {$intRootId}, expanded: true, - category_id: <?= (int) $block->getCategoryId() ?> + category_id: {$intCategoryId} }; - tree<?= $block->escapeJs($block->getId()) ?>.loadTree({parameters:parameters, data:<?= /* @noEscape */ $block->getTreeJson() ?>},true); + tree{$block->escapeJs($block->getId())}.loadTree({parameters:parameters, data:{$block->getTreeJson()}},true); }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml index cbda491a64740..4e70bff5a4884 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml @@ -5,14 +5,14 @@ */ ?> <?php -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Element */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php /* @var $block \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element */ $element = $block->getElement(); -$note = $element->getNote() ? '<div class="note admin__field-note">' . $block->escapeHtml($element->getNote()) . '</div>' : ''; +$note = $element->getNote() ? + '<div class="note admin__field-note">' . $block->escapeHtml($element->getNote()) . '</div>' : ''; $elementBeforeLabel = $element->getExtType() == 'checkbox' || $element->getExtType() == 'radio'; $addOn = $element->getBeforeElementHtml() || $element->getAfterElementHtml(); $fieldId = ($element->getHtmlId()) ? ' id="attribute-' . $element->getHtmlId() . '-container"' : ''; @@ -26,6 +26,9 @@ $fieldClass .= ($entity && $entity->getIsUserDefined()) ? ' user-defined type-' $fieldAttributes = $fieldId . ' class="' . $block->escapeHtmlAttr($fieldClass) . '" ' . $block->getUiId('form-field', $block->escapeHtmlAttr($element->getId())); + +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <?php $block->checkFieldDisable() ?> @@ -33,20 +36,19 @@ $fieldAttributes = $fieldId . ' class="' . $block->escapeHtmlAttr($fieldClass) . $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'toggleValueElements(this, this.parentNode.parentNode.parentNode)'; ?> -<?php if (!$element->getNoDisplay()) :?> - <?php if ($element->getType() == 'hidden') :?> +<?php if (!$element->getNoDisplay()):?> + <?php if ($element->getType() == 'hidden'):?> <?= $element->getElementHtml() ?> - <?php else :?> + <?php else:?> <div<?= /* @noEscape */ $fieldAttributes ?> data-attribute-code="<?= $element->getHtmlId() ?>" - data-apply-to="<?= $block->escapeHtmlAttr($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( - $element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : [] - ))?>" + data-apply-to="<?= /* @noEscape */ $jsonHelper->jsonEncode($element->hasEntityAttribute() ? + $element->getEntityAttribute()->getApplyTo() : []) ?>" > - <?php if ($elementBeforeLabel) :?> + <?php if ($elementBeforeLabel):?> <?= $block->getElementHtml() ?> <?= $element->getLabelHtml('', $block->getScopeLabel()) ?> <?= /* @noEscape */ $note ?> - <?php else :?> + <?php else:?> <?= $element->getLabelHtml('', $block->getScopeLabel()) ?> <div class="admin__field-control control"> <?= ($addOn) ? '<div class="addon">' . $block->getElementHtml() . '</div>' : $block->getElementHtml() ?> @@ -54,16 +56,20 @@ $fieldAttributes = $fieldId . ' class="' . $block->escapeHtmlAttr($fieldClass) . </div> <?php endif; ?> <div class="field-service"> - <?php if ($block->canDisplayUseDefault()) :?> + <?php if ($block->canDisplayUseDefault()):?> <label for="<?= $element->getHtmlId() ?>_default" class="choice use-default"> - <input <?php if ($element->getReadonly()) :?> disabled="disabled"<?php endif; ?> + <input <?php if ($element->getReadonly()):?> disabled="disabled"<?php endif; ?> type="checkbox" name="use_default[]" class="use-default-control" id="<?= $element->getHtmlId() ?>_default" - <?php if ($block->usedDefault()) :?> checked="checked"<?php endif; ?> - onclick="<?= $block->escapeHtmlAttr($elementToggleCode) ?>" + <?php if ($block->usedDefault()):?> checked="checked"<?php endif; ?> value="<?= $block->escapeHtmlAttr($block->getAttributeCode()) ?>"/> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + $elementToggleCode, + "#" . $element->getHtmlId() . "_default" + ) ?> <span class="use-default-label"><?= $block->escapeHtml(__('Use Default Value')) ?></span> </label> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml index 64384ac391a8d..8dde7013763dc 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml @@ -5,9 +5,14 @@ */ use Magento\Catalog\Helper\Data; -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +/** @var \Magento\Backend\Block\Template $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$scriptString = <<<script require([ "jquery", 'Magento_Ui/js/modal/alert', @@ -207,32 +212,39 @@ function switchDefaultValueField() setRowVisibility('is_unique', false); setRowVisibility('frontend_class', false); break; - - <?php // phpcs:ignore Magento2.Templates.ThisInTemplate ?> - <?php foreach ($this->helper(Data::class)->getAttributeHiddenFields() as $type => $fields): ?> - case '<?= $block->escapeJs($type) ?>': +script; +foreach ($jsonHelper->getAttributeHiddenFields() as $type => $fields): + $scriptString .= <<<script + case '{$block->escapeJs($type)}': var isFrontTabHidden = false; - <?php foreach ($fields as $one): ?> - <?php if ($one == '_front_fieldset'): ?> +script; + foreach ($fields as $one): + if ($one == '_front_fieldset'): + $scriptString .= <<<script getFrontTab().hide(); isFrontTabHidden = true; - <?php elseif ($one == '_default_value'): ?> +script; + elseif ($one == '_default_value'): + $scriptString .= <<<script defaultValueTextVisibility = defaultValueTextareaVisibility = defaultValueDateVisibility = defaultValueYesnoVisibility = false; - <?php elseif ($one == '_scope'): ?> - scopeVisibility = false; - <?php else: ?> - setRowVisibility('<?= $block->escapeJs($one) ?>', false); - <?php endif; ?> - <?php endforeach; ?> - +script; + elseif ($one == '_scope'): + $scriptString .= 'scopeVisibility = false;'; + else: + $scriptString .= "setRowVisibility('" . $block->escapeJs($one) . "', false);"; + endif; + endforeach; + $scriptString .= <<<script if (!isFrontTabHidden){ getFrontTab().show(); } break; - <?php endforeach; ?> +script; + endforeach; + $scriptString .= <<<script default: getFrontTab().show(); @@ -278,7 +290,7 @@ function setRowVisibility(id, isVisible) function updateRequriedOptions() { - if ($F('frontend_input')=='select' && $F('is_required')==1) { + if (\$F('frontend_input')=='select' && \$F('is_required')==1) { $('option-count-check').addClassName('required-options-count'); } else { $('option-count-check').removeClassName('required-options-count'); @@ -364,4 +376,6 @@ window.getFrontTab = getFrontTab; window.toggleApplyVisibility = toggleApplyVisibility; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml index dd1009cc5e033..447a9c4149bfa 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml @@ -5,6 +5,7 @@ */ /** @var $block Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="attribute-set"> @@ -17,7 +18,7 @@ </div> <div class="edit-attribute-set attribute-set-col"> <?= $block->getSetFormHtml() ?> - <script> + <?php $scriptString = <<<script require([ "jquery", "mage/mage" @@ -26,14 +27,17 @@ jQuery('#set-prop-form').mage('validation', {errorClass: 'mage-error'}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> <div class="attribute-set-col fieldset-wrapper"> <div class="fieldset-wrapper-title"> <span class="title"><?= $block->escapeHtml(__('Groups')) ?></span> </div> - <?php if (!$block->getIsReadOnly()) :?> - <?= /* @noEscape */ $block->getAddGroupButton() ?> <?= /* @noEscape */ $block->getDeleteGroupButton() ?> + <?php if (!$block->getIsReadOnly()):?> + <?= /* @noEscape */ $block->getAddGroupButton() ?>  + <?= /* @noEscape */ $block->getDeleteGroupButton() ?> <p class="note-block"><?= $block->escapeHtml(__('Double click on a group to rename it.')) ?></p> <?php endif; ?> @@ -45,8 +49,19 @@ <span class="title"><?= $block->escapeHtml(__('Unassigned Attributes')) ?></span> </div> <div id="tree-div2" class="attribute-set-tree"></div> - <script id="ie-deferred-loader" defer="defer" src="//:"></script> - <script> + <?= /* @noEscape */ $secureRenderer->renderTag( + 'script', + ['id' => "ie-deferred-loader", 'defer' => "defer", 'src' => "//:"], + ' ', + false + ) ?> + <?php $readOnly = ($block->getIsReadOnly() ? 'false' : 'true'); + $groupTree = /* @noEscape */ $block->getGroupTreeJson(); + $attributeTreeJson = /* @noEscape */ $block->getAttributeTreeJson(); + $systemAttributeWarning = $block->escapeJs( + __('This group contains system attributes. Please move system attributes to another group and try again.') + ); + $scriptString = <<<script define("tree-panel", [ "jquery", @@ -57,8 +72,8 @@ ], function(jQuery, prompt, alert){ //<![CDATA[ - var allowDragAndDrop = <?= ($block->getIsReadOnly() ? 'false' : 'true') ?>; - var canEditGroups = <?= ($block->getIsReadOnly() ? 'false' : 'true') ?>; + var allowDragAndDrop = {$readOnly}; + var canEditGroups = {$readOnly}; var TreePanels = function() { // shorthand @@ -85,7 +100,7 @@ }); tree.setRootNode(this.root); - buildCategoryTree(this.root, <?= /* @noEscape */ $block->getGroupTreeJson() ?>); + buildCategoryTree(this.root, {$groupTree}); // render the tree tree.render(); this.root.expand(false, false); @@ -93,7 +108,7 @@ this.ge = new Ext.tree.TreeEditor(tree, { allowBlank:false, - blankText:'<?= $block->escapeJs(__('A name is required.')) ?>', + blankText:'{$block->escapeJs(__('A name is required.'))}', selectOnFocus:true, cls:'folder' }); @@ -124,7 +139,7 @@ id:'free' }); tree2.setRootNode(this.root2); - buildCategoryTree(this.root2, <?= /* @noEscape */ $block->getAttributeTreeJson() ?>); + buildCategoryTree(this.root2, {$attributeTreeJson}); this.root2.addListener('beforeinsert', editSet.rightBeforeInsert); this.root2.addListener('beforeappend', editSet.rightBeforeAppend); @@ -144,12 +159,15 @@ for( i in rootNode.childNodes ) { if(rootNode.childNodes[i].id) { var group = rootNode.childNodes[i]; - editSet.req.groups[gIterator] = new Array(group.id, group.attributes.text.strip(), (gIterator+1)); + editSet.req.groups[gIterator] = new Array(group.id, group.attributes.text.strip(), + (gIterator+1)); var iterator = 0 for( j in group.childNodes ) { iterator ++; if( group.childNodes[j].id > 0 ) { - editSet.req.attributes[group.childNodes[j].id] = new Array(group.childNodes[j].id, group.id, iterator, group.childNodes[j].attributes.entity_id); + editSet.req.attributes[group.childNodes[j].id] = + new Array(group.childNodes[j].id, group.id, iterator, + group.childNodes[j].attributes.entity_id); } } iterator = 0; @@ -164,7 +182,8 @@ for( i in rootNode.childNodes ) { if(rootNode.childNodes[i].id) { if( rootNode.childNodes[i].id > 0 ) { - editSet.req.not_attributes[iterator] = rootNode.childNodes[i].attributes.entity_id; + editSet.req.not_attributes[iterator] = + rootNode.childNodes[i].attributes.entity_id; } iterator ++; } @@ -231,7 +250,7 @@ if( editSet.SystemNodesExists(editSet.currentNode) ) { alert({ - content: '<?= $block->escapeJs(__('This group contains system attributes. Please move system attributes to another group and try again.')) ?>' + content: '{$systemAttributeWarning}' }); return; } @@ -258,7 +277,7 @@ SystemNodesExists : function(currentNode) { if (!currentNode) { alert({ - content: '<?= $block->escapeJs(__('Please select a node.')) ?>' + content: '{$block->escapeJs(__('Please select a node.'))}' }); return; } @@ -279,8 +298,8 @@ addGroup : function() { prompt({ - title: "<?= $block->escapeJs($block->escapeHtml(__('Add New Group'))) ?>", - content: "<?= $block->escapeJs($block->escapeHtml(__('Please enter a new group name.'))) ?>", + title: "{$block->escapeJs($block->escapeHtml(__('Add New Group')))}", + content: "{$block->escapeJs($block->escapeHtml(__('Please enter a new group name.')))}", value: "", validation: true, validationRules: ['required-entry'], @@ -344,8 +363,11 @@ result = false; } for (var i=0; i < TreePanels.root.childNodes.length; i++) { - if (TreePanels.root.childNodes[i].text.toLowerCase() == name.toLowerCase() && TreePanels.root.childNodes[i].id != exceptNodeId) { - errorText = '<?= $block->escapeJs(__('An attribute group named "/name/" already exists.')) ?>'; + if (TreePanels.root.childNodes[i].text.toLowerCase() == name.toLowerCase() && + TreePanels.root.childNodes[i].id != exceptNodeId) { + errorText = '{$block->escapeJs( + __('An attribute group named "/name/" already exists.') + )}'; alert({ content: errorText.replace("/name/",name) }); @@ -373,7 +395,8 @@ editSet.req.form_key = FORM_KEY; } var req = {data : Ext.util.JSON.encode(editSet.req)}; - var con = new Ext.lib.Ajax.request('POST', '<?= $block->escapeJs($block->escapeUrl($block->getMoveUrl())) ?>', {success:editSet.success,failure:editSet.failure}, req); + var con = new Ext.lib.Ajax.request('POST', '{$block->escapeJs($block->getMoveUrl())}', + {success:editSet.success,failure:editSet.failure}, req); }, success : function(o) { @@ -391,7 +414,7 @@ failure : function(o) { alert({ - content: '<?= $block->escapeJs(__('Sorry, we\'re unable to complete this request.')) ?>' + content: '{$block->escapeJs(__('Sorry, we\'re unable to complete this request.'))}' }); }, @@ -408,7 +431,9 @@ rightBeforeAppend : function(tree, nodeThis, node, newParent) { if (node.attributes.is_user_defined == 0) { alert({ - content: '<?= $block->escapeJs(__('You can\'t remove attributes from this attribute set.')) ?>' + content: '{$block->escapeJs( + __('You can\'t remove attributes from this attribute set.') + )}' }); return false; } else { @@ -424,7 +449,9 @@ if (node.attributes.is_unassignable == 0) { alert({ - content: '<?= $block->escapeJs(__('You can\'t remove attributes from this attribute set.')) ?>' + content: '{$block->escapeJs( + __('You can\'t remove attributes from this attribute set.') + )}' }); return false; } else { @@ -448,7 +475,7 @@ rightRemove : function(tree, nodeThis, node) { if( nodeThis.firstChild == null && node.id != 'empty' ) { var newNode = new Ext.tree.TreeNode({ - text : '<?= $block->escapeJs(__('Empty')) ?>', + text : '{$block->escapeJs(__('Empty'))}', id : 'empty', cls : 'folder', is_user_defined : 1, @@ -485,6 +512,8 @@ }); require(["tree-panel"]); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/group.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/group.phtml index 5717e9f0a0f0b..c3d8d5bc0f44e 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/group.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/group.phtml @@ -3,5 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div id="tree-div1" style="height:400px;margin-top:5px;overflow:auto"></div> +<?= /* @noEscape */ $secureRenderer->renderTag('div', ['id' => "tree-div1"], ' ', false) ?> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("height:400px;margin-top:5px;overflow:auto", '#tree-div1') ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml index 227ed4be81fae..8f58d357f83e4 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml @@ -3,12 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= $block->getFormHtml() ?> -<script> + +<?php $scriptString = <<<script require(['jquery', "mage/mage"], function(jQuery){ - jQuery('#<?= $block->escapeJs($block->getFormId()) ?>').mage('form').mage('validation'); + jQuery('#{$block->escapeJs($block->getFormId())}').mage('form').mage('validation'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml index 32466a1dfa965..5ca88689b9e5f 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml @@ -3,31 +3,85 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +$blockId = $block->getId(); ?> -<div id="product_composite_configure" class="product-configure-popup" style="display:none;"> - <iframe name="product_composite_configure_iframe" id="product_composite_configure_iframe" style="width:0; height:0; border:0px solid #fff; position:absolute; top:-1000px; left:-1000px" onload="window.productConfigure && productConfigure.onLoadIFrame()"></iframe> - <form action="" method="post" id="product_composite_configure_form" enctype="multipart/form-data" onsubmit="productConfigure.onConfirmBtn(); return false;" target="product_composite_configure_iframe"> +<div id="product_composite_configure" + class="product-configure-popup product-configure-popup-<?= $block->escapeHtmlAttr($blockId) ?>"> + <iframe name="product_composite_configure_iframe" id="product_composite_configure_iframe"></iframe> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onload', + "window.productConfigure && productConfigure.onLoadIFrame()", + 'iframe[name=\'product_composite_configure_iframe\']:last-of-type' + ) ?> + + <form action="" method="post" id="product_composite_configure_form" enctype="multipart/form-data" + target="product_composite_configure_iframe" class="product_composite_configure_form"> <div class="entry-edit"> - <div id="product_composite_configure_messages" style="display: none;" > + <div id="product_composite_configure_messages" class="product_composite_configure_messages"> <div class="messages"><div class="message message-error error"><div></div></div></div> </div> <div id="product_composite_configure_form_fields" class="content product-composite-configure-inner"></div> - <div id="product_composite_configure_form_additional" style="display:none;"></div> - <div id="product_composite_configure_form_confirmed" style="display:none;"></div> + <div id="product_composite_configure_form_additional" class="product_composite_configure_form_additional"> + </div> + <div id="product_composite_configure_form_confirmed" class="product_composite_configure_form_confirmed"> + </div> </div> <input type="hidden" name="as_js_varname" value="iFrameResponse" /> <input type="hidden" name="form_key" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" /> </form> - <div id="product_composite_configure_confirmed" style="display:none;"></div> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onsubmit', + 'productConfigure.onConfirmBtn();event.preventDefault()', + '.product_composite_configure_form:last-of-type' + ) ?> + + <div id="product_composite_configure_confirmed" class="product_composite_configure_confirmed"></div> + + <?php $scriptString = <<<script + prodCompConfIframe = document.querySelector( + ".product-configure-popup-$blockId iframe[name='product_composite_configure_iframe']" + ); + prodCompConfIframe.style.width = 0; + prodCompConfIframe.style.height = 0; + prodCompConfIframe.style.border = "0px solid #fff"; + prodCompConfIframe.style.position = "absolute"; + prodCompConfIframe.style.top = "-1000px"; + prodCompConfIframe.style.left = "-1000px"; + + prodCompConfMessages = document.querySelector( + ".product-configure-popup-$blockId .product_composite_configure_messages" + ); + prodCompConfMessages.style.display = "none"; + + prodCompConfFormAdd = document.querySelector( + ".product-configure-popup-$blockId .product_composite_configure_form_additional" + ); + prodCompConfFormAdd.style.display = "none"; + + prodCompConfFormConf = document.querySelector( + ".product-configure-popup-$blockId .product_composite_configure_form_confirmed" + ); + prodCompConfFormConf.style.display = "none"; + + prodCompConfConf = document.querySelector( + ".product-configure-popup-$blockId .product_composite_configure_confirmed" + ); + prodCompConfConf.style.display = "none"; + + prodConfPopup = document.querySelector(".product-configure-popup-$blockId"); + prodConfPopup.style.display = "none"; - <script> require([ "jquery", "mage/mage" ], function(jQuery){ - - jQuery('#product_composite_configure_form').mage('form').mage('validation'); - + jQuery('.product_composite_configure_form').each(function () { + jQuery(this).mage('form').mage('validation'); + }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/js.phtml index 722e4ae7ef1f0..f5dfc3ae79fbe 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/js.phtml @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script require([ "prototype", "Magento_Catalog/catalog/product/composite/configure" @@ -89,4 +91,6 @@ var DateOption = Class.create({ productConfigure.opConfig.dateOption = new DateOption(); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml index 68a7a3a69cfd3..fde7a9351756c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ ?> + <?php $_option = $block->getOption(); ?> <?php $_optionId = (int)$_option->getId(); ?> +<?php $optionId = /* @noEscape */ $_optionId ?> <div class="admin__field field<?= $_option->getIsRequire() ? ' required' : '' ?>"> <label class="label admin__field-label"> <?= $block->escapeHtml($_option->getTitle()) ?> @@ -15,28 +19,30 @@ <div class="admin__field-control control"> <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME - || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE) :?> + || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE):?> <?= $block->getDateHtml() ?> - <?php if (!$block->useCalendar()) :?> - <script> + <?php if (!$block->useCalendar()):?> + <?php $scriptString = <<<script require([ "prototype", "Magento_Catalog/catalog/product/composite/configure" ], function(){ window.dateOption = productConfigure.opConfig.dateOption; - Event.observe('options_<?= /* @noEscape */ $_optionId ?>_month', 'change', dateOption.reloadMonth.bind(dateOption)); - Event.observe('options_<?= /* @noEscape */ $_optionId ?>_year', 'change', dateOption.reloadMonth.bind(dateOption)); + Event.observe('options_{$optionId}_month', 'change', dateOption.reloadMonth.bind(dateOption)); + Event.observe('options_{$optionId}_year', 'change', dateOption.reloadMonth.bind(dateOption)); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> <?php endif; ?> <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME - || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME) :?> + || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME):?> <span class="time-picker"><?= $block->getTimeHtml() ?></span> <?php endif; ?> @@ -44,24 +50,28 @@ name="validate_datetime_<?= /* @noEscape */ $_optionId ?>" class="validate-datetime-<?= /* @noEscape */ $_optionId ?>" value="" /> - <script> + <?php $scriptString = <<<script require([ "jquery", "mage/backend/validation" ], function(jQuery){ //<![CDATA[ - <?php if ($_option->getIsRequire()) :?> - jQuery.validator.addMethod('validate-datetime-<?= /* @noEscape */ $_optionId ?>', function(v) { - var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @noEscape */ $_optionId ?>"]'); +script; + if ($_option->getIsRequire()): + $scriptString .= <<<script + jQuery.validator.addMethod('validate-datetime-{$optionId}', function(v) { + var dateTimeParts = jQuery('.datetime-picker[id^="options_{$optionId}"]'); for (var i=0; i < dateTimeParts.length; i++) { if (dateTimeParts[i].value == "") return false; } return true; - }, '<?= $block->escapeJs(__('This is a required option.')) ?>'); - <?php else :?> - jQuery.validator.addMethod('validate-datetime-<?= /* @noEscape */ $_optionId ?>', function(v) { - var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @noEscape */ $_optionId ?>"]'); + }, '{$block->escapeJs(__('This is a required option.'))}'); +script; + else: + $scriptString .= <<<script + jQuery.validator.addMethod('validate-datetime-{$optionId}', function(v) { + var dateTimeParts = jQuery('.datetime-picker[id^="options_{$optionId}"]'); var hasWithValue = false, hasWithNoValue = false; var pattern = /day_part$/i; for (var i=0; i < dateTimeParts.length; i++) { @@ -74,11 +84,15 @@ } } return hasWithValue ^ hasWithNoValue; - }, '<?= $block->escapeJs(__('The field isn\'t complete.')) ?>'); - <?php endif; ?> + }, '{$block->escapeJs(__('The field isn\'t complete.'))}'); +script; + endif; + $scriptString .= <<<script //]]> }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml index 89d005a178fac..a181ed8d67120 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ ?> <?php $_option = $block->getOption(); ?> <?php $_fileInfo = $block->getFileInfo(); ?> <?php $_fileExists = $_fileInfo->hasData() ? true : false; ?> @@ -14,15 +16,20 @@ <?php $_fileNamed = $_fileName . '_name'; ?> <?php $_rand = rand(); ?> -<script> +<?php +$rand = /* @noEscape */ $_rand; +$fileName = /* @noEscape */ $_fileName; +$fieldNameAction = /* @noEscape */ $_fieldNameAction; +$fileNamed = /* @noEscape */ $_fileNamed; +$scriptString = <<<script require(['prototype'], function(){ //<![CDATA[ - opFile<?= /* @noEscape */ $_rand ?> = { + opFile{$rand} = { initializeFile: function(inputBox) { - this.inputFile = inputBox.select('input[name="<?= /* @noEscape */ $_fileName ?>"]')[0]; - this.inputFileAction = inputBox.select('input[name="<?= /* @noEscape */ $_fieldNameAction ?>"]')[0]; - this.fileNameBox = inputBox.up('div').select('.<?= /* @noEscape */ $_fileNamed ?>')[0]; + this.inputFile = inputBox.select('input[name="{$fileName}"]')[0]; + this.inputFileAction = inputBox.select('input[name="{$fieldNameAction}"]')[0]; + this.fileNameBox = inputBox.up('div').select('.{$fileNamed}')[0]; }, toggleFileChange: function(inputBox) { @@ -57,44 +64,68 @@ require(['prototype'], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <div class="admin__field <?= $_option->getIsRequire() ? ' required _required' : '' ?>"> <label class="admin__field-label label"> <?= $block->escapeHtml($_option->getTitle()) ?> <?= /* @noEscape */ $block->getFormattedPrice() ?> </label> - <div class="admin__field-control control"> - <?php if ($_fileExists) :?> + <div class="admin__field-control control" id="<?= /* @noEscape */ $_fileName ?>"> + <?php if ($_fileExists):?> <span class="<?= /* @noEscape */ $_fileNamed ?>"><?= $block->escapeHtml($_fileInfo->getTitle()) ?></span> - <a href="javascript:void(0)" class="label" onclick="opFile<?= /* @noEscape */ $_rand ?>.toggleFileChange($(this).next('.input-box'))"> + <a href="#" class="label"> <?= $block->escapeHtml(__('Change')) ?> </a>  - <?php if (!$_option->getIsRequire()) :?> - <input type="checkbox" onclick="opFile<?= /* @noEscape */ $_rand ?>.toggleFileDelete($(this), $(this).next('.input-box'))" price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>"/> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "event.preventDefault(); opFile" . /* @noEscape */ $_rand . + ".toggleFileChange($(this).next('.input-box'))", + '#' . /* @noEscape */ $_fileName . ' a' + ); ?> + <?php if (!$_option->getIsRequire()):?> + <input type="checkbox" + price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>"/> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "opFile" . /* @noEscape */ $_rand . ".toggleFileDelete($(this), $(this).next('.input-box'))", + '#' . /* @noEscape */ $_fileName . ' input[type="checkbox"]' + ) ?> <span class="label"><?= $block->escapeHtml(__('Delete')) ?></span> <?php endif; ?> <?php endif; ?> <div class="input-box" <?= $_fileExists ? 'style="display:none"' : '' ?>> <!-- ToDo UI: add appropriate file class when z-index issue in ui dialog will be resolved --> - <input type="file" name="<?= /* @noEscape */ $_fileName ?>" class="product-custom-option<?= $_option->getIsRequire() ? ' required-entry' : '' ?>" price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>" <?= $_fileExists ? 'disabled="disabled"' : '' ?>/> - <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" value="<?= /* @noEscape */ $_fieldValueAction ?>" /> + <input type="file" name="<?= /* @noEscape */ $_fileName ?>" + class="product-custom-option<?= $_option->getIsRequire() ? ' required-entry' : '' ?>" + price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>" + <?= $_fileExists ? 'disabled="disabled"' : '' ?>/> + <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" + value="<?= /* @noEscape */ $_fieldValueAction ?>" /> - <?php if ($_option->getFileExtension()) :?> + <?php if ($_option->getFileExtension()):?> <div class="admin__field-note"> - <span><?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong></span> + <span><?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: + <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong> + </span> </div> <?php endif; ?> - <?php if ($_option->getImageSizeX() > 0) :?> + <?php if ($_option->getImageSizeX() > 0):?> <div class="admin__field-note"> - <span><?= $block->escapeHtml(__('Maximum image width')) ?>: <strong><?= (int)$_option->getImageSizeX() ?> <?= $block->escapeHtml(__('px.')) ?></strong></span> + <span><?= $block->escapeHtml(__('Maximum image width')) ?>: + <strong><?= (int)$_option->getImageSizeX() ?> <?= $block->escapeHtml(__('px.')) ?></strong> + </span> </div> <?php endif; ?> - <?php if ($_option->getImageSizeY() > 0) :?> + <?php if ($_option->getImageSizeY() > 0):?> <div class="admin__field-note"> - <span><?= $block->escapeHtml(__('Maximum image height')) ?>: <strong><?= (int)$_option->getImageSizeY() ?> <?= $block->escapeHtml(__('px.')) ?></strong></span> + <span><?= $block->escapeHtml(__('Maximum image height')) ?>: + <strong><?= (int)$_option->getImageSizeY() ?> <?= $block->escapeHtml(__('px.')) ?></strong> + </span> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml index 66df098a194ae..eaaee91d7226c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml @@ -3,12 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<?php -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** * @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="admin__scope-old"> @@ -34,30 +32,40 @@ title="<?= $block->escapeHtmlAttr(__('Product online status')) ?>"></label> </div> - <?php if ($block->getProductId()) :?> + <?php if ($block->getProductId()):?> <?= $block->getDeleteButtonHtml() ?> <?php endif; ?> - <?php if ($block->getProductSetId()) :?> + <?php if ($block->getProductSetId()):?> <?= $block->getChangeAttributeSetButtonHtml() ?> <?= $block->getSaveSplitButtonHtml() ?> <?php endif; ?> <?= $block->getBackButtonHtml() ?> </div> </div> -<?php if ($block->getUseContainer()) :?> +<?php if ($block->getUseContainer()): ?> <form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" enctype="multipart/form-data" data-form="edit-product" data-product-id="<?= $block->escapeHtmlAttr($block->getProduct()->getId()) ?>"> <?php endif; ?> <?= $block->getBlockHtml('formkey') ?> - <div data-role="tabs" id="product-edit-form-tabs"></div> <?php /* @TODO: remove id after elimination of setDestElementId('product-edit-form-tabs') */?> + <div data-role="tabs" id="product-edit-form-tabs"></div> + <?php /* @TODO: remove id after elimination of setDestElementId('product-edit-form-tabs') */?> <?= $block->getChildHtml('product-type-tabs') ?> - <input type="hidden" id="product_type_id" value="<?= $block->escapeHtmlAttr($block->getProduct()->getTypeId()) ?>"/> - <input type="hidden" id="attribute_set_id" value="<?= $block->escapeHtmlAttr($block->getProduct()->getAttributeSetId()) ?>"/> + <input type="hidden" id="product_type_id" + value="<?= $block->escapeHtmlAttr($block->getProduct()->getTypeId()) ?>"/> + <input type="hidden" id="attribute_set_id" + value="<?= $block->escapeHtmlAttr($block->getProduct()->getAttributeSetId()) ?>"/> <button type="submit" class="hidden"></button> -<?php if ($block->getUseContainer()) :?> +<?php if ($block->getUseContainer()):?> </form> <?php endif; ?> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$jsonFieldsAutogenerationMasks = /* @noEscape */ $jsonHelper->jsonEncode($block->getFieldsAutogenerationMasks()); +$jsonAttributesAllowedForAutogeneration = /* @noEscape */ $jsonHelper->jsonEncode( + $block->getAttributesAllowedForAutogeneration() +); +$scriptString = <<<scriptStr require([ "jquery", "Magento_Catalog/catalog/type-events", @@ -66,8 +74,8 @@ require([ "mage/backend/tabs", "domReady!" ], function($, TypeSwitcher){ - var $form = $('[data-form=edit-product]'); - $form.data('typeSwitcher', TypeSwitcher.init()); + var \$form = $('[data-form=edit-product]'); + \$form.data('typeSwitcher', TypeSwitcher.init()); var scriptTagManager = (function($) { var hiddenPrefix = 'hidden', @@ -109,7 +117,7 @@ require([ $(this).val($(this).val().substr(0, maxLength)); } }); - $form.mage('form', { + \$form.mage('form', { handlersData: { save: {}, saveAndContinueEdit: { @@ -129,10 +137,10 @@ require([ } } }); - $form.mage('validation', {validationUrl: '<?= $block->escapeJs($block->escapeUrl($block->getValidationUrl())) ?>'}); + \$form.mage('validation', {validationUrl: '{$block->escapeJs($block->getValidationUrl())}'}); - var masks = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getFieldsAutogenerationMasks()) ?>; - var availablePlaceholders = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getAttributesAllowedForAutogeneration()) ?>; + var masks = {$jsonFieldsAutogenerationMasks}; + var availablePlaceholders = {$jsonAttributesAllowedForAutogeneration}; var Autogenerator = function(masks) { this._masks = masks || {}; this._fieldReverseIndex = this._buildReverseIndex(this._masks); @@ -155,13 +163,13 @@ require([ 'change init', elementSelector, $.proxy(function(event) { - var $element = $(event.target); - if (event.type == 'init' && $element.data('disablerInited')) { + var \$element = $(event.target); + if (event.type == 'init' && \$element.data('disablerInited')) { return; } else { - $element.data('disablerInited', true); + \$element.data('disablerInited', true); } - $element.data(this.data.disabled, $element.val().replace(/\s/g, '') != ''); + \$element.data(this.data.disabled, \$element.val().replace(/\s/g, '') != ''); }, this) ).find(elementSelector).trigger('init'); }; @@ -169,19 +177,19 @@ require([ $("#product_info_tabs").on("tabscreate tabsactivate", $.proxy(disabler, this)); $.each(this._masks, function(field, mask) { - var $field = $('#' + field); - if (!$field.val() && mask && mask.length > 0 && !self.varRegexp.test(mask)) { - $field.val(mask); + var \$field = $('#' + field); + if (!\$field.val() && mask && mask.length > 0 && !self.varRegexp.test(mask)) { + \$field.val(mask); } - $field.trigger('change'); + \$field.trigger('change'); }); $.each(self._fieldReverseIndex, function(field) { - var fields = this, $field = $('#' + field); + var fields = this, \$field = $('#' + field); var filler = function(onlyText) { $.each(fields, function() { - var $el = $('#' + this); - if ($el.data(self.data.disabled)) { + var \$el = $('#' + this); + if (\$el.data(self.data.disabled)) { return; } if (onlyText === true && self.varRegexp.test(self._masks[this])) { @@ -190,12 +198,12 @@ require([ var value = self._masks[this].replace(self.varsRegexp, function(maskfieldName) { return $('#' + maskfieldName.slice(2, -2)).val(); }); - $el.val(value); + \$el.val(value); }); }; - if ($field.length) { + if (\$field.length) { self.form.on('keyup change blur click paste', '#' + field, filler); - $field.trigger('change'); + \$field.trigger('change'); } }); }, @@ -217,7 +225,7 @@ require([ } }); - $form.data('autogenerator', new Autogenerator(masks).bindAll()); + \$form.data('autogenerator', new Autogenerator(masks).bindAll()); $('.widget-button-save .item-default').parent().hide(); @@ -229,7 +237,7 @@ require([ $('#status').val($(this).prop('checked') ? '1' : '2'); }); - $form.on('changeAttributeSet', function(event, data) { + \$form.on('changeAttributeSet', function(event, data) { if (data.label) { $('#product-template-suggest-container .action-toggle>span').text(data.label); $('[data-role=affected-attribute-set-selector] [data-role=name-container]').text(data.label); @@ -240,13 +248,13 @@ require([ uri += /\?/.test(uri) ? '&' : '?'; uri += 'set=' + window.encodeURIComponent(data.id); - var $form = $('[data-form=edit-product]'); - $form.attr('action', $form.attr('action').replace(/(\/|&|\?)?\bset(\/|=)\d+/g, '')); - $form.find('#attribute_set_id').attr('name', 'set').val(data.id); + var \$form = $('[data-form=edit-product]'); + \$form.attr('action', \$form.attr('action').replace(/(\/|&|\?)?\bset(\/|=)\d+/g, '')); + \$form.find('#attribute_set_id').attr('name', 'set').val(data.id); $.ajax({ url: uri.replace('/edit/', '/new/') + '&popup=1', type: 'post', - data: $form.serializeArray(), + data: \$form.serializeArray(), dataType: 'html', context: $('body'), showLoader: true @@ -255,67 +263,68 @@ require([ data = scriptTagManager.disableScripts(data); var removedElementClass = 'removed'; - var $page = $('body'); - var $newPage = $(data); + var \$page = $('body'); + var \$newPage = $(data); var nameMapper = function() { return $(this).attr('name'); }; var activeTabId = $('.ui-tabs-active>a').attr('id'); //add new tab tabs or reorder - $page.find('#product_info_tabs .tabs').each(function(i, tabContainer) { - $newPage.find('#product_info_tabs .tabs').each(function(j, newTabContainer) { + \$page.find('#product_info_tabs .tabs').each(function(i, tabContainer) { + \$newPage.find('#product_info_tabs .tabs').each(function(j, newTabContainer) { if (i != j) { return; } - var $tabContainer = $(tabContainer); + var \$tabContainer = $(tabContainer); $(tabContainer).find('li').removeClass(removedElementClass); - var $tabs = $(tabContainer) + var \$tabs = $(tabContainer) .find('li:not(.' + removedElementClass + ') .tab-item-link.user-defined:not(.ajax)'); - var $newTabs = $(newTabContainer).find('.tab-item-link.user-defined:not(.ajax)'), - tabsNames = $tabs.map(nameMapper).toArray(); + var \$newTabs = $(newTabContainer).find('.tab-item-link.user-defined:not(.ajax)'), + tabsNames = \$tabs.map(nameMapper).toArray(); //hide not exists elements $.each( - _.difference(tabsNames, $newTabs.map(nameMapper).toArray()), + _.difference(tabsNames, \$newTabs.map(nameMapper).toArray()), function(index, tabName) { - $tabContainer.find('[name=' + tabName + ']').closest('li') + \$tabContainer.find('[name=' + tabName + ']').closest('li') .addClass(removedElementClass); - $page.find('#' + tabName) + \$page.find('#' + tabName) .addClass(removedElementClass) .addClass('ignore-validate'); } ); $(newTabContainer).find('.tab-item-link.user-defined:not(.ajax)').each(function(index, tab) { - var $tab = $(tab), - tabName = nameMapper.apply($tab), - $tabsContent = $tab.closest('li').clone(); - $tabsContent.find('.fieldset>.field').remove(); - if (nameMapper.apply($tabs.eq(index)) == tabName) { + var \$tab = $(tab), + tabName = nameMapper.apply(\$tab), + \$tabsContent = \$tab.closest('li').clone(); + \$tabsContent.find('.fieldset>.field').remove(); + if (nameMapper.apply(\$tabs.eq(index)) == tabName) { return true; } - var $tabToMove = $.inArray(tabName, tabsNames) !== -1 - ? $tabs.filter(function() { + var \$tabToMove = $.inArray(tabName, tabsNames) !== -1 + ? \$tabs.filter(function() { return nameMapper.apply(this) === tabName; }).closest('li') - : $tabsContent; + : \$tabsContent; if (index === 0) { - $tabToMove.prependTo($tabContainer); + \$tabToMove.prependTo(\$tabContainer); } else { - $tabToMove.insertAfter($tabs.eq(index - 1).closest('li')); + \$tabToMove.insertAfter(\$tabs.eq(index - 1).closest('li')); } - $tabToMove.removeClass(removedElementClass).removeClass('ignore-validate'); - $tabs = $tabContainer.find('li:not(.' + removedElementClass + ') .tab-item-link.user-defined:not(.ajax)'); + \$tabToMove.removeClass(removedElementClass).removeClass('ignore-validate'); + \$tabs = \$tabContainer + .find('li:not(.' + removedElementClass + ') .tab-item-link.user-defined:not(.ajax)'); }); }); }); //add new fieldsets or reorder - $newPage.find('#product_info_tabs .fieldset.user-defined').each(function(index, newFieldset) { + \$newPage.find('#product_info_tabs .fieldset.user-defined').each(function(index, newFieldset) { var fieldsetContainer, newFieldsetContainer, sourceContainer, destinationContainer; newFieldsetContainer = $(newFieldset).parents('[data-ui-id*=-tab-content-]').first(); - if ($page.find('[data-ui-id=' + newFieldsetContainer.data('uiId') + ']').length === 0) { + if (\$page.find('[data-ui-id=' + newFieldsetContainer.data('uiId') + ']').length === 0) { fieldsetContainer = newFieldsetContainer .clone() .removeClass(removedElementClass) @@ -323,10 +332,10 @@ require([ //Enable hidden js scripts in node. These scripts will be performed after inserting into page fieldsetContainer = scriptTagManager.enableScripts(fieldsetContainer); } else { - fieldsetContainer = $page.find('[data-ui-id=' + newFieldsetContainer.data('uiId') + ']').first(); + fieldsetContainer = \$page.find('[data-ui-id=' + newFieldsetContainer.data('uiId') + ']').first(); } sourceContainer = newFieldsetContainer.parents('[data-ui-id*=-tab-content-]').first(); - destinationContainer = $page.find('[data-ui-id=' + sourceContainer.data('uiId') + ']').first(); + destinationContainer = \$page.find('[data-ui-id=' + sourceContainer.data('uiId') + ']').first(); fieldsetContainer.appendTo(destinationContainer); }); @@ -334,7 +343,7 @@ require([ return $(this).data('attributeCode'); }; //add new element elements or reorder - $page.find('[data-form=edit-product] [data-role=tabs] .fieldset, #product_info_tabs .fieldset') + \$page.find('[data-form=edit-product] [data-role=tabs] .fieldset, #product_info_tabs .fieldset') .removeClass('ignore-validate') .removeClass(removedElementClass) .each(function(i, fieldSet) { @@ -342,49 +351,49 @@ require([ if ($(fieldSet).attr('id') != $(newFieldSet).attr('id')) { return } - var $elements = $(fieldSet).find('>.field:not(.' + removedElementClass + ')'); - var $newFieldSet = $(newFieldSet); - var $newElements = $newFieldSet.find('>.field'); + var \$elements = $(fieldSet).find('>.field:not(.' + removedElementClass + ')'); + var \$newFieldSet = $(newFieldSet); + var \$newElements = \$newFieldSet.find('>.field'); - $elements.removeClass(removedElementClass); + \$elements.removeClass(removedElementClass); - var elementNames = $elements.map(nameDataMapper).toArray(); + var elementNames = \$elements.map(nameDataMapper).toArray(); //hide not exists elements $.each( - _.difference(elementNames, $newElements.map(nameDataMapper).toArray()), + _.difference(elementNames, \$newElements.map(nameDataMapper).toArray()), function(index, elementId) { - $page.find('#attribute-' + elementId + '-container') + \$page.find('#attribute-' + elementId + '-container') .addClass(removedElementClass) .addClass('ignore-validate'); } ); - $newElements.each(function(index, element) { - var $element = $(element), - elementId = nameDataMapper.apply($element); - if (nameDataMapper.apply($elements.get(index)) == elementId) { + \$newElements.each(function(index, element) { + var \$element = $(element), + elementId = nameDataMapper.apply(\$element); + if (nameDataMapper.apply(\$elements.get(index)) == elementId) { return true; } - var $elementToMove = $('.fieldset>.field[data-attribute-code="' + elementId + '"]'); - if ($elementToMove.length === 0) { - $elementToMove = $element.clone(); + var \$elementToMove = $('.fieldset>.field[data-attribute-code="' + elementId + '"]'); + if (\$elementToMove.length === 0) { + \$elementToMove = \$element.clone(); } if (index === 0) { - $elementToMove.prependTo(fieldSet); + \$elementToMove.prependTo(fieldSet); } else { - $elementToMove.insertAfter($elements.get(index - 1)) + \$elementToMove.insertAfter(\$elements.get(index - 1)) } - $elementToMove.trigger('contentUpdated'); - $elementToMove.removeClass(removedElementClass).removeClass('.ignore-validate'); - $elements = $(fieldSet).find('>.field:not(.' + removedElementClass + ')'); + \$elementToMove.trigger('contentUpdated'); + \$elementToMove.removeClass(removedElementClass).removeClass('.ignore-validate'); + \$elements = $(fieldSet).find('>.field:not(.' + removedElementClass + ')'); }); }; - $newPage.find('#product_info_tabs .fieldset').each(updateFieldsetElements); + \$newPage.find('#product_info_tabs .fieldset').each(updateFieldsetElements); fieldsetContainer = $(fieldSet).parents('[data-ui-id*=-tab-content-]').first(); - var newFieldsetContainer = $newPage.find('[data-ui-id=' + $(fieldsetContainer).data('uiId') + ']'); + var newFieldsetContainer = \$newPage.find('[data-ui-id=' + $(fieldsetContainer).data('uiId') + ']'); if (newFieldsetContainer.length == 0) { $(fieldsetContainer).find('fieldset .field') .addClass('ignore-validate') @@ -405,7 +414,9 @@ require([ }); }); -</script> +scriptStr; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <script type="text/x-magento-init"> { "*": { diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml index 1d22624751b32..344123cbe5640 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml @@ -5,8 +5,10 @@ */ /** @var Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Inventory $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require(['jquery'], function($){ $('[data-role=toggle-editability-all]').change(function(e) { var toggler = $(this); @@ -27,7 +29,9 @@ someEditable.prop('disabled', useConfigSettings.prop('checked')); }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php $defaultMinSaleQty = $block->getDefaultConfigValue('min_sale_qty'); @@ -243,9 +247,10 @@ if (!is_numeric($defaultMinSaleQty)) { class="select" disabled="disabled"> <?php foreach ($block->getBackordersOption() as $option):?> - <?php $_selected = ($option['value'] == $block->getDefaultConfigValue('backorders')) - ? ' selected="selected"' : '' ?> - <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" + <?php $_selected = ($option['value'] == $block->getDefaultConfigValue('backorders')) ? + ' selected="selected"' : '' ?> + <option + value="<?= $block->escapeHtmlAttr($option['value']) ?>" <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?> </option> <?php endforeach; ?> @@ -397,8 +402,9 @@ if (!is_numeric($defaultMinSaleQty)) { name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[is_in_stock]" class="select" disabled="disabled"> <option value="1"><?= $block->escapeHtml(__('In Stock')) ?></option> - <option value="0"<?php if ($block->getDefaultConfigValue('is_in_stock') == 0):?> - selected<?php endif; ?>><?= $block->escapeHtml(__('Out of Stock')) ?> + <option value="0" + <?php if ($block->getDefaultConfigValue('is_in_stock') == 0):?> selected<?php endif; ?>> + <?= $block->escapeHtml(__('Out of Stock')) ?> </option> </select> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml index 98b06050e0d1d..d5859240875cd 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml @@ -5,6 +5,7 @@ */ /** @var $block Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\Websites */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="fieldset-wrapper" id="add-products-to-website-wrapper"> @@ -15,24 +16,27 @@ <br> <div class="store-scope"> <div class="store-tree" id="add-products-to-website-content"> - <?php foreach ($block->getWebsiteCollection() as $_website) :?> + <?php foreach ($block->getWebsiteCollection() as $_website):?> <div class="website-name"> <input name="add_website_ids[]" value="<?= $block->escapeHtmlAttr($_website->getId()) ?>" - <?php if ($block->getWebsitesReadonly()) :?> + <?php if ($block->getWebsitesReadonly()):?> disabled="disabled" <?php endif;?> class="checkbox website-checkbox" id="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>" type="checkbox" /> - <label for="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"><?= $block->escapeHtml($_website->getName()) ?></label> + <label for="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"> + <?= $block->escapeHtml($_website->getName()) ?> + </label> </div> - <dl class="webiste-groups" id="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>_data"> - <?php foreach ($block->getGroupCollection($_website) as $_group) :?> + <dl class="webiste-groups" + id="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>_data"> + <?php foreach ($block->getGroupCollection($_website) as $_group):?> <dt><?= $block->escapeHtml($_group->getName()) ?></dt> <dd class="group-stores"> <ul> - <?php foreach ($block->getStoreCollection($_group) as $_store) :?> + <?php foreach ($block->getStoreCollection($_group) as $_store):?> <li> <?= $block->escapeHtml($_store->getName()) ?> </li> @@ -55,30 +59,35 @@ <br> <div class="messages"> <div class="message message-notice"> - <div><?= $block->escapeHtml(__('To hide an item in catalog or search results, set the status to "Disabled".')) ?></div> + <div><?= $block->escapeHtml( + __('To hide an item in catalog or search results, set the status to "Disabled".') + ) ?> + </div> </div> </div> <div class="store-scope"> <div class="store-tree" id="remove-products-to-website-content"> - <?php foreach ($block->getWebsiteCollection() as $_website) :?> + <?php foreach ($block->getWebsiteCollection() as $_website):?> <div class="website-name"> <input name="remove_website_ids[]" value="<?= $block->escapeHtmlAttr($_website->getId()) ?>" - <?php if ($block->getWebsitesReadonly()) :?> + <?php if ($block->getWebsitesReadonly()):?> disabled="disabled" <?php endif;?> class="checkbox website-checkbox" id="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>" type="checkbox" /> - <label for="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"><?= $block->escapeHtml($_website->getName()) ?></label> + <label for="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"> + <?= $block->escapeHtml($_website->getName()) ?> + </label> </div> <dl class="webiste-groups" id="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>_data"> - <?php foreach ($block->getGroupCollection($_website) as $_group) :?> + <?php foreach ($block->getGroupCollection($_website) as $_group):?> <dt><?= $block->escapeHtml($_group->getName()) ?></dt> <dd class="group-stores"> <ul> - <?php foreach ($block->getStoreCollection($_group) as $_store) :?> + <?php foreach ($block->getStoreCollection($_group) as $_store):?> <li> <?= $block->escapeHtml($_store->getName()) ?> </li> @@ -93,7 +102,7 @@ </fieldset> </div> -<script> +<?php $scriptString = <<<script require([ 'prototype' ], function () { @@ -120,4 +129,6 @@ require([ } } }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml index d073053e2f854..261de795f7199 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml @@ -7,6 +7,7 @@ // phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\AttributeSet */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <script id="product-template-selector-template" type="text/x-magento-template"> <% if (!data.term && data.items.length && !data.allShown()) { %> @@ -23,16 +24,19 @@ </button> <% } %> </script> -<script> +<?php /** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$selectorOptions = /* @noEscape */ $jsonHelper->jsonEncode($block->getSelectorOptions()); +$scriptString = <<<script require(["jquery","mage/mage","mage/backend/suggest"],function ($) { - var $suggest = $('#product-template-suggest'); - $suggest.closest('.dropdown-menu').siblings('[data-toggle=dropdown]').on('click.toggleDropdown', function () { + var \$suggest = $('#product-template-suggest'); + \$suggest.closest('.dropdown-menu').siblings('[data-toggle=dropdown]').on('click.toggleDropdown', function () { if ($(this).hasClass('active')) { - $suggest.click(); + \$suggest.click(); } }); - $suggest - .mage('suggest',<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSelectorOptions()) ?>) + \$suggest + .mage('suggest', {$selectorOptions}) .on('suggestselect', function (e, ui) { if (ui.item.id) { $('[data-form=edit-product]').trigger('changeAttributeSet', ui.item); @@ -40,4 +44,6 @@ } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml index f12a99e6c7843..22dd5de45a073 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml @@ -4,8 +4,13 @@ * See COPYING.txt for license details. */ /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\NewCategory */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" style="display:none"> +<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>"> <?= $block->getFormHtml() ?> <?= $block->getAfterElementHtml() ?> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + $block->escapeJs($block->getNameInLayout()) +) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml index ad38d250a3345..7129190d47fb5 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options */ ?> <div class="fieldset-wrapper" id="product-custom-options-wrapper" data-block="product-custom-options"> <div class="fieldset-wrapper-title"> @@ -12,14 +14,19 @@ <span><?= $block->escapeHtml(__('Custom Options')) ?></span> </strong> </div> - <div class="fieldset-wrapper-content" id="product-custom-options-content" data-role="product-custom-options-content"> + <div class="fieldset-wrapper-content" id="product-custom-options-content" + data-role="product-custom-options-content"> <fieldset class="fieldset"> <div class="messages"> - <div class="message message-error" id="dynamic-price-warning" style="display: none;"> + <div class="message message-error" id="dynamic-price-warning"> <div class="message-inner"> - <div class="message-content"><?= $block->escapeHtml(__('We can\'t save custom-defined options for bundles with dynamic pricing.')) ?></div> + <div class="message-content"> + <?= $block->escapeHtml(__( + 'We can\'t save custom-defined options for bundles with dynamic pricing.' + )) ?></div> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", '#dynamic-price-warning') ?> </div> <div id="product_options_container" class="sortable-wrapper"> @@ -35,7 +42,7 @@ </div> </div> -<script> +<?php $scriptString = <<<script require(['jquery'], function($){ var priceType = $('#price_type'); var priceWarning = $('#dynamic-price-warning'); @@ -43,4 +50,6 @@ require(['jquery'], function($){ priceWarning.show(); } }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml index 713366e73aba5..ce7dac70010b1 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml @@ -5,8 +5,10 @@ */ // phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Option */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Option */ ?> <?= $block->getTemplatesHtml() ?> <script id="custom-option-base-template" type="text/x-magento-template"> <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="option_<%- data.id %>"> @@ -68,7 +70,8 @@ type="text" value="<%- data.title %>" data-store-label="<%- data.title %>" - <% if (typeof data.scopeTitleDisabled != 'undefined' && data.scopeTitleDisabled != null) { %> disabled="disabled" <% } %> + <% if (typeof data.scopeTitleDisabled != 'undefined' && + data.scopeTitleDisabled != null) { %> disabled="disabled" <% } %> > <%- data.checkboxScopeTitle %> </div> @@ -92,20 +95,43 @@ <label for="field-option-req"> <?= $block->escapeHtml(__('Required')) ?> </label> - <span style="display:none"><?= $block->getRequireSelectHtml() ?></span> + <span id="span_<?= /* @noEscape */ $block->getFieldId() ?>"> + <?= $block->getRequireSelectHtml() ?> + </span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + "span_#" . /* @noEscape */ $block->getFieldId() + ) ?> </fieldset> </fieldset> </div> </div> </script> -<div id="import-container" style="display: none;"></div> -<?php if (!$block->isReadonly()) :?> +<div id="import-container"></div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#import-container') ?> +<?php if (!$block->isReadonly()):?> <div><input type="hidden" name="affect_product_custom_options" value="1"/></div> <?php endif; ?> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); + +$customOptions = /* @noEscape */ $jsonHelper->jsonEncode( + [ + 'fieldId' => $block->getFieldId(), + 'productGridUrl' => $block->escapeJs($block->getProductGridUrl()), + 'formKey' => $block->getFormKey(), + 'customOptionsUrl' => $block->escapeJs($block->getCustomOptionsUrl()), + 'isReadonly' => (bool) $block->isReadonly(), + 'itemCount' => (int) $block->getItemCount(), + 'currentProductId' => (int) $block->getCurrentProductId(), + ] +); + +$scriptString = <<<script require([ "jquery", "Magento_Catalog/js/custom-options" @@ -113,23 +139,17 @@ require([ jQuery(function ($) { var fieldSet = $('[data-block=product-custom-options]'); - fieldSet.customOptions(<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( - [ - 'fieldId' => $block->getFieldId(), - 'productGridUrl' => $block->escapeUrl($block->getProductGridUrl()), - 'formKey' => $block->getFormKey(), - 'customOptionsUrl' => $block->escapeUrl($block->getCustomOptionsUrl()), - 'isReadonly' => (bool) $block->isReadonly(), - 'itemCount' => (int) $block->getItemCount(), - 'currentProductId' => (int) $block->getCurrentProductId(), - ] - )?>); + fieldSet.customOptions({$customOptions}); //adding data to templates - <?php /** @var $_value \Magento\Framework\DataObject */ ?> - <?php foreach ($block->getOptionValues() as $_value) :?> - fieldSet.customOptions('addOption', <?= /* @noEscape */ $_value->toJson() ?>); - <?php endforeach; ?> +script; +/** @var $_value \Magento\Framework\DataObject */ +foreach ($block->getOptionValues() as $_value): + $scriptString .= " fieldSet.customOptions('addOption', " . /* @noEscape */ $_value->toJson() . ');' . PHP_EOL; +endforeach; +$scriptString .= <<<script }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml index e66a18c677cc3..7e1c48d535dc1 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml @@ -7,6 +7,7 @@ // phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Price\Tier */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $element = $block->getElement(); ?> <?php $_htmlId = $block->getElement()->getHtmlId() ?> @@ -20,33 +21,42 @@ $element = $block->getElement(); <?php $_showWebsite = $block->isShowWebsiteColumn(); ?> <?php $_showWebsite = $block->isMultiWebsites(); ?> -<div class="field" id="attribute-<?= /* @noEscape */ $_htmlId ?>-container" data-attribute-code="<?= /* @noEscape */ $_htmlId ?>" +<?php /** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> +<div class="field" id="attribute-<?= /* @noEscape */ $_htmlId ?>-container" + data-attribute-code="<?= /* @noEscape */ $_htmlId ?>" data-apply-to="<?= $block->escapeHtml( - $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( - $element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : [] - ) + $jsonHelper->jsonEncode($element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : []) )?>"> <label class="label"><span><?= $block->escapeHtml($block->getElement()->getLabel()) ?></span></label> <div class="control"> <table class="admin__control-table tiers_table" id="tiers_table"> <thead> <tr> - <th class="col-websites" <?php if (!$_showWebsite) :?>style="display:none"<?php endif; ?>><?= $block->escapeHtml(__('Web Site')) ?></th> + <th class="col-websites"><?= $block->escapeHtml(__('Web Site')) ?></th> <th class="col-customer-group"><?= $block->escapeHtml(__('Customer Group')) ?></th> <th class="col-qty required"><?= $block->escapeHtml(__('Quantity')) ?></th> - <th class="col-price required"><?= $block->escapeHtml($block->getPriceColumnHeader(__('Item Price'))) ?></th> + <th class="col-price required"> + <?= $block->escapeHtml($block->getPriceColumnHeader(__('Item Price'))) ?> + </th> <th class="col-delete"><?= $block->escapeHtml(__('Action')) ?></th> </tr> + <?php if (!$_showWebsite): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', 'th.col-websites'); ?> + <?php endif; ?> </thead> <tbody id="<?= /* @noEscape */ $_htmlId ?>_container"></tbody> <tfoot> <tr> - <td colspan="<?php if (!$_showWebsite) :?>4<?php else :?>5<?php endif; ?>" class="col-actions-add"><?= $block->getAddButtonHtml() ?></td> + <td colspan="<?php if (!$_showWebsite):?>4<?php else:?>5<?php endif; ?>" + class="col-actions-add"><?= $block->getAddButtonHtml() ?> + </td> </tr> </tfoot> </table> -<script> +<?php $htmlName = /* @noEscape */ $_htmlName; +$scriptString = <<<script require([ 'mage/template', "prototype", @@ -55,39 +65,81 @@ require([ //<![CDATA[ var tierPriceRowTemplate = '<tr>' - + '<td class="col-websites"<?php if (!$_showWebsite) :?> style="display:none"<?php endif; ?>>' - + '<select class="<?= $block->escapeHtmlAttr($_htmlClass) ?> required-entry" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][website_id]" id="tier_price_row_<%- data.index %>_website">' - <?php foreach ($block->getWebsites() as $_websiteId => $_info) :?> - + '<option value="<?= $block->escapeHtmlAttr($_websiteId) ?>"><?= $block->escapeHtml($_info['name']) ?><?php if (!empty($_info['currency'])) :?> [<?= $block->escapeHtml($_info['currency']) ?>]<?php endif; ?></option>' - <?php endforeach ?> + + '<td class="col-websites">' + + '<select class="{$block->escapeHtmlAttr($_htmlClass)} required-entry" + name="{$htmlName}[<%- data.index %>][website_id]" id="tier_price_row_<%- data.index %>_website">' +script; +foreach ($block->getWebsites() as $_websiteId => $_info): + $scriptString .= <<<script + + '<option value="{$block->escapeHtmlAttr($_websiteId)}">{$block->escapeHtml($_info['name'])} +script; + if (!empty($_info['currency'])): + $scriptString .= <<<script + [{$block->escapeHtml($_info['currency'])}] +script; + endif; + $scriptString .= <<<script + </option>' +script; + endforeach; + $scriptString .= <<<script + '</select></td>' - + '<td class="col-customer-group"><select class="<?= $block->escapeHtmlAttr($_htmlClass) ?> custgroup required-entry" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][cust_group]" id="tier_price_row_<%- data.index %>_cust_group">' - <?php foreach ($block->getCustomerGroups() as $_groupId => $_groupName) :?> - + '<option value="<?= $block->escapeHtmlAttr($_groupId) ?>"><?= $block->escapeHtml($_groupName) ?></option>' - <?php endforeach ?> + + '<td class="col-customer-group"><select class="{$block->escapeJs($_htmlClass)} custgroup required-entry" + name="{$htmlName}[<%- data.index %>][cust_group]" id="tier_price_row_<%- data.index %>_cust_group">' +script; +foreach ($block->getCustomerGroups() as $_groupId => $_groupName): + $scriptString .= <<<script + + '<option value="{$block->escapeJs($_groupId)}">{$block->escapeJs($_groupName)}</option>' +script; + endforeach; + $scriptString .= <<<script + '</select></td>' + '<td class="col-qty">' - + '<input class="<?= $block->escapeHtmlAttr($_htmlClass) ?> qty required-entry validate-greater-than-zero" type="text" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][price_qty]" value="<%- data.qty %>" id="tier_price_row_<%- data.index %>_qty" />' - + '<span><?= $block->escapeHtml(__("and above")) ?></span>' + + '<input class="{$block->escapeJs($_htmlClass)} qty required-entry validate-greater-than-zero" + type="text" name="{$htmlName}[<%- data.index %>][price_qty]" value="<%- data.qty %>" + id="tier_price_row_<%- data.index %>_qty" />' + + '<span>{$block->escapeHtml(__("and above"))}</span>' + '</td>' - + '<td class="col-price"><input class="<?= $block->escapeHtmlAttr($_htmlClass) ?> required-entry <?= $block->escapeHtmlAttr($_priceValueValidation) ?>" type="text" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][price]" value="<%- data.price %>" id="tier_price_row_<%- data.index %>_price" /></td>' - + '<td class="col-delete"><input type="hidden" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][delete]" class="delete" value="" id="tier_price_row_<%- data.index %>_delete" />' - + '<button title="<?= $block->escapeHtml(__('Delete Tier')) ?>" type="button" class="action- scalable delete icon-btn delete-product-option" id="tier_price_row_<%- data.index %>_delete_button" onclick="return tierPriceControl.deleteItem(event);">' - + '<span><?= $block->escapeHtml(__("Delete")) ?></span></button></td>' + + '<td class="col-price"><input class="{$block->escapeJs($_htmlClass)} required-entry + {$block->escapeJs($_priceValueValidation)}" type="text" name="{$htmlName}[<%- data.index %>][price]" + value="<%- data.price %>" id="tier_price_row_<%- data.index %>_price" /></td>' + + '<td class="col-delete"><input type="hidden" name="{$htmlName}[<%- data.index %>][delete]" class="delete" + value="" id="tier_price_row_<%- data.index %>_delete" />' + + '<button title="{$block->escapeJs(__('Delete Tier'))}" type="button" + class="action- scalable delete icon-btn delete-product-option" + id="tier_price_row_<%- data.index %>_delete_button">' + + '<span>{$block->escapeJs(__("Delete"))}</span></button></td>' + '</tr>'; - +script; + +if (!$_showWebsite): + $scriptString .= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', 'td.col-websites'); +endif; + $scriptString .= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'return tierPriceControl.deleteItem(event);', + "'td#tier_price_row_<%- data.index %>_delete_button" + ); + + $defaultWesite = (int) $block->getDefaultWebsite(); + $defaultCustomerGroup = (int) $block->getDefaultCustomerGroup(); + $scriptString .= <<<script var tierPriceControl = { template: mageTemplate(tierPriceRowTemplate), itemsCount: 0, addItem : function () { - <?php if ($_readonly) :?> +script; + if ($_readonly): + $scriptString .= <<<script if (arguments.length < 4) { return; } - <?php endif; ?> +script; + endif; + $scriptString .= <<<script var data = { - website_id: '<?= (int) $block->getDefaultWebsite() ?>', - group: '<?= (int) $block->getDefaultCustomerGroup() ?>', + website_id: '{$defaultWesite}', + group: '{$defaultCustomerGroup}', qty: '', price: '', readOnly: false, @@ -104,7 +156,7 @@ var tierPriceControl = { data.readOnly = arguments[4]; } - Element.insert($('<?= $block->escapeJs($_htmlId) ?>_container'), { + Element.insert($('{$block->escapeJs($_htmlId)}_container'), { bottom : this.template({ data: data }) @@ -113,14 +165,17 @@ var tierPriceControl = { $('tier_price_row_' + data.index + '_cust_group').value = data.group; $('tier_price_row_' + data.index + '_website').value = data.website_id; - <?php if ($block->isShowWebsiteColumn() && !$block->isAllowChangeWebsite()) :?> +script; + if ($block->isShowWebsiteColumn() && !$block->isAllowChangeWebsite()): + $scriptString .= <<<script var wss = $('tier_price_row_' + data.index + '_website'); var txt = wss.options[wss.selectedIndex].text; wss.insert({after:'<span class="website-name">' + txt + '</span>'}); wss.hide(); - <?php endif;?> - +script; + endif; + $scriptString .= <<<script if (data.readOnly == '1') { ['website', 'cust_group', 'qty', 'price', 'delete'].each(function(idx){ $('tier_price_row_'+data.index+'_'+idx).disabled = true; @@ -128,12 +183,20 @@ var tierPriceControl = { $('tier_price_row_'+data.index+'_delete_button').hide(); } - <?php if ($_readonly) :?> - $('<?= $block->escapeJs($_htmlId) ?>_container').select('input', 'select').each(this.disableElement); - $('<?= $block->escapeJs($_htmlId) ?>_container').up('table').select('button').each(this.disableElement); - <?php else :?> - $('<?= $block->escapeJs($_htmlId) ?>_container').select('input', 'select').each(function(el){ Event.observe(el, 'change', el.setHasChanges.bind(el)); }); - <?php endif; ?> +script; + if ($_readonly): + $scriptString .= <<<script + $('{$block->escapeJs($_htmlId)}_container').select('input', 'select').each(this.disableElement); + $('{$block->escapeJs($_htmlId)}_container').up('table').select('button').each(this.disableElement); +script; + else: + $scriptString .= <<<script + $('{$block->escapeJs($_htmlId)}_container').select('input', 'select').each(function(el) { + Event.observe(el, 'change', el.setHasChanges.bind(el)); + }); +script; + endif; + $scriptString .= <<<script }, disableElement: function(el) { el.disabled = true; @@ -150,18 +213,30 @@ var tierPriceControl = { return false; } }; -<?php foreach ($block->getValues() as $_item) :?> -tierPriceControl.addItem('<?= $block->escapeJs($_item['website_id']) ?>', '<?= $block->escapeJs($_item['cust_group']) ?>', '<?= $_item['price_qty']*1 ?>', '<?= $block->escapeJs($_item['price']) ?>', <?= (int)!empty($_item['readonly']) ?>); +script; + ?> +<?php foreach ($block->getValues() as $_item):?> + <?php $readonly = (int)!empty($_item['readonly']); + $price_qty = $_item['price_qty']*1; + $scriptString .= <<<script +tierPriceControl.addItem('{$block->escapeJs($_item['website_id'])}', '{$block->escapeJs($_item['cust_group'])}', + '{$price_qty}', '{$block->escapeJs($_item['price'])}', {$readonly}); +script; + ?> <?php endforeach; ?> -<?php if ($_readonly) :?> -$('<?= $block->escapeJs($_htmlId) ?>_container').up('table').select('button') - .each(tierPriceControl.disableElement); +<?php if ($_readonly):?> + <?php $scriptString .= <<<script +$('{$block->escapeJs($_htmlId)}_container').up('table').select('button').each(tierPriceControl.disableElement); +script; + ?> <?php endif; ?> - +<?php $scriptString .= <<<script window.tierPriceControl = tierPriceControl; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml index 0193d7764cbb5..59b5eb7f523c1 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml @@ -5,11 +5,12 @@ */ /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Websites */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <fieldset id="grop_fields" class="fieldset"> <legend class="legend"><span><?= $block->escapeHtml(__('Product In Websites')) ?></span></legend> <br> - <?php if ($block->getProductId()) :?> + <?php if ($block->getProductId()):?> <div class="messages"> <div class="message message-notice"> <?= $block->escapeHtml(__('To hide an item in catalog or search results, set the status to "Disabled".')) ?> @@ -20,37 +21,44 @@ <?= $block->getHintHtml() ?> <div class="store-tree"> <?php $_websites = $block->getWebsiteCollection() ?> - <?php foreach ($_websites as $_website) :?> + <?php foreach ($_websites as $_website):?> <div class="website-name"> <input name="product[website_ids][]" value="<?= (int) $_website->getId() ?>" - <?php if ($block->isReadonly()) :?> + <?php if ($block->isReadonly()):?> disabled="disabled" <?php endif;?> class="checkbox website-checkbox" id="product_website_<?= (int) $_website->getId() ?>" type="checkbox" - <?php if ($block->hasWebsite($_website->getId()) || !$block->getProductId() && count($_websites) === 1) :?> + <?php if ($block->hasWebsite($_website->getId()) || + !$block->getProductId() && count($_websites) === 1):?> checked="checked" <?php endif; ?> /> - <label for="product_website_<?= (int) $_website->getId() ?>"><?= $block->escapeHtml($_website->getName()) ?></label> + <label for="product_website_<?= (int) $_website->getId() ?>"> + <?= $block->escapeHtml($_website->getName()) ?> + </label> </div> <dl class="webiste-groups" id="product_website_<?= (int) $_website->getId() ?>_data"> - <?php foreach ($block->getGroupCollection($_website) as $_group) :?> + <?php foreach ($block->getGroupCollection($_website) as $_group):?> <dt><?= $block->escapeHtml($_group->getName()) ?></dt> <dd> <ul> - <?php foreach ($block->getStoreCollection($_group) as $_store) :?> + <?php foreach ($block->getStoreCollection($_group) as $_store):?> <li> <?= $block->escapeHtml($_store->getName()) ?> - <?php if ($block->getWebsites() && !$block->hasWebsite($_website->getId())) :?> - <span class="website-<?= (int) $_website->getId() ?>-select" style="display:none"> + <?php if ($block->getWebsites() && !$block->hasWebsite($_website->getId())):?> + <span class="website-<?= (int) $_website->getId() ?>-select"> <?= $block->escapeHtml( __('(Copy data from: %1)', $block->getChooseFromStoreHtml($_store)), ['select', 'option', 'optgroup'] ) ?> - </span> + </span> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'span.website-' . (int)$_website->getId() . '-select' + ) ?> <?php endif; ?> </li> <?php endforeach; ?> @@ -63,11 +71,11 @@ </div> </fieldset> -<script> +<?php $scriptString = <<<script require(["prototype"], function(){ //<![CDATA[ - var productWebsiteCheckboxes = $$('.website-checkbox'); + var productWebsiteCheckboxes = \$$('.website-checkbox'); for (var i = 0; i < productWebsiteCheckboxes.length; i++) { Event.observe(productWebsiteCheckboxes[i], 'click', toggleStoreFromChoosers); @@ -76,7 +84,8 @@ require(["prototype"], function(){ function toggleStoreFromChoosers(event) { var element = Event.element(event); var selects = $('product_website_' + element.value + '_data').getElementsBySelector('select'); - var selectBlocks = $('product_website_' + element.value + '_data').getElementsByClassName('website-' + element.value + '-select'); + var selectBlocks = $('product_website_' + element.value + '_data') + .getElementsByClassName('website-' + element.value + '-select'); for (var i = 0; i < selects.length; i++) { selects[i].disabled = !element.checked; } @@ -93,4 +102,6 @@ require(["prototype"], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml index d5db46f706ce3..94d71dbb5ab28 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml @@ -4,20 +4,19 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $elementName = $block->getElement()->getName() . '[images]'; $formName = $block->getFormName(); +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <div id="<?= $block->getHtmlId() ?>" class="gallery" data-mage-init='{"productGallery":{"template":"#<?= $block->getHtmlId() ?>-template"}}' data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" data-images="<?= $block->escapeHtml($block->getImagesJson()) ?>" - data-types="<?= $block->escapeHtml( - $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes()) - ) ?>" + data-types="<?= $block->escapeHtml($jsonHelper->jsonEncode($block->getImageTypes())) ?>" > <?php if (!$block->getElement()->getReadonly()) {?> <div class="image image-placeholder"> @@ -138,7 +137,9 @@ $formName = $block->getFormName(); <textarea data-role="image-description" rows="3" class="admin__control-textarea" - name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> + name="<?= $block->escapeHtmlAttr($elementName) + ?>[<%- data.file_id %>][label]"><%- data.label %> + </textarea> </div> </div> @@ -149,7 +150,7 @@ $formName = $block->getFormName(); <div class="admin__field-control"> <ul class="multiselect-alt"> <?php - foreach ($block->getMediaAttributes() as $attribute) : + foreach ($block->getMediaAttributes() as $attribute): ?> <li class="item"> <label> @@ -182,7 +183,8 @@ $formName = $block->getFormName(); <label class="admin__field-label"> <span><?= $block->escapeHtml(__('Image Resolution')) ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) ?>"></div> + <div class="admin__field-value" + data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) ?>"></div> </div> <div class="admin__field field-image-hide"> @@ -208,6 +210,4 @@ $formName = $block->getFormName(); </script> <?= $block->getChildHtml('new-video') ?> </div> -<script> - jQuery('body').trigger('contentUpdated'); -</script> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], "jQuery('body').trigger('contentUpdated');", false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml index 0a13aee5930ad..4a0a37147dd13 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml @@ -4,11 +4,16 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var \Magento\Catalog\Block\Adminhtml\Product\Edit\Js $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php +/** @var TaxHelper $taxHelper */ +$taxHelper = $block->getData('taxHelper'); +$priceFormat = /* @noEscape */ $taxHelper->getPriceFormat($block->getStore()); +$allRatesByProductClassJson = /* @noEscape */ $block->getAllRatesByProductClassJson(); +$scriptString = <<<script require([ "jquery", "prototype", @@ -30,8 +35,8 @@ function registerTaxRecalcs() { Event.observe($('tax_class_id'), 'change', recalculateTax); } -var priceFormat = <?= /* @noEscape */ $this->helper(Magento\Tax\Helper\Data::class)->getPriceFormat($block->getStore()) ?>; -var taxRates = <?= /* @noEscape */ $block->getAllRatesByProductClassJson() ?>; +var priceFormat = {$priceFormat}; +var taxRates = {$allRatesByProductClassJson}; function recalculateTax() { if (typeof dynamicTaxes == 'undefined') { @@ -75,16 +80,22 @@ function bindActiveProductTab(event, ui) { jQuery(document).on('tabsactivate', bindActiveProductTab); // bind active tab -<?php if ($tabsBlock = $block->getLayout()->getBlock('product_tabs')) :?> +script; +if ($tabsBlock = $block->getLayout()->getBlock('product_tabs')): + $scriptString .= <<<script jQuery(function () { - if (jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').length && jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').is(':mage-tabs')) { - var activeAnchor = jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').tabs('activeAnchor'); + if (jQuery('#{$block->escapeJs($tabsBlock->getId())}').length && + jQuery('#{$block->escapeJs($tabsBlock->getId())}').is(':mage-tabs')) { + var activeAnchor = jQuery('#{$block->escapeJs($tabsBlock->getId())}').tabs('activeAnchor'); if (activeAnchor && $('store_switcher')) { $('store_switcher').switchParams = 'active_tab/' + activeAnchor.prop('name') + '/'; } } }); -<?php endif; ?> +script; +endif; + +$scriptString .= <<<script window.recalculateTax = recalculateTax; window.bindActiveProductTab = bindActiveProductTab; @@ -92,4 +103,6 @@ window.registerTaxRecalcs = registerTaxRecalcs; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml index 5028d3c1e83d0..2cf5a78a8138a 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml @@ -5,10 +5,11 @@ */ /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Inventory */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->isReadonly()) :?> +<?php if ($block->isReadonly()):?> <?php $_readonly = ' disabled="disabled" '; ?> -<?php else :?> +<?php else: ?> <?php $_readonly = ''; ?> <?php endif; ?> <fieldset class="fieldset form-inline"> @@ -21,43 +22,56 @@ </label> <div class="control"> <select id="inventory_manage_stock" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][manage_stock]" <?= /* @noEscape */ $_readonly ?>> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][manage_stock]" <?= /* @noEscape */ $_readonly ?>> <option value="1"><?= $block->escapeHtml(__('Yes')) ?></option> - <option value="0"<?php if ($block->getFieldValue('manage_stock') == 0) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> + <option value="0"<?php if ($block->getFieldValue('manage_stock') == 0):?> + selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('No')) ?> + </option> </select> <input type="hidden" id="inventory_manage_stock_default" value="<?= $block->escapeHtmlAttr($block->getDefaultConfigValue('manage_stock')) ?>"> - <?php $_checked = ($block->getFieldValue('use_config_manage_stock') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_manage_stock') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_manage_stock" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_manage_stock]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_manage_stock"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> - <?php if (!$block->isReadonly()) :?> - <script> + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_manage_stock" + ) ?> + <label for="inventory_use_config_manage_stock"><?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> + <?php if (!$block->isReadonly()):?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_manage_stock'), $('inventory_use_config_manage_stock').parentNode); + toggleValueElements($('inventory_use_config_manage_stock'), + $('inventory_use_config_manage_stock').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> - <?php if (!$block->getProduct()->isComposite()) :?> + <?php if (!$block->getProduct()->isComposite()): ?> <div class="field"> <label class="label" for="inventory_qty"> <span><?= $block->escapeHtml(__('Qty')) ?></span> </label> <div class="control"> - <?php if (!$_readonly) :?> + <?php if (!$_readonly): ?> <input type="hidden" id="original_inventory_qty" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][original_inventory_qty]" + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][original_inventory_qty]" value="<?= $block->getFieldValue('qty') * 1 ?>"> <?php endif;?> <input type="text" @@ -66,7 +80,7 @@ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][qty]" value="<?= $block->getFieldValue('qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -83,24 +97,34 @@ value="<?= $block->getFieldValue('min_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_min_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_min_qty') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_min_qty" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_min_qty]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_min_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_min_qty" + ) ?> + + <label for="inventory_use_config_min_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()): ?> + <?php $scriptString = <<<script require(["prototype"], function(){ - toggleValueElements($('inventory_use_config_min_qty'), $('inventory_use_config_min_qty').parentNode); + toggleValueElements($('inventory_use_config_min_qty'), + $('inventory_use_config_min_qty').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()):?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -114,24 +138,35 @@ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][min_sale_qty]" value="<?= $block->getFieldValue('min_sale_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_min_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_min_sale_qty') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_min_sale_qty" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_min_sale_qty]" + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][use_config_min_sale_qty]" value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" class="checkbox" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_min_sale_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_min_sale_qty" + ) ?> + <label for="inventory_use_config_min_sale_qty"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()):?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_min_sale_qty'), $('inventory_use_config_min_sale_qty').parentNode); + toggleValueElements($('inventory_use_config_min_sale_qty'), + $('inventory_use_config_min_sale_qty').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -146,60 +181,75 @@ id="inventory_max_sale_qty" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][max_sale_qty]" value="<?= $block->getFieldValue('max_sale_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> - <?php $_checked = ($block->getFieldValue('use_config_max_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_max_sale_qty') || $block->isNew()) ? + 'checked="checked"' : '' ?> <div class="control-inner-wrap"> <input type="checkbox" id="inventory_use_config_max_sale_qty" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_max_sale_qty]" + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][use_config_max_sale_qty]" value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" class="checkbox" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_max_sale_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_max_sale_qty" + ) ?> + <label for="inventory_use_config_max_sale_qty"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()):?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_max_sale_qty'), $('inventory_use_config_max_sale_qty').parentNode); + toggleValueElements($('inventory_use_config_max_sale_qty'), + $('inventory_use_config_max_sale_qty').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()):?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> - <?php if ($block->canUseQtyDecimals()) :?> + <?php if ($block->canUseQtyDecimals()): ?> <div class="field"> <label class="label" for="inventory_is_qty_decimal"> <span><?= $block->escapeHtml(__('Qty Uses Decimals')) ?></span> </label> <div class="control"> <select id="inventory_is_qty_decimal" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_qty_decimal]" <?= /* @noEscape */ $_readonly ?>> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][is_qty_decimal]" <?= /* @noEscape */ $_readonly ?>> <option value="0"><?= $block->escapeHtml(__('No')) ?></option> - <option value="1"<?php if ($block->getFieldValue('is_qty_decimal') == 1) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> + <option value="1"<?php if ($block->getFieldValue('is_qty_decimal') == 1):?> + selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?> + </option> </select> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> - <?php if (!$block->isVirtual()) :?> + <?php if (!$block->isVirtual()): ?> <div class="field"> <label class="label" for="inventory_is_decimal_divided"> <span><?= $block->escapeHtml(__('Allow Multiple Boxes for Shipping')) ?></span> </label> <div class="control"> <select id="inventory_is_decimal_divided" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_decimal_divided]" <?= /* @noEscape */ $_readonly ?>> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][is_decimal_divided]" <?= /* @noEscape */ $_readonly ?>> <option value="0"><?= $block->escapeHtml(__('No')) ?></option> - <option value="1"<?php if ($block->getFieldValue('is_decimal_divided') == 1) :?> + <option value="1"<?php if ($block->getFieldValue('is_decimal_divided') == 1): ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> </select> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()):?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -212,31 +262,45 @@ </label> <div class="control"> <select id="inventory_backorders" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][backorders]" <?= /* @noEscape */ $_readonly ?>> - <?php foreach ($block->getBackordersOption() as $option) :?> - <?php $_selected = ($option['value'] == $block->getFieldValue('backorders')) ? 'selected="selected"' : '' ?> - <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?></option> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][backorders]" <?= /* @noEscape */ $_readonly ?>> + <?php foreach ($block->getBackordersOption() as $option):?> + <?php $_selected = ($option['value'] == $block->getFieldValue('backorders')) ? + 'selected="selected"' : '' ?> + <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" + <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?> + </option> <?php endforeach; ?> </select> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_backorders') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_backorders') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_backorders" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_backorders]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_backorders"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_backorders" + ) ?> + <label for="inventory_use_config_backorders"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()): ?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_backorders'), $('inventory_use_config_backorders').parentNode); + toggleValueElements($('inventory_use_config_backorders'), + $('inventory_use_config_backorders').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -250,26 +314,38 @@ class="input-text validate-number" id="inventory_notify_stock_qty" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][notify_stock_qty]" - value="<?= $block->getFieldValue('notify_stock_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> + value="<?= $block->getFieldValue('notify_stock_qty') * 1 ?>" + <?= /* @noEscape */ $_readonly ?>> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_notify_stock_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_notify_stock_qty') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_notify_stock_qty" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_notify_stock_qty]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_notify_stock_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][use_config_notify_stock_qty]" + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_notify_stock_qty" + ) ?> + <label for="inventory_use_config_notify_stock_qty"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()): ?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_notify_stock_qty'), $('inventory_use_config_notify_stock_qty').parentNode); + toggleValueElements($('inventory_use_config_notify_stock_qty'), + $('inventory_use_config_notify_stock_qty').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()):?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -282,32 +358,48 @@ <div class="control"> <?php $qtyIncrementsEnabled = $block->getFieldValue('enable_qty_increments'); ?> <select id="inventory_enable_qty_increments" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][enable_qty_increments]" <?= /* @noEscape */ $_readonly ?>> - <option value="1"<?php if ($qtyIncrementsEnabled) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> - <option value="0"<?php if (!$qtyIncrementsEnabled) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][enable_qty_increments]" <?= /* @noEscape */ $_readonly ?>> + <option value="1"<?php if ($qtyIncrementsEnabled):?> selected="selected"<?php endif; ?>> + <?= $block->escapeHtml(__('Yes')) ?> + </option> + <option value="0"<?php if (!$qtyIncrementsEnabled):?> selected="selected"<?php endif; ?>> + <?= $block->escapeHtml(__('No')) ?> + </option> </select> <input type="hidden" id="inventory_enable_qty_increments_default" value="<?= $block->escapeHtmlAttr($block->getDefaultConfigValue('enable_qty_increments')) ?>"> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_enable_qty_inc') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_enable_qty_inc') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_enable_qty_increments" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_enable_qty_increments]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_enable_qty_increments"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][use_config_enable_qty_increments]" + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_enable_qty_increments" + ) ?> + <label for="inventory_use_config_enable_qty_increments"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()): ?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_enable_qty_increments'), $('inventory_use_config_enable_qty_increments').parentNode); + toggleValueElements($('inventory_use_config_enable_qty_increments'), + $('inventory_use_config_enable_qty_increments').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -323,23 +415,33 @@ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][qty_increments]" value="<?= $block->getFieldValue('qty_increments') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_qty_increments') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_qty_increments') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_qty_increments" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_qty_increments]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_qty_increments"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_qty_increments" + ) ?> + <label for="inventory_use_config_qty_increments"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()): ?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_qty_increments'), $('inventory_use_config_qty_increments').parentNode); + toggleValueElements($('inventory_use_config_qty_increments'), + $('inventory_use_config_qty_increments').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -350,21 +452,25 @@ </label> <div class="control"> <select id="inventory_stock_availability" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_in_stock]" <?= /* @noEscape */ $_readonly ?>> - <?php foreach ($block->getStockOption() as $option) :?> - <?php $_selected = ($block->getFieldValue('is_in_stock') !== null && $option['value'] == $block->getFieldValue('is_in_stock')) ? 'selected="selected"' : '' ?> - <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?></option> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][is_in_stock]" <?= /* @noEscape */ $_readonly ?>> + <?php foreach ($block->getStockOption() as $option):?> + <?php $_selected = ($block->getFieldValue('is_in_stock') !== null && + $option['value'] == $block->getFieldValue('is_in_stock')) ? 'selected="selected"' : '' ?> + <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" + <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?> + </option> <?php endforeach; ?> </select> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()):?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> </div> </fieldset> -<script> +<?php $scriptString = <<<script require(["jquery","prototype"], function(jQuery){ //<![CDATA[ @@ -380,7 +486,7 @@ inventory_qty_increments: true }; - $$('#table_cataloginventory > div').each(function(el) { + \$$('#table_cataloginventory > div').each(function(el) { if (el == $('inventory_manage_stock').up(1)) { return; } @@ -406,15 +512,23 @@ } function applyEnableDecimalDivided() { - <?php if (!$block->isVirtual()) :?> +script; +if (!$block->isVirtual()): + $scriptString .= <<<script $('inventory_is_decimal_divided').up('.field').hide(); - <?php endif; ?> +script; + endif; + $scriptString .= <<<script $('inventory_qty_increments').removeClassName('validate-digits').removeClassName('validate-number'); $('inventory_min_sale_qty').removeClassName('validate-digits').removeClassName('validate-number'); if ($('inventory_is_qty_decimal').value == 1) { - <?php if (!$block->isVirtual()) :?> +script; +if (!$block->isVirtual()): + $scriptString .= <<<script $('inventory_is_decimal_divided').up('.field').show(); - <?php endif; ?> +script; + endif; + $scriptString .= <<<script $('inventory_qty_increments').addClassName('validate-number'); $('inventory_min_sale_qty').addClassName('validate-number'); } else { @@ -448,4 +562,6 @@ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml index 17fb517b32547..f58e213b60772 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Attributes\Search */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="product-attribute-search-container" class="suggest-expandable attribute-selector"> <div class="action-dropdown"> - <button type="button" class="action-toggle action-choose" data-mage-init='{"dropdown":{}}' data-toggle="dropdown"> + <button type="button" class="action-toggle action-choose" data-mage-init='{"dropdown":{}}' + data-toggle="dropdown"> <span><?= $block->escapeHtml(__('Add Attribute')) ?></span> </button> <div class="dropdown-menu"> @@ -21,7 +21,8 @@ </div> </div> -<script data-template-for="product-attribute-search-<?= $block->escapeHtmlAttr($block->getGroupId()) ?>" type="text/x-magento-template"> +<script data-template-for="product-attribute-search-<?= $block->escapeHtmlAttr($block->getGroupId()) ?>" + type="text/x-magento-template"> <ul data-mage-init='{"menu":[]}'> <% if (data.items.length) { %> <% _.each(data.items, function(value){ %> @@ -32,9 +33,15 @@ <div class="actions"><?= $block->escapeHtml($block->getAttributeCreate()) ?></div> </script> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$selectorOptions = /* @noEscape */ $jsonHelper->jsonEncode($block->getSelectorOptions()); +$scriptString = <<<script require(["jquery","mage/mage","mage/backend/suggest"], function($) { - var $suggest = $('[data-role="product-attribute-search"][data-group="<?= $block->escapeHtml($block->getGroupCode()) ?>"]'); + var $suggest = $('[data-role="product-attribute-search"][data-group="{$block->escapeHtml( + $block->getGroupCode() + )}"]'); $suggest.on('suggestclose', function(e) { $suggest.closest('.dropdown-menu').siblings('[data-toggle=dropdown]').trigger('close.dropdown'); @@ -51,13 +58,13 @@ }); }); - $suggest.mage('suggest', <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSelectorOptions()) ?>) + $suggest.mage('suggest', {$selectorOptions}) .on('suggestselect', function (e, ui) { $(this).val(''); var templateId = $('#attribute_set_id').val(); if (ui.item.id) { $.ajax({ - url: '<?= $block->escapeJs($block->escapeUrl($block->getAddAttributeUrl())) ?>', + url: '{$block->escapeJs($block->getAddAttributeUrl())}', type: 'POST', dataType: 'json', data: {attribute_id: ui.item.id, template_id: templateId, group: $(this).data('group')}, @@ -70,5 +77,7 @@ } }); }); -</script> +script; +?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml index c814298d1dbc5..3ba507d2b3ebb 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml @@ -5,9 +5,10 @@ */ /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Grid */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="<?= $block->getHtmlId() ?>" class="admin__grid-massaction"> - <?php if ($block->getHideFormElement() !== true) :?> + <?php if ($block->getHideFormElement() !== true):?> <form action="" id="<?= $block->getHtmlId() ?>-form" method="post"> <?php endif ?> <div class="admin__grid-massaction-form"> @@ -15,20 +16,25 @@ <select id="<?= $block->getHtmlId() ?>-select" class="local-validation admin__control-select"> - <option class="admin__control-select-placeholder" value="" selected><?= $block->escapeHtml(__('Actions')) ?></option> - <?php foreach ($block->getItems() as $_item) :?> - <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= $block->escapeHtml($_item->getLabel()) ?></option> + <option class="admin__control-select-placeholder" value="" + selected><?= $block->escapeHtml(__('Actions')) ?> + </option> + <?php foreach ($block->getItems() as $_item):?> + <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" + <?= ($_item->getSelected() ? ' selected="selected"' : '') ?>> + <?= $block->escapeHtml($_item->getLabel()) ?> + </option> <?php endforeach; ?> </select> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-hiddens"></span> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-additional"></span> <?= $block->getApplyButtonHtml() ?> </div> - <?php if ($block->getHideFormElement() !== true) :?> + <?php if ($block->getHideFormElement() !== true):?> </form> <?php endif ?> <div class="no-display"> - <?php foreach ($block->getItems() as $_item) :?> + <?php foreach ($block->getItems() as $_item):?> <div id="<?= $block->getHtmlId() ?>-item-<?= $block->escapeHtmlAttr($_item->getId()) ?>-block"> <?= $_item->getAdditionalActionBlockHtml() ?> </div> @@ -39,7 +45,7 @@ <select id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>-mass-select" data-menu="grid-mass-select"> <optgroup label="<?= $block->escapeHtmlAttr(__('Mass Actions')) ?>"> <option disabled selected></option> - <?php if ($block->getUseSelectAll()) :?> + <?php if ($block->getUseSelectAll()):?> <option value="selectAll"> <?= $block->escapeHtml(__('Select All')) ?> </option> @@ -58,34 +64,40 @@ <label for="<?= $block->getHtmlId() ?>-mass-select"></label> </div> -<script> + <?php $scriptString = <<<script require(['jquery'], function($){ 'use strict'; - $('#<?= $block->getHtmlId() ?>-mass-select').change(function () { + $('#{$block->getHtmlId()}-mass-select').change(function () { var massAction = $('option:selected', this).val(); switch (massAction) { - <?php if ($block->getUseSelectAll()) :?> +script; + if ($block->getUseSelectAll()): + $scriptString .= <<<script case 'selectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectAll(); + return {$block->escapeJs($block->getJsObjectName())}.selectAll(); break case 'unselectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectAll(); + return {$block->escapeJs($block->getJsObjectName())}.unselectAll(); break - <?php endif; ?> +script; + endif; + $scriptString .= <<<script case 'selectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectVisible(); + return {$block->escapeJs($block->getJsObjectName())}.selectVisible(); break case 'unselectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectVisible(); + return {$block->escapeJs($block->getJsObjectName())}.unselectVisible(); break } this.blur(); }); }); - - <?php if (!$block->getParentBlock()->canDisplayContainer()) :?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.setGridIds('<?= /* @noEscape */ $block->getGridIdsJson() ?>'); - <?php endif; ?> -</script> +script; + if (!$block->getParentBlock()->canDisplayContainer()): + $scriptString .= $block->escapeJs($block->getJsObjectName()) . ".setGridIds('" . + /* @noEscape */ $block->getGridIdsJson() . "');"; + endif; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/url_filter_applier.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/url_filter_applier.phtml new file mode 100644 index 0000000000000..3e00503a882db --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/url_filter_applier.phtml @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var $block \Magento\Backend\Block\Template */ +?> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Ui/js/grid/url-filter-applier": { + "listingNamespace": "product_listing" + } + } + } +</script> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml index d3689a0db1306..6e941821e9505 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml @@ -470,7 +470,7 @@ </imports> </settings> </field> - <field name="page_layout" sortOrder="190" formElement="select" component="Magento_Catalog/js/components/use-parent-settings/select" class="Magento\Catalog\Ui\Component\Form\Field\Category\PageLayout"> + <field name="page_layout" sortOrder="190" formElement="select" component="Magento_Catalog/js/components/use-parent-settings/select"> <settings> <dataType>string</dataType> <label translate="true">Layout</label> 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/adminhtml/web/js/product-gallery.js b/app/code/Magento/Catalog/view/adminhtml/web/js/product-gallery.js index b2a12bea30150..1af0f10770c41 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/product-gallery.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/product-gallery.js @@ -188,12 +188,17 @@ define([ _addItem: function (event, imageData) { var count = this.element.find(this.options.imageSelector).length, element, - imgElement; + imgElement, + position = count + 1, + lastElement = this.element.find(this.options.imageSelector + ':last'); + if (lastElement.length === 1) { + position = parseInt(lastElement.data('imageData').position || count, 10) + 1; + } imageData = $.extend({ 'file_id': imageData['value_id'] ? imageData['value_id'] : Math.random().toString(33).substr(2, 18), 'disabled': imageData.disabled ? imageData.disabled : 0, - 'position': count + 1, + 'position': position, sizeLabel: bytesToSize(imageData.size) }, imageData); @@ -206,7 +211,7 @@ define([ if (count === 0) { element.prependTo(this.element); } else { - element.insertAfter(this.element.find(this.options.imageSelector + ':last')); + element.insertAfter(lastElement); } if (!this.options.initialized && diff --git a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml index 78d2883d7401a..950eb16fb5019 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml @@ -8,9 +8,10 @@ use Magento\Catalog\Model\Product\Option; /** * @var $block \Magento\Catalog\Block\Product\View\Options\Type\Select\Checkable + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $option = $block->getOption(); -if ($option) : ?> +if ($option): ?> <?php $configValue = $block->getPreconfiguredValue($option); $optionType = $option->getType(); @@ -19,17 +20,23 @@ if ($option) : ?> ?> <div class="options-list nested" id="options-<?= $block->escapeHtmlAttr($option->getId()) ?>-list"> - <?php if ($optionType === Option::OPTION_TYPE_RADIO && !$option->getIsRequire()) :?> + <?php if ($optionType === Option::OPTION_TYPE_RADIO && !$option->getIsRequire()):?> <div class="field choice admin__field admin__field-option"> <input type="radio" id="options_<?= $block->escapeHtmlAttr($option->getId()) ?>" class="radio admin__control-radio product-custom-option" name="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]" data-selector="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]" - onclick="<?= $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" value="" checked="checked" /> + <?php if (!$block->getSkipJsReloadPrice()): ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'opConfig.reloadPrice()', + "options_" . $block->escapeJs($option->getId()) + ) ?> + <?php endif; ?> <label class="label admin__field-label" for="options_<?= $block->escapeHtmlAttr($option->getId()) ?>"> <span> <?= $block->escapeHtml(__('None')) ?> @@ -38,7 +45,7 @@ if ($option) : ?> </div> <?php endif; ?> - <?php foreach ($option->getValues() as $value) : ?> + <?php foreach ($option->getValues() as $value): ?> <?php $checked = ''; $count++; diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml index af50446c93a95..9e740e693fbf2 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml @@ -10,6 +10,7 @@ // phpcs:disable Generic.WhiteSpace.ScopeIndent /** @var \Magento\Catalog\Pricing\Render\PriceBox $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ /** @var \Magento\Catalog\Pricing\Price\TierPrice $tierPriceModel */ $tierPriceModel = $block->getPrice(); @@ -56,10 +57,16 @@ $product = $block->getSaleableItem(); } ?> <?= $block->escapeHtml(__('Buy %1 for: ', $price['price_qty'])) ?> - <a href="javascript:void(0);" + <a href="#" id="<?= $block->escapeHtmlAttr($popupId) ?>" data-tier-price="<?= $block->escapeHtml($block->jsonEncode($tierPriceData)) ?>"> - <?= $block->escapeHtml(__('Click for price')) ?></a> + <?= $block->escapeHtml(__('Click for price')) ?> + </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#' . $block->escapeHtmlAttr($popupId) + ) ?> <?php else: $priceAmountBlock = $block->renderAmount( $price['price'], diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml index e0443d5a55d97..aab181a8f7321 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml @@ -5,28 +5,32 @@ */ /** @var \Magento\Catalog\Block\Product\Gallery $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_width = $block->getImageWidth(); ?> -<div class="product-image-popup" style="width:<?= /* @noEscape */ $_width ?>px;"> - <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= $block->escapeHtml(__('Close Window')) ?></span></a></div> - <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()) :?> +<div class="product-image-popup"> + <div class="buttons-set"><a href="#" class="button" role="close-window"> + <span><?= $block->escapeHtml(__('Close Window')) ?></span></a></div> + <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()):?> <div class="nav"> - <?php if ($_prevUrl = $block->getPreviousImageUrl()) :?> - <a href="<?= $block->escapeUrl($_prevUrl) ?>" class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> + <?php if ($_prevUrl = $block->getPreviousImageUrl()):?> + <a href="<?= $block->escapeUrl($_prevUrl) ?>" + class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> <?php endif; ?> - <?php if ($_nextUrl = $block->getNextImageUrl()) :?> - <a href="<?= $block->escapeUrl($_nextUrl) ?>" class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> + <?php if ($_nextUrl = $block->getNextImageUrl()):?> + <a href="<?= $block->escapeUrl($_nextUrl) ?>" + class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> <?php endif; ?> </div> <?php endif; ?> - <?php if ($_imageTitle = $block->escapeHtml($block->getCurrentImage()->getLabel())) :?> + <?php if ($_imageTitle = $block->escapeHtml($block->getCurrentImage()->getLabel())):?> <h1 class="image-label"><?= /* @noEscape */ $_imageTitle ?></h1> <?php endif; ?> <?php $imageUrl = $block->getImageUrl(); ?> <img src="<?= $block->escapeUrl($imageUrl) ?>" - <?php if ($_width) :?> + <?php if ($_width):?> width="<?= /* @noEscape */ $_width ?>" <?php endif; ?> alt="<?= $block->escapeHtmlAttr($block->getCurrentImage()->getLabel()) ?>" @@ -34,15 +38,23 @@ id="product-gallery-image" class="image" data-mage-init='{"catalogGallery":{}}'/> - <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= $block->escapeHtml(__('Close Window')) ?></span></a></div> - <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()) :?> + <div class="buttons-set">< + a href="#" class="button" role="close-window"><span><?= $block->escapeHtml(__('Close Window')) ?></span></a> + </div> + <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()):?> <div class="nav"> - <?php if ($_prevUrl = $block->getPreviousImageUrl()) :?> - <a href="<?= $block->escapeUrl($_prevUrl) ?>" class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> + <?php if ($_prevUrl = $block->getPreviousImageUrl()):?> + <a href="<?= $block->escapeUrl($_prevUrl) ?>" + class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> <?php endif; ?> - <?php if ($_nextUrl = $block->getNextImageUrl()) :?> - <a href="<?= $block->escapeUrl($_nextUrl) ?>" class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> + <?php if ($_nextUrl = $block->getNextImageUrl()):?> + <a href="<?= $block->escapeUrl($_nextUrl) ?>" + class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> <?php endif; ?> </div> <?php endif; ?> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'width:' . /* @noEscape */ $_width . 'px;', + 'div.product-image-popup' +) ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml index 020eafcff2442..0ac6bc88df8ce 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml @@ -7,6 +7,7 @@ <?php /** @var $block \Magento\Catalog\Block\Product\Image */ /** @var $escaper \Magento\Framework\Escaper */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ /** * Enable lazy loading for images with borders and if variable enable_lazy_loading_for_images_without_borders * is enabled in view.xml. Otherwise small size images without borders may be distorted. So max-width is used for them @@ -17,12 +18,11 @@ $enableLazyLoadingWithoutBorders = (bool)$block->getVar( 'enable_lazy_loading_for_images_without_borders', 'Magento_Catalog' ); +$width = (int)$block->getWidth(); +$paddingBottom = $block->getRatio() * 100; ?> - -<span class="product-image-container" - style="width:<?= $escaper->escapeHtmlAttr($block->getWidth()) ?>px;"> - <span class="product-image-wrapper" - style="padding-bottom: <?= ($block->getRatio() * 100) ?>%;"> +<span class="product-image-container product-image-container-<?= /* @noEscape */ $block->getProductId() ?>"> + <span class="product-image-wrapper"> <img class="<?= $escaper->escapeHtmlAttr($block->getClass()) ?>" <?php foreach ($block->getCustomAttributes() as $name => $value): ?> <?= $escaper->escapeHtmlAttr($name) ?>="<?= $escaper->escapeHtmlAttr($value) ?>" @@ -38,3 +38,29 @@ $enableLazyLoadingWithoutBorders = (bool)$block->getVar( <?php endif; ?> alt="<?= $escaper->escapeHtmlAttr($block->getLabel()) ?>"/></span> </span> +<?php +$styles = <<<STYLE +.product-image-container-{$block->getProductId()} { + width: {$width}px; +} +.product-image-container-{$block->getProductId()} span.product-image-wrapper { + padding-bottom: {$paddingBottom}%; +} +STYLE; +//In case a script was using "style" attributes of these elements +$script = <<<SCRIPT +prodImageContainers = document.querySelectorAll(".product-image-container-{$block->getProductId()}"); +for (var i = 0; i < prodImageContainers.length; i++) { + prodImageContainers[i].style.width = "{$width}px"; +} +prodImageContainersWrappers = document.querySelectorAll( + ".product-image-container-{$block->getProductId()} span.product-image-wrapper" +); +for (var i = 0; i < prodImageContainersWrappers.length; i++) { + prodImageContainersWrappers[i].style.paddingBottom = "{$paddingBottom}%"; +} +SCRIPT; + +?> +<?= /* @noEscape */ $secureRenderer->renderTag('style', [], $styles, false) ?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', ['type' => 'text/javascript'], $script, false) ?> 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 554caf6026001..6a47978f1e5c6 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml @@ -7,27 +7,28 @@ use Magento\Framework\App\Action\Action; ?> <?php -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** * Product list template * * @var $block \Magento\Catalog\Block\Product\ListProduct * @var \Magento\Framework\Escaper $escaper + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_productCollection = $block->getLoadedProductCollection(); /** @var \Magento\Catalog\Helper\Output $_helper */ -$_helper = $this->helper(Magento\Catalog\Helper\Output::class); +$_helper = $block->getData('outputHelper'); ?> -<?php if (!$_productCollection->count()) :?> - <div class="message info empty"><div><?= $escaper->escapeHtml(__('We can\'t find products matching the selection.')) ?></div></div> -<?php else :?> +<?php if (!$_productCollection->count()): ?> + <div class="message info empty"> + <div><?= $escaper->escapeHtml(__('We can\'t find products matching the selection.')) ?></div> + </div> +<?php else: ?> <?= $block->getToolbarHtml() ?> <?= $block->getAdditionalHtml() ?> <?php - if ($block->getMode() == 'grid') { + if ($block->getMode() === 'grid') { $viewMode = 'grid'; $imageDisplayArea = 'category_page_grid'; $showDescription = false; @@ -46,14 +47,16 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); <div class="products wrapper <?= /* @noEscape */ $viewMode ?> products-<?= /* @noEscape */ $viewMode ?>"> <ol class="products list items product-items"> <?php /** @var $_product \Magento\Catalog\Model\Product */ ?> - <?php foreach ($_productCollection as $_product) :?> + <?php foreach ($_productCollection as $_product): ?> <li class="item product product-item"> - <div class="product-item-info" data-container="product-<?= /* @noEscape */ $viewMode ?>"> + <div class="product-item-info" + id="product-item-info_<?= /* @noEscape */ $_product->getId() ?>" + data-container="product-<?= /* @noEscape */ $viewMode ?>"> <?php $productImage = $block->getImage($_product, $imageDisplayArea); if ($pos != null) { - $position = ' style="left:' . $productImage->getWidth() . 'px;' - . 'top:' . $productImage->getHeight() . 'px;"'; + $position = 'left:' . $productImage->getWidth() . 'px;' + . 'top:' . $productImage->getHeight() . 'px;'; } ?> <?php // Product Image ?> @@ -63,25 +66,22 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); <?= $productImage->toHtml() ?> </a> <div class="product details product-item-details"> - <?php - $_productNameStripped = $block->stripTags($_product->getName(), null, true); - ?> + <?php $_productNameStripped = $block->stripTags($_product->getName(), null, true); ?> <strong class="product name product-item-name"> <a class="product-item-link" href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>"> - <?= /* @noEscape */ $_helper->productAttribute($_product, $_product->getName(), 'name') ?> + <?=/* @noEscape */ $_helper->productAttribute($_product, $_product->getName(), 'name')?> </a> </strong> <?= $block->getReviewsSummaryHtml($_product, $templateType) ?> <?= /* @noEscape */ $block->getProductPrice($_product) ?> - <?php if ($_product->isAvailable()) :?> - <?= $block->getProductDetailsHtml($_product) ?> - <?php endif; ?> + + <?= $block->getProductDetailsHtml($_product) ?> <div class="product-item-inner"> - <div class="product actions product-item-actions"<?= strpos($pos, $viewMode . '-actions') ? $escaper->escapeHtmlAttr($position) : '' ?>> - <div class="actions-primary"<?= strpos($pos, $viewMode . '-primary') ? $escaper->escapeHtmlAttr($position) : '' ?>> - <?php if ($_product->isSaleable()) :?> + <div class="product actions product-item-actions"> + <div class="actions-primary"> + <?php if ($_product->isSaleable()):?> <?php $postParams = $block->getAddToCartPostParams($_product); ?> <form data-role="tocart-form" data-product-sku="<?= $escaper->escapeHtml($_product->getSku()) ?>" @@ -90,32 +90,52 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); <input type="hidden" name="product" value="<?= /* @noEscape */ $postParams['data']['product'] ?>"> - <input type="hidden" name="<?= /* @noEscape */ Action::PARAM_NAME_URL_ENCODED ?>" - value="<?= /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> + <input type="hidden" + name="<?= /* @noEscape */ Action::PARAM_NAME_URL_ENCODED ?>" + value="<?= + /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] + ?>"> <?= $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> - <?php else :?> - <?php if ($_product->isAvailable()) :?> - <div class="stock available"><span><?= $escaper->escapeHtml(__('In stock')) ?></span></div> - <?php else :?> - <div class="stock unavailable"><span><?= $escaper->escapeHtml(__('Out of stock')) ?></span></div> + <?php else:?> + <?php if ($_product->isAvailable()):?> + <div class="stock available"> + <span><?= $escaper->escapeHtml(__('In stock')) ?></span></div> + <?php else:?> + <div class="stock unavailable"> + <span><?= $escaper->escapeHtml(__('Out of stock')) ?></span></div> <?php endif; ?> <?php endif; ?> </div> - <div data-role="add-to-links" class="actions-secondary"<?= strpos($pos, $viewMode . '-secondary') ? $escaper->escapeHtmlAttr($position) : '' ?>> - <?php if ($addToBlock = $block->getChildBlock('addto')) :?> + <?= strpos($pos, $viewMode . '-primary') ? + /* @noEscape */ $secureRenderer->renderStyleAsTag( + $position, + 'product-item-info_' . $_product->getId() . ' div.actions-primary' + ) : '' ?> + <div data-role="add-to-links" class="actions-secondary"> + <?php if ($addToBlock = $block->getChildBlock('addto')): ?> <?= $addToBlock->setProduct($_product)->getChildHtml() ?> <?php endif; ?> </div> + <?= strpos($pos, $viewMode . '-secondary') ? + /* @noEscape */ $secureRenderer->renderStyleAsTag( + $position, + 'product-item-info_' . $_product->getId() . ' div.actions-secondary' + ) : '' ?> </div> - <?php if ($showDescription) :?> + <?php if ($showDescription): ?> <div class="product description product-item-description"> - <?= /* @noEscape */ $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') ?> + <?= /* @noEscape */ $_helper->productAttribute( + $_product, + $_product->getShortDescription(), + 'short_description' + ) ?> <a href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>" title="<?= /* @noEscape */ $_productNameStripped ?>" class="action more"><?= $escaper->escapeHtml(__('Learn More')) ?></a> @@ -124,12 +144,17 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); </div> </div> </div> + <?= strpos($pos, $viewMode . '-actions') ? + /* @noEscape */ $secureRenderer->renderStyleAsTag( + $position, + 'product-item-info_' . $_product->getId() . ' div.product-item-actions' + ) : '' ?> </li> <?php endforeach; ?> </ol> </div> <?= $block->getToolbarHtml() ?> - <?php if (!$block->isRedirectToCartEnabled()) :?> + <?php if (!$block->isRedirectToCartEnabled()): ?> <script type="text/x-magento-init"> { "[data-role=tocart-form], .form.map.checkout": { diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml index c8b35e4dc5aa6..e426b940deab7 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml @@ -7,12 +7,8 @@ use Magento\Catalog\ViewModel\Product\Listing\PreparePostData; use Magento\Framework\App\ActionInterface; -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -// phpcs:disable Generic.WhiteSpace.ScopeIndent.Incorrect -// phpcs:disable Generic.Files.LineLength -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper - /* @var $block \Magento\Catalog\Block\Product\AbstractProduct */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php @@ -164,11 +160,19 @@ $_item = null; <?php if ($exist):?> -<?php if ($type == 'related' || $type == 'upsell'):?> -<?php if ($type == 'related'):?> -<div class="block <?= $block->escapeHtmlAttr($class) ?>" data-mage-init='{"relatedProducts":{"relatedCheckbox":".related.checkbox"}}' data-limit="<?= $block->escapeHtmlAttr($limit) ?>" data-shuffle="<?= /* @noEscape */ $shuffle ?>" data-shuffle-weighted="<?= /* @noEscape */ $isWeightedRandom ?>"> + <?php if ($type == 'related' || $type == 'upsell'):?> + <?php if ($type == 'related'):?> +<div class="block <?= $block->escapeHtmlAttr($class) ?>" + data-mage-init='{"relatedProducts":{"relatedCheckbox":".related.checkbox"}}' + data-limit="<?= $block->escapeHtmlAttr($limit) ?>" + data-shuffle="<?= /* @noEscape */ $shuffle ?>" + data-shuffle-weighted="<?= /* @noEscape */ $isWeightedRandom ?>"> <?php else:?> - <div class="block <?= $block->escapeHtmlAttr($class) ?>" data-mage-init='{"upsellProducts":{}}' data-limit="<?= $block->escapeHtmlAttr($limit) ?>" data-shuffle="<?= /* @noEscape */ $shuffle ?>" data-shuffle-weighted="<?= /* @noEscape */ $isWeightedRandom ?>"> + <div class="block <?= $block->escapeHtmlAttr($class) ?>" + data-mage-init='{"upsellProducts":{}}' + data-limit="<?= $block->escapeHtmlAttr($limit) ?>" + data-shuffle="<?= /* @noEscape */ $shuffle ?>" + data-shuffle-weighted="<?= /* @noEscape */ $isWeightedRandom ?>"> <?php endif; ?> <?php else:?> <div class="block <?= $block->escapeHtmlAttr($class) ?>"> @@ -195,7 +199,13 @@ $_item = null; <?php endif; ?> <?php endif; ?> <?php if ($type == 'related' || $type == 'upsell'):?> - <li class="item product product-item" style="display: none;" data-shuffle-group="<?= $block->escapeHtmlAttr($_item->getPriority()) ?>" > + <li class="item product product-item" + id="product-item_<?= /* @noEscape */ $_item->getId() ?>" + data-shuffle-group="<?= $block->escapeHtmlAttr($_item->getPriority()) ?>" > + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none;', + 'li#product-item_' . $_item->getId() + ) ?> <?php else:?> <li class="item product product-item"> <?php endif; ?> @@ -222,17 +232,16 @@ $_item = null; <?php if ($canItemsAddToCart && !$_item->isComposite() && $_item->isSaleable() && $type == 'related'):?> <?php if (!$_item->getRequiredOptions()):?> - <div class="field choice related"><input - type="checkbox" - class="checkbox related" - id="related-checkbox<?= $block->escapeHtmlAttr($_item->getId()) ?>" - name="related_products[]" - value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" /> - <label - class="label" - for="related-checkbox<?= $block->escapeHtmlAttr( - $_item->getId() - ) ?>"><span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + <div class="field choice related"> + <input + type="checkbox" + class="checkbox related" + id="related-checkbox<?= $block->escapeHtmlAttr($_item->getId()) ?>" + name="related_products[]" + value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" /> + <label class="label" + for="related-checkbox<?= $block->escapeHtmlAttr($_item->getId()) + ?>"><span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </label> </div> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml index e83e55ad2a03c..f5fd1c5aa64e1 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ ?> <?php $_option = $block->getOption(); ?> <?php $_fileInfo = $block->getFileInfo(); ?> <?php $_fileExists = $_fileInfo->hasData(); ?> @@ -19,13 +21,18 @@ <span><?= $block->escapeHtml($_option->getTitle()) ?></span> <?= /* @noEscape */ $block->getFormattedPrice() ?> </label> - <?php if ($_fileExists) :?> + <?php if ($_fileExists):?> <div class="control"> <span class="<?= /* @noEscape */ $_fileNamed ?>"><?= $block->escapeHtml($_fileInfo->getTitle()) ?></span> - <a href="javascript:void(0)" class="label" id="change-<?= /* @noEscape */ $_fileName ?>" > + <a href="#" class="label" id="change-<?= /* @noEscape */ $_fileName ?>" > <?= $block->escapeHtml(__('Change')) ?> </a> - <?php if (!$_option->getIsRequire()) :?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#change-' ./* @noEscape */ $_fileName + ) ?> + <?php if (!$_option->getIsRequire()):?> <input type="checkbox" id="delete-<?= /* @noEscape */ $_fileName ?>" /> <span class="label"><?= $block->escapeHtml(__('Delete')) ?></span> <?php endif; ?> @@ -38,28 +45,36 @@ "fieldNameAction":"<?= /* @noEscape */ $_fieldNameAction ?>", "changeFileSelector":"#change-<?= /* @noEscape */ $_fileName ?>", "deleteFileSelector":"#delete-<?= /* @noEscape */ $_fileName ?>"} - }' - <?= $_fileExists ? 'style="display:none"' : '' ?>> + }'> <input type="file" name="<?= /* @noEscape */ $_fileName ?>" id="<?= /* @noEscape */ $_fileName ?>" class="product-custom-option<?= $_option->getIsRequire() ? ' required' : '' ?>" <?= $_fileExists ? 'disabled="disabled"' : '' ?> /> - <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" value="<?= /* @noEscape */ $_fieldValueAction ?>" /> - <?php if ($_option->getFileExtension()) :?> + <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" + value="<?= /* @noEscape */ $_fieldValueAction ?>" /> + <?php if ($_option->getFileExtension()):?> <p class="note"> - <?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong> + <?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: + <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong> </p> <?php endif; ?> - <?php if ($_option->getImageSizeX() > 0) :?> + <?php if ($_option->getImageSizeX() > 0):?> <p class="note"> - <?= $block->escapeHtml(__('Maximum image width')) ?>: <strong><?= (int)$_option->getImageSizeX() ?> <?= $block->escapeHtml(__('px.')) ?></strong> + <?= $block->escapeHtml(__('Maximum image width')) ?>: <strong><?= (int)$_option->getImageSizeX() + ?> <?= $block->escapeHtml(__('px.')) ?></strong> </p> <?php endif; ?> - <?php if ($_option->getImageSizeY() > 0) :?> + <?php if ($_option->getImageSizeY() > 0):?> <p class="note"> - <?= $block->escapeHtml(__('Maximum image height')) ?>: <strong><?= (int)$_option->getImageSizeY() ?> <?= $block->escapeHtml(__('px.')) ?></strong> + <?= $block->escapeHtml(__('Maximum image height')) ?>: <strong><?= (int)$_option->getImageSizeY() + ?> <?= $block->escapeHtml(__('px.')) ?></strong> </p> <?php endif; ?> </div> + <?= $_fileExists ? + /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + '#input-box-' . /* @noEscape */ $_fileName + ) : '' ?> </div> 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/Catalog/view/frontend/web/js/product/list/toolbar.js b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js index dfc0b4291cd6e..013732ca57875 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js @@ -35,10 +35,26 @@ define([ /** @inheritdoc */ _create: function () { - this._bind($(this.options.modeControl), this.options.mode, this.options.modeDefault); - this._bind($(this.options.directionControl), this.options.direction, this.options.directionDefault); - this._bind($(this.options.orderControl), this.options.order, this.options.orderDefault); - this._bind($(this.options.limitControl), this.options.limit, this.options.limitDefault); + this._bind( + $(this.options.modeControl, this.element), + this.options.mode, + this.options.modeDefault + ); + this._bind( + $(this.options.directionControl, this.element), + this.options.direction, + this.options.directionDefault + ); + this._bind( + $(this.options.orderControl, this.element), + this.options.order, + this.options.orderDefault + ); + this._bind( + $(this.options.limitControl, this.element), + this.options.limit, + this.options.limitDefault + ); }, /** @inheritdoc */ diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php index 4e75139c1a882..e78224ba0af38 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php @@ -12,6 +12,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\Tiers; use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\TiersFactory; use Magento\CatalogCustomerGraphQl\Model\Resolver\Customer\GetCustomerGroup; @@ -60,25 +61,33 @@ class PriceTiers implements ResolverInterface */ private $priceProviderPool; + /** + * @var PriceCurrencyInterface + */ + private $priceCurrency; + /** * @param ValueFactory $valueFactory * @param TiersFactory $tiersFactory * @param GetCustomerGroup $getCustomerGroup * @param Discount $discount * @param PriceProviderPool $priceProviderPool + * @param PriceCurrencyInterface $priceCurrency */ public function __construct( ValueFactory $valueFactory, TiersFactory $tiersFactory, GetCustomerGroup $getCustomerGroup, Discount $discount, - PriceProviderPool $priceProviderPool + PriceProviderPool $priceProviderPool, + PriceCurrencyInterface $priceCurrency ) { $this->valueFactory = $valueFactory; $this->tiersFactory = $tiersFactory; $this->getCustomerGroup = $getCustomerGroup; $this->discount = $discount; $this->priceProviderPool = $priceProviderPool; + $this->priceCurrency = $priceCurrency; } /** @@ -130,6 +139,7 @@ private function formatProductTierPrices(array $tierPrices, float $productPrice, $tiers = []; foreach ($tierPrices as $tierPrice) { + $tierPrice->setValue($this->priceCurrency->convertAndRound($tierPrice->getValue())); $percentValue = $tierPrice->getExtensionAttributes()->getPercentageValue(); if ($percentValue && is_numeric($percentValue)) { $discount = $this->discount->getDiscountByPercent($productPrice, (float)$percentValue); diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php index 320e0adc29b9f..140659abfbfe6 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -8,6 +8,7 @@ namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation; use Magento\Framework\App\ResourceConnection; +use Magento\Store\Model\Store; /** * Fetch product attribute option data including attribute info @@ -41,16 +42,18 @@ public function __construct(ResourceConnection $resourceConnection) * Get option data. Return list of attributes with option data * * @param array $optionIds + * @param int|null $storeId * @param array $attributeCodes * @return array * @throws \Zend_Db_Statement_Exception */ - public function getOptions(array $optionIds, array $attributeCodes = []): array + public function getOptions(array $optionIds, ?int $storeId, array $attributeCodes = []): array { if (!$optionIds) { return []; } + $storeId = $storeId ?: Store::DEFAULT_STORE_ID; $connection = $this->resourceConnection->getConnection(); $select = $connection->select() ->from( @@ -70,9 +73,21 @@ public function getOptions(array $optionIds, array $attributeCodes = []): array ['option_value' => $this->resourceConnection->getTableName('eav_attribute_option_value')], 'options.option_id = option_value.option_id', [ - 'option_label' => 'option_value.value', 'option_id' => 'option_value.option_id', ] + )->joinLeft( + ['option_value_store' => $this->resourceConnection->getTableName('eav_attribute_option_value')], + "options.option_id = option_value_store.option_id AND option_value_store.store_id = {$storeId}", + [ + 'option_label' => $connection->getCheckSql( + 'option_value_store.value_id > 0', + 'option_value_store.value', + 'option_value.value' + ) + ] + )->where( + 'a.attribute_id = options.attribute_id AND option_value.store_id = ?', + Store::DEFAULT_STORE_ID ); $select->where('option_value.option_id IN (?)', $optionIds); diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php index 0ec65c88024f2..105e91320de49 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -71,7 +71,7 @@ public function __construct( */ public function build(AggregationInterface $aggregation, ?int $storeId): array { - $attributeOptions = $this->getAttributeOptions($aggregation); + $attributeOptions = $this->getAttributeOptions($aggregation, $storeId); // build layer per attribute $result = []; @@ -133,10 +133,11 @@ private function isBucketEmpty(?BucketInterface $bucket): bool * Get list of attributes with options * * @param AggregationInterface $aggregation + * @param int|null $storeId * @return array * @throws \Zend_Db_Statement_Exception */ - private function getAttributeOptions(AggregationInterface $aggregation): array + private function getAttributeOptions(AggregationInterface $aggregation, ?int $storeId): array { $attributeOptionIds = []; $attributes = []; @@ -154,6 +155,6 @@ function (AggregationValueInterface $value) { return []; } - return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $attributes); + return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $storeId, $attributes); } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index 1057d21283ea8..b6837b334fdd8 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -101,9 +101,10 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte } if (!$searchCriteria->getSortOrders()) { - $this->addDefaultSortOrder($searchCriteria, $isSearch); + $this->addDefaultSortOrder($searchCriteria, $args, $isSearch); } + $this->addEntityIdSort($searchCriteria, $isSearch); $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter'])); $searchCriteria->setCurrentPage($args['currentPage']); @@ -132,6 +133,25 @@ private function addVisibilityFilter(SearchCriteriaInterface $searchCriteria, bo $this->addFilter($searchCriteria, 'visibility', $visibilityIds, 'in'); } + /** + * Add sort by Entity ID + * + * @param SearchCriteriaInterface $searchCriteria + * @param bool $isSearch + */ + private function addEntityIdSort(SearchCriteriaInterface $searchCriteria, bool $isSearch): void + { + if ($isSearch) { + return; + } + $sortOrderArray = $searchCriteria->getSortOrders(); + $sortOrderArray[] = $this->sortOrderBuilder + ->setField('_id') + ->setDirection(SortOrder::SORT_DESC) + ->create(); + $searchCriteria->setSortOrders($sortOrderArray); + } + /** * Prepare price aggregation algorithm * @@ -179,18 +199,32 @@ private function addFilter( * Sort by relevance DESC by default * * @param SearchCriteriaInterface $searchCriteria + * @param array $args * @param bool $isSearch */ - private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, $isSearch = false): void + private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, array $args, $isSearch = false): void { - $sortField = $isSearch ? 'relevance' : EavAttributeInterface::POSITION; - $sortDirection = $isSearch ? SortOrder::SORT_DESC : SortOrder::SORT_ASC; - $defaultSortOrder = $this->sortOrderBuilder - ->setField($sortField) - ->setDirection($sortDirection) - ->create(); + $defaultSortOrder = []; + if ($isSearch) { + $defaultSortOrder[] = $this->sortOrderBuilder + ->setField('relevance') + ->setDirection(SortOrder::SORT_DESC) + ->create(); + } else { + $categoryIdFilter = isset($args['filter']['category_id']) ? $args['filter']['category_id'] : false; + if ($categoryIdFilter) { + if (!is_array($categoryIdFilter[array_key_first($categoryIdFilter)]) + || count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1 + ) { + $defaultSortOrder[] = $this->sortOrderBuilder + ->setField(EavAttributeInterface::POSITION) + ->setDirection(SortOrder::SORT_ASC) + ->create(); + } + } + } - $searchCriteria->setSortOrders([$defaultSortOrder]); + $searchCriteria->setSortOrders($defaultSortOrder); } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index 69592657241a0..0bfd9d58ec969 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -8,7 +8,10 @@ namespace Magento\CatalogGraphQl\Model; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\NodeKind; use Magento\Eav\Model\Entity\Collection\AbstractCollection; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Joins attributes for provided field node field names. @@ -43,11 +46,12 @@ public function __construct(array $fieldToAttributeMap = []) * * @param FieldNode $fieldNode * @param AbstractCollection $collection + * @param ResolveInfo $resolveInfo * @return void */ - public function join(FieldNode $fieldNode, AbstractCollection $collection): void + public function join(FieldNode $fieldNode, AbstractCollection $collection, ResolveInfo $resolveInfo): void { - foreach ($this->getQueryFields($fieldNode) as $field) { + foreach ($this->getQueryFields($fieldNode, $resolveInfo) as $field) { $this->addFieldToCollection($collection, $field); } } @@ -56,26 +60,70 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection): void * Get an array of queried fields. * * @param FieldNode $fieldNode + * @param ResolveInfo $resolveInfo * @return string[] */ - public function getQueryFields(FieldNode $fieldNode): array + public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): array { if (null === $this->getFieldNodeSelections($fieldNode)) { $query = $fieldNode->selectionSet->selections; $selectedFields = []; + $fragmentFields = []; /** @var FieldNode $field */ foreach ($query as $field) { - if ($field->kind === 'InlineFragment') { - continue; + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field); + } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD && + ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) { + + foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) { + if (isset($spreadNode->selectionSet->selections)) { + $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo); + } else { + $selectedFields[] = $spreadNode->name->value; + } + } + } else { + $selectedFields[] = $field->name->value; } - $selectedFields[] = $field->name->value; } - $this->setSelectionsForFieldNode($fieldNode, $selectedFields); + if ($fragmentFields) { + $selectedFields = array_merge($selectedFields, array_merge(...$fragmentFields)); + } + $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields)); } return $this->getFieldNodeSelections($fieldNode); } + /** + * Add fields from inline fragment nodes + * + * @param ResolveInfo $resolveInfo + * @param InlineFragmentNode $inlineFragmentField + * @param array $inlineFragmentFields + * @return string[] + */ + private function addInlineFragmentFields( + ResolveInfo $resolveInfo, + InlineFragmentNode $inlineFragmentField, + $inlineFragmentFields = [] + ): array { + $query = $inlineFragmentField->selectionSet->selections; + /** @var FieldNode $field */ + foreach ($query as $field) { + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $this->addInlineFragmentFields($resolveInfo, $field, $inlineFragmentFields); + } elseif (isset($field->selectionSet->selections)) { + continue; + } else { + $inlineFragmentFields[] = $field->name->value; + } + } + + return array_unique($inlineFragmentFields); + } + /** * Add field to collection select * diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php index 1fae247c981d2..dc93005983776 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php @@ -13,6 +13,7 @@ use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Filter; +use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Sort; use Magento\Search\Model\Query; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\ScopeInterface; @@ -71,6 +72,7 @@ public function getResult(array $criteria, StoreInterface $store) $categoryIds = []; $criteria[Filter::ARGUMENT_NAME] = $this->formatMatchFilters($criteria['filters'], $store); $criteria[Filter::ARGUMENT_NAME][CategoryInterface::KEY_IS_ACTIVE] = ['eq' => 1]; + $criteria[Sort::ARGUMENT_NAME][CategoryInterface::KEY_POSITION] = ['ASC']; $searchCriteria = $this->searchCriteriaBuilder->build('categoryList', $criteria); $pageSize = $criteria['pageSize'] ?? 20; $currentPage = $criteria['currentPage'] ?? 1; diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php index b5d02511da4e7..ab100c7272ba0 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php @@ -8,6 +8,9 @@ namespace Magento\CatalogGraphQl\Model\Category; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\NodeKind; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Used for determining the depth information for a requested category tree in a GraphQL request @@ -17,22 +20,57 @@ class DepthCalculator /** * Calculate the total depth of a category tree inside a GraphQL request * + * @param ResolveInfo $resolveInfo * @param FieldNode $fieldNode * @return int */ - public function calculate(FieldNode $fieldNode) : int + public function calculate(ResolveInfo $resolveInfo, FieldNode $fieldNode) : int { $selections = $fieldNode->selectionSet->selections ?? []; $depth = count($selections) ? 1 : 0; $childrenDepth = [0]; foreach ($selections as $node) { - if ($node->kind === 'InlineFragment' || null !== $node->alias) { + if (isset($node->alias) && null !== $node->alias) { continue; } - $childrenDepth[] = $this->calculate($node); + if ($node->kind === NodeKind::INLINE_FRAGMENT) { + $childrenDepth[] = $this->addInlineFragmentDepth($resolveInfo, $node); + } elseif ($node->kind === NodeKind::FRAGMENT_SPREAD && isset($resolveInfo->fragments[$node->name->value])) { + foreach ($resolveInfo->fragments[$node->name->value]->selectionSet->selections as $spreadNode) { + $childrenDepth[] = $this->calculate($resolveInfo, $spreadNode); + } + } else { + $childrenDepth[] = $this->calculate($resolveInfo, $node); + } } return $depth + max($childrenDepth); } + + /** + * Add inline fragment fields into calculating of category depth + * + * @param ResolveInfo $resolveInfo + * @param InlineFragmentNode $inlineFragmentField + * @param array $depth + * @return int + */ + private function addInlineFragmentDepth( + ResolveInfo $resolveInfo, + InlineFragmentNode $inlineFragmentField, + $depth = [] + ): int { + $selections = $inlineFragmentField->selectionSet->selections; + /** @var FieldNode $field */ + foreach ($selections as $field) { + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $depth[] = $this->addInlineFragmentDepth($resolveInfo, $field, $depth); + } elseif ($field->selectionSet && $field->selectionSet->selections) { + $depth[] = $this->calculate($resolveInfo, $field); + } + } + + return $depth ? max($depth) : 0; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php index 5a230ceed0ca4..c6de07bdedd19 100644 --- a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php +++ b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php @@ -10,7 +10,7 @@ use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class ProductLinksTypeResolver implements TypeResolverInterface { @@ -20,9 +20,9 @@ class ProductLinksTypeResolver implements TypeResolverInterface private $linkTypes = ['related', 'upsell', 'crosssell']; /** - * {@inheritdoc} + * @inheritdoc */ - public function resolveType(array $data) : string + public function resolveType(array $data): string { if (isset($data['link_type'])) { $linkType = $data['link_type']; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php index 535fe3a80cd25..d7118d71db89b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php @@ -7,18 +7,18 @@ namespace Magento\CatalogGraphQl\Model\Resolver; -use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; +use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; +use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; -use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Store\Model\StoreManagerInterface; /** @@ -121,7 +121,7 @@ function () use ($that, $categoryIds, $info) { } if (!$this->collection->isLoaded()) { - $that->attributesJoiner->join($info->fieldNodes[0], $this->collection); + $that->attributesJoiner->join($info->fieldNodes[0], $this->collection, $info); $this->collection->addIdFilter($this->categoryIds); } /** @var CategoryInterface | \Magento\Catalog\Model\Category $item */ @@ -130,7 +130,7 @@ function () use ($that, $categoryIds, $info) { // Try to extract all requested fields from the loaded collection data $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true); $categories[$item->getId()]['model'] = $item; - $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0]); + $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0], $info); $extractedFields = array_keys($categories[$item->getId()]); $foundFields = array_intersect($requestedFields, $extractedFields); if (count($requestedFields) === count($foundFields)) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php index 85b86f313de4d..b966fce43f56d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -63,7 +63,7 @@ public function resolve( 'eq' => $value['id'] ] ]; - $searchResult = $this->searchQuery->getResult($args, $info); + $searchResult = $this->searchQuery->getResult($args, $info, $context); //possible division by 0 if ($searchResult->getPageSize()) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php index 14732ecf37c63..187fd05c1001e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php @@ -22,7 +22,15 @@ class BatchProductLinks implements BatchServiceContractResolverInterface /** * @var string[] */ - private static $linkTypes = ['related', 'upsell', 'crosssell']; + private $linkTypes; + + /** + * @param array $linkTypes + */ + public function __construct(array $linkTypes) + { + $this->linkTypes = $linkTypes; + } /** * @inheritDoc @@ -44,7 +52,7 @@ public function convertToServiceArgument(ResolveRequestInterface $request) /** @var \Magento\Catalog\Model\Product $product */ $product = $value['model']; - return new ListCriteria((string)$product->getId(), self::$linkTypes, $product); + return new ListCriteria((string)$product->getId(), $this->linkTypes, $product); } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableEnteredOptionValueUid.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableEnteredOptionValueUid.php new file mode 100644 index 0000000000000..2d6975bb8a4e2 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableEnteredOptionValueUid.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Format new option uid in base64 encode for entered custom options + */ +class CustomizableEnteredOptionValueUid implements ResolverInterface +{ + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + + /** + * Create a option uid for entered option in "<option-type>/<option-id>" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (isset($value['uid'])) { + return $value['uid']; + } + if (!isset($value['option_id']) || empty($value['option_id'])) { + throw new GraphQlInputException(__('"option_id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['option_id'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableSelectedOptionValueUid.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableSelectedOptionValueUid.php new file mode 100644 index 0000000000000..795782d6e3718 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableSelectedOptionValueUid.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Format new option uid in base64 encode for selected custom options + */ +class CustomizableSelectedOptionValueUid implements ResolverInterface +{ + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + + /** + * Create a option uid for selected option in "<option-type>/<option-id>/<option-value-id>" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (isset($value['uid'])) { + return $value['uid']; + } + if (!isset($value['option_id']) || empty($value['option_id'])) { + throw new GraphQlInputException(__('"option_id" value should be specified.')); + } + + if (!isset($value['option_type_id']) || empty($value['option_type_id'])) { + throw new GraphQlInputException(__('"option_type_id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['option_id'], + $value['option_type_id'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php index e1338930afe5d..8843ad02320c6 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php @@ -18,7 +18,7 @@ * @inheritdoc * * Format a product's media gallery information to conform to GraphQL schema representation - * @deprecated + * @deprecated 100.3.3 */ class MediaGalleryEntries implements ResolverInterface { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php index 9ddad4e6451fa..3139c35774008 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use GraphQL\Language\AST\NodeKind; use Magento\Framework\GraphQl\Query\FieldTranslator; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -43,9 +44,9 @@ public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeN continue; } foreach ($node->selectionSet->selections as $selectionNode) { - if ($selectionNode->kind === 'InlineFragment') { + if ($selectionNode->kind === NodeKind::INLINE_FRAGMENT) { foreach ($selectionNode->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === 'InlineFragment') { + if ($inlineSelection->kind === NodeKind::INLINE_FRAGMENT) { continue; } $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php new file mode 100644 index 0000000000000..1b42b0fde2bcb --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Pricing\Price\SpecialPrice as PricingSpecialPrice; + +/** + * Resolver for Special Price + */ +class SpecialPrice implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + /** @var ProductInterface $product */ + $product = $value['model']; + /** @var PricingSpecialPrice $specialPrice */ + $specialPrice = $product->getPriceInfo()->getPrice(PricingSpecialPrice::PRICE_CODE); + + if ($specialPrice->getValue()) { + return $specialPrice->getValue(); + } + + return null; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index e3d9ba2a9b3c6..1a244b8a10546 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -69,7 +69,7 @@ public function resolve( ); } - $searchResult = $this->searchQuery->getResult($args, $info); + $searchResult = $this->searchQuery->getResult($args, $info, $context); if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) { throw new GraphQlInputException( diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php index fc5a563c82b4e..c553d4486f9e9 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php @@ -8,15 +8,16 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; use GraphQL\Language\AST\FieldNode; -use Magento\CatalogGraphQl\Model\Category\DepthCalculator; -use Magento\CatalogGraphQl\Model\Category\LevelCalculator; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use GraphQL\Language\AST\NodeKind; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; -use Magento\Catalog\Model\Category; +use Magento\CatalogGraphQl\Model\Category\DepthCalculator; +use Magento\CatalogGraphQl\Model\Category\LevelCalculator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Category tree data provider @@ -85,8 +86,8 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato { $categoryQuery = $resolveInfo->fieldNodes[0]; $collection = $this->collectionFactory->create(); - $this->joinAttributesRecursively($collection, $categoryQuery); - $depth = $this->depthCalculator->calculate($categoryQuery); + $this->joinAttributesRecursively($collection, $categoryQuery, $resolveInfo); + $depth = $this->depthCalculator->calculate($resolveInfo, $categoryQuery); $level = $this->levelCalculator->calculate($rootCategoryId); // If root category is being filter, we've to remove first slash @@ -124,24 +125,27 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato * * @param Collection $collection * @param FieldNode $fieldNode + * @param ResolveInfo $resolveInfo * @return void */ - private function joinAttributesRecursively(Collection $collection, FieldNode $fieldNode) : void - { + private function joinAttributesRecursively( + Collection $collection, + FieldNode $fieldNode, + ResolveInfo $resolveInfo + ): void { if (!isset($fieldNode->selectionSet->selections)) { return; } $subSelection = $fieldNode->selectionSet->selections; - $this->attributesJoiner->join($fieldNode, $collection); + $this->attributesJoiner->join($fieldNode, $collection, $resolveInfo); /** @var FieldNode $node */ foreach ($subSelection as $node) { - if ($node->kind === 'InlineFragment') { + if ($node->kind === NodeKind::INLINE_FRAGMENT || $node->kind === NodeKind::FRAGMENT_SPREAD) { continue; } - - $this->joinAttributesRecursively($collection, $node); + $this->joinAttributesRecursively($collection, $node, $resolveInfo); } } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php index 86616cc14fe50..22bbc991a78e2 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductDataProvider; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Deferred resolver for product data. diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php index 2076ec6726988..3e955ae303453 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php @@ -14,6 +14,7 @@ use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; use Magento\Framework\Api\SearchResultsInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Product field data provider, used for GraphQL resolver processing. @@ -73,18 +74,20 @@ public function __construct( * @param string[] $attributes * @param bool $isSearch * @param bool $isChildSearch + * @param ContextInterface|null $context * @return SearchResultsInterface */ public function getList( SearchCriteriaInterface $searchCriteria, array $attributes = [], bool $isSearch = false, - bool $isChildSearch = false + bool $isChildSearch = false, + ContextInterface $context = null ): SearchResultsInterface { /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ $collection = $this->collectionFactory->create(); - $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes); + $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes, $context); if (!$isChildSearch) { $visibilityIds = $isSearch diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php index fef224b12acfc..abed0ed2a897d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Adds passed in attributes to product collection results @@ -34,12 +35,20 @@ public function __construct($fieldToAttributeMap = []) } /** - * @inheritdoc + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { foreach ($attributeNames as $name) { $this->addAttribute($collection, $name); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/ExtensibleEntityProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/ExtensibleEntityProcessor.php index 5fff991c0d6cd..3c19965c5f7b5 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/ExtensibleEntityProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/ExtensibleEntityProcessor.php @@ -11,6 +11,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Add necessary joins for extensible entities. @@ -33,16 +34,20 @@ public function __construct(JoinProcessorInterface $joinProcessor) } /** + * Process collection to add additional joins, attributes, and clauses to a product collection. + * * @param Collection $collection * @param SearchCriteriaInterface $searchCriteria * @param array $attributeNames + * @param ContextInterface|null $context * @return Collection * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { $this->joinProcessor->process($collection); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/MediaGalleryProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/MediaGalleryProcessor.php index be300e11f12ec..b636bcb001a3b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/MediaGalleryProcessor.php @@ -11,6 +11,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Catalog\Model\Product\Media\Config as MediaConfig; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Add attributes required for every GraphQL product resolution process. @@ -35,12 +36,20 @@ public function __construct(MediaConfig $mediaConfig) } /** - * @inheritdoc + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { if (in_array('media_gallery_entries', $attributeNames)) { $mediaAttributes = $this->mediaConfig->getMediaAttributeCodes(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php index 4c5b657874713..b545047d01541 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Add attributes required for every GraphQL product resolution process. @@ -19,12 +20,20 @@ class RequiredColumnsProcessor implements CollectionProcessorInterface { /** - * {@inheritdoc} + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { $collection->addAttributeToSelect('special_price'); $collection->addAttributeToSelect('special_price_from'); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/SearchCriteriaProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/SearchCriteriaProcessor.php index e4c338f599577..45df0d3343c11 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/SearchCriteriaProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/SearchCriteriaProcessor.php @@ -11,6 +11,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface as SearchCriteriaApplier; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Apply search criteria data to passed in collection. @@ -33,12 +34,20 @@ public function __construct(SearchCriteriaApplier $searchCriteriaApplier) } /** - * {@inheritdoc} + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { $this->searchCriteriaApplier->process($searchCriteria, $collection); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/StockProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/StockProcessor.php index e68136f64e5cf..61085c10a7335 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/StockProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/StockProcessor.php @@ -12,6 +12,7 @@ use Magento\Framework\Api\SearchCriteriaInterface; use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Model\ResourceModel\Stock\Status as StockStatusResource; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Add stock filtering if configuration requires it. @@ -41,12 +42,20 @@ public function __construct(StockConfigurationInterface $stockConfig, StockStatu } /** - * {@inheritdoc} + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { if (!$this->stockConfig->isShowOutOfStock()) { $this->stockStatusResource->addIsInStockFilterToCollection($collection); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/VisibilityStatusProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/VisibilityStatusProcessor.php index 30174a94aaba0..964edc9d5a0ad 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/VisibilityStatusProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/VisibilityStatusProcessor.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Join visibility and status tables to product collection @@ -19,12 +20,20 @@ class VisibilityStatusProcessor implements CollectionProcessorInterface { /** - * {@inheritdoc} + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { $collection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner'); $collection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner'); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessorInterface.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessorInterface.php index 62501a1a2382b..18e249ff23ac7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessorInterface.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessorInterface.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Add additional joins, attributes, and clauses to a product collection. @@ -21,11 +22,13 @@ interface CollectionProcessorInterface * @param Collection $collection * @param SearchCriteriaInterface $searchCriteria * @param array $attributeNames + * @param ContextInterface|null $context * @return Collection */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionProcessor.php index 687899c1e60ac..415dbf565a0b7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionProcessor.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * {@inheritdoc} @@ -29,15 +30,22 @@ public function __construct(array $collectionProcessors = []) } /** - * {@inheritdoc} + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { foreach ($this->collectionProcessors as $collectionProcessor) { - $collection = $collectionProcessor->process($collection, $searchCriteria, $attributeNames); + $collection = $collectionProcessor->process($collection, $searchCriteria, $attributeNames, $context); } return $collection; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index 4c83afb89cc46..4807cad54bd50 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -18,6 +18,7 @@ use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchResultsInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Product field data provider for product search, used for GraphQL resolver processing. @@ -84,12 +85,14 @@ public function __construct( * @param SearchCriteriaInterface $searchCriteria * @param SearchResultInterface $searchResult * @param array $attributes + * @param ContextInterface|null $context * @return SearchResultsInterface */ public function getList( SearchCriteriaInterface $searchCriteria, SearchResultInterface $searchResult, - array $attributes = [] + array $attributes = [], + ContextInterface $context = null ): SearchResultsInterface { /** @var Collection $collection */ $collection = $this->collectionFactory->create(); @@ -103,7 +106,7 @@ public function getList( $this->getSortOrderArray($searchCriteriaForCollection) )->apply(); - $this->collectionPreProcessor->process($collection, $searchCriteriaForCollection, $attributes); + $this->collectionPreProcessor->process($collection, $searchCriteriaForCollection, $attributes, $context); $collection->load(); $this->collectionPostProcessor->process($collection, $attributes); @@ -150,6 +153,12 @@ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) $sortOrders = $searchCriteria->getSortOrders(); if (is_array($sortOrders)) { foreach ($sortOrders as $sortOrder) { + // I am replacing _id with entity_id because in ElasticSearch _id is required for sorting by ID. + // Where as entity_id is required when using ID as the sort in $collection->load();. + // @see \Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search::getResult + if ($sortOrder->getField() === '_id') { + $sortOrder->setField('entity_id'); + } $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php index 670eee9c4583e..d70a3aa7e63c3 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php @@ -7,7 +7,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; -use Magento\Catalog\Model\Layer\Resolver as LayerResolver; use Magento\Catalog\Model\Product; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Exception\InputException; @@ -16,6 +15,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductProvider; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; +use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Search\Model\Query; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Store\Model\ScopeInterface; @@ -35,11 +35,6 @@ class Filter implements ProductQueryInterface */ private $productDataProvider; - /** - * @var LayerResolver - */ - private $layerResolver; - /** * FieldSelection */ @@ -58,7 +53,6 @@ class Filter implements ProductQueryInterface /** * @param SearchResultFactory $searchResultFactory * @param ProductProvider $productDataProvider - * @param LayerResolver $layerResolver * @param FieldSelection $fieldSelection * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param ScopeConfigInterface $scopeConfig @@ -66,14 +60,12 @@ class Filter implements ProductQueryInterface public function __construct( SearchResultFactory $searchResultFactory, ProductProvider $productDataProvider, - LayerResolver $layerResolver, FieldSelection $fieldSelection, SearchCriteriaBuilder $searchCriteriaBuilder, ScopeConfigInterface $scopeConfig ) { $this->searchResultFactory = $searchResultFactory; $this->productDataProvider = $productDataProvider; - $this->layerResolver = $layerResolver; $this->fieldSelection = $fieldSelection; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->scopeConfig = $scopeConfig; @@ -84,16 +76,18 @@ public function __construct( * * @param array $args * @param ResolveInfo $info + * @param ContextInterface $context * @return SearchResult */ public function getResult( array $args, - ResolveInfo $info + ResolveInfo $info, + ContextInterface $context ): SearchResult { $fields = $this->fieldSelection->getProductsFieldSelection($info); try { $searchCriteria = $this->buildSearchCriteria($args, $info); - $searchResults = $this->productDataProvider->getList($searchCriteria, $fields); + $searchResults = $this->productDataProvider->getList($searchCriteria, $fields, false, false, $context); } catch (InputException $e) { return $this->createEmptyResult($args); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/ProductQueryInterface.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/ProductQueryInterface.php index 580af5d87be26..fca6f3d4f7770 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/ProductQueryInterface.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/ProductQueryInterface.php @@ -8,6 +8,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Search for products by criteria @@ -19,7 +20,8 @@ interface ProductQueryInterface * * @param array $args * @param ResolveInfo $info + * @param ContextInterface $context * @return SearchResult */ - public function getResult(array $args, ResolveInfo $info): SearchResult; + public function getResult(array $args, ResolveInfo $info, ContextInterface $context): SearchResult; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index fbb0e42f2afeb..4eb76fb5c2d5b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -12,7 +12,9 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; use Magento\Framework\Api\Search\SearchCriteriaInterface; +use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Search\Api\SearchInterface; use Magento\Search\Model\Search\PageSizeProvider; @@ -80,12 +82,14 @@ public function __construct( * * @param array $args * @param ResolveInfo $info + * @param ContextInterface $context * @return SearchResult - * @throws \Exception + * @throws InputException */ public function getResult( array $args, - ResolveInfo $info + ResolveInfo $info, + ContextInterface $context ): SearchResult { $queryFields = $this->fieldSelection->getProductsFieldSelection($info); $searchCriteria = $this->buildSearchCriteria($args, $info); @@ -101,7 +105,12 @@ public function getResult( //Address limitations of sort and pagination on search API apply original pagination from GQL query $searchCriteria->setPageSize($realPageSize); $searchCriteria->setCurrentPage($realCurrentPage); - $searchResults = $this->productsProvider->getList($searchCriteria, $itemsResults, $queryFields); + $searchResults = $this->productsProvider->getList( + $searchCriteria, + $itemsResults, + $queryFields, + $context + ); $totalPages = $realPageSize ? ((int)ceil($searchResults->getTotalCount() / $realPageSize)) : 0; diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index d6e9bfa3c0505..46d7454a6d7e2 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -11,10 +11,10 @@ "magento/module-store": "*", "magento/module-eav-graph-ql": "*", "magento/module-catalog-search": "*", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-graph-ql": "*" }, "suggest": { - "magento/module-graph-ql": "*", "magento/module-graph-ql-cache": "*", "magento/module-store-graph-ql": "*" }, diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index 5fec7bfd4fda7..03f9d7ad03f04 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -74,4 +74,14 @@ <preference type="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider" for="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface"/> <preference type="Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search" for="Magento\CatalogGraphQl\Model\Resolver\Products\Query\ProductQueryInterface"/> + + <type name="\Magento\CatalogGraphQl\Model\Resolver\Product\BatchProductLinks"> + <arguments> + <argument name="linkTypes" xsi:type="array"> + <item name="related" xsi:type="string">related</item> + <item name="upsell" xsi:type="string">upsell</item> + <item name="crosssell" xsi:type="string">crosssell</item> + </argument> + </arguments> + </type> </config> 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 a9720bf17445b..35067a6cb99af 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -88,7 +88,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") short_description: ComplexTextValue @doc(description: "A short description of the product. Its use depends on the theme.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") - special_price: Float @doc(description: "The discounted price of the product.") + special_price: Float @doc(description: "The discounted price of the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\SpecialPrice") special_from_date: String @doc(description: "The beginning date that a product has a special price.") special_to_date: String @doc(description: "The end date that a product has a special price.") attribute_set_id: Int @doc(description: "The attribute set assigned to the product.") @@ -132,6 +132,7 @@ type CustomizableAreaValue @doc(description: "CustomizableAreaValue defines the price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } type CategoryTree implements CategoryInterface @doc(description: "Category Tree implementation.") { @@ -153,6 +154,7 @@ type CustomizableDateValue @doc(description: "CustomizableDateValue defines the price: Float @doc(description: "The price assigned to this option.") price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } type CustomizableDropDownOption implements CustomizableOptionInterface @doc(description: "CustomizableDropDownOption contains information about a drop down menu that is defined as part of a customizable option.") { @@ -166,6 +168,7 @@ type CustomizableDropDownValue @doc(description: "CustomizableDropDownValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the option is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. } type CustomizableMultipleOption implements CustomizableOptionInterface @doc(description: "CustomizableMultipleOption contains information about a multiselect that is defined as part of a customizable option.") { @@ -179,6 +182,7 @@ type CustomizableMultipleValue @doc(description: "CustomizableMultipleValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the option is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type CustomizableFieldOption implements CustomizableOptionInterface @doc(description: "CustomizableFieldOption contains information about a text field that is defined as part of a customizable option.") { @@ -191,6 +195,7 @@ type CustomizableFieldValue @doc(description: "CustomizableFieldValue defines th price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } type CustomizableFileOption implements CustomizableOptionInterface @doc(description: "CustomizableFileOption contains information about a file picker that is defined as part of a customizable option.") { @@ -205,6 +210,7 @@ type CustomizableFileValue @doc(description: "CustomizableFileValue defines the file_extension: String @doc(description: "The file extension to accept.") image_size_x: Int @doc(description: "The maximum width of an image.") image_size_y: Int @doc(description: "The maximum height of an image.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } interface MediaGalleryInterface @doc(description: "Contains basic information about a product image or video.") @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\MediaGalleryTypeResolver") { @@ -274,6 +280,7 @@ type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines th sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the radio button is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. } type CustomizableCheckboxOption implements CustomizableOptionInterface @doc(description: "CustomizableCheckbbixOption contains information about a set of checkbox values that are defined as part of a customizable option.") { @@ -287,6 +294,7 @@ type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the checkbox value is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. } type VirtualProduct implements ProductInterface, CustomizableProductInterface @doc(description: "A virtual product is non-tangible product that does not require shipping and is not kept in inventory.") { @@ -484,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/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 530bf6b1a0057..bcd103c6d62ba 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -979,6 +979,7 @@ protected function getExportData() * * @return array Keys are product IDs, values arrays with keys as store IDs * and values as store-specific versions of Product entity. + * @since 100.2.1 */ protected function loadCollection(): array { @@ -1168,7 +1169,7 @@ protected function collectMultirawData() * @param \Magento\Catalog\Model\Product $item * @param int $storeId * @return bool - * @deprecated + * @deprecated 100.2.3 */ protected function hasMultiselectData($item, $storeId) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index c5fcac99767bd..74c6576e6bcdf 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -225,7 +225,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity /** * Links attribute name-to-link type ID. * - * @deprecated use DI for LinkProcessor class if you want to add additional types + * @deprecated 101.1.0 use DI for LinkProcessor class if you want to add additional types * * @var array */ @@ -554,7 +554,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity /** * @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory - * @deprecated this variable isn't used anymore. + * @deprecated 101.0.0 this variable isn't used anymore. */ protected $_stockResItemFac; @@ -618,7 +618,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity /** * @var array - * @deprecated 100.1.5 + * @deprecated 100.0.3 * @since 100.0.3 */ protected $productUrlKeys = []; @@ -969,6 +969,7 @@ public function getMultipleValueSeparator() * Return empty attribute value constant * * @return string + * @since 101.0.0 */ public function getEmptyAttributeValueConstant() { @@ -1289,7 +1290,7 @@ protected function _prepareRowForDb(array $rowData) * * Must be called after ALL products saving done. * - * @deprecated use linkProcessor Directly + * @deprecated 101.1.0 use linkProcessor Directly * * @return $this */ @@ -1473,7 +1474,7 @@ private function getNewSkuFieldsForSelect() * * @return void * @since 100.0.4 - * @deprecated + * @deprecated 100.2.3 */ protected function initMediaGalleryResources() { @@ -1595,6 +1596,7 @@ protected function _saveProducts() } $rowSku = $rowData[self::COL_SKU]; + $rowSkuNormalized = mb_strtolower($rowSku); if (null === $rowSku) { $this->getErrorAggregator()->addRowToSkip($rowNum); @@ -1604,9 +1606,9 @@ protected function _saveProducts() $storeId = !empty($rowData[self::COL_STORE]) ? $this->getStoreIdByCode($rowData[self::COL_STORE]) : Store::DEFAULT_STORE_ID; - $rowExistingImages = $existingImages[$storeId][$rowSku] ?? []; + $rowExistingImages = $existingImages[$storeId][$rowSkuNormalized] ?? []; $rowStoreMediaGalleryValues = $rowExistingImages; - $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSku] ?? []; + $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSkuNormalized] ?? []; if (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row @@ -1762,10 +1764,11 @@ protected function _saveProducts() continue; } - if (isset($rowExistingImages[$uploadedFile])) { - $currentFileData = $rowExistingImages[$uploadedFile]; + $uploadedFileNormalized = ltrim($uploadedFile, '/\\'); + if (isset($rowExistingImages[$uploadedFileNormalized])) { + $currentFileData = $rowExistingImages[$uploadedFileNormalized]; $currentFileData['store_id'] = $storeId; - $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFile]); + $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFileNormalized]); if (array_key_exists($uploadedFile, $imageHiddenStates) && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] ) { @@ -3077,6 +3080,9 @@ private function formatStockDataForRow(array $rowData): array ); if ($this->stockConfiguration->isQty($this->skuProcessor->getNewSku($sku)['type_id'])) { + if (isset($rowData['qty']) && $rowData['qty'] == 0) { + $row['is_in_stock'] = 0; + } $stockItemDo->setData($row); $row['is_in_stock'] = $row['is_in_stock'] ?? $this->stockStateProvider->verifyStock($stockItemDo); if ($this->stockStateProvider->verifyNotification($stockItemDo)) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php index a45338c391a58..22a83671f630a 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php @@ -89,7 +89,6 @@ public function saveLinks( $resource = $this->linkFactory->create(); $mainTable = $resource->getMainTable(); $positionAttrId = []; - $nextLinkId = $this->resourceHelper->getNextAutoincrement($mainTable); // pre-load 'position' attributes ID for each link type once foreach ($this->linkNameToId as $linkId) { @@ -103,6 +102,7 @@ public function saveLinks( $positionAttrId[$linkId] = $importEntity->getConnection()->fetchOne($select, $bind); } while ($bunch = $dataSourceModel->getNextBunch()) { + $nextLinkId = $this->resourceHelper->getNextAutoincrement($mainTable); $this->processLinkBunches($importEntity, $linkField, $bunch, $resource, $nextLinkId, $positionAttrId); } } @@ -110,7 +110,7 @@ public function saveLinks( /** * Add link types (exists for backwards compatibility) * - * @deprecated Use DI to inject to the constructor + * @deprecated 101.1.0 Use DI to inject to the constructor * @param array $nameToIds */ public function addNameToIds(array $nameToIds): void diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php index a94a87a44b32a..d4694b72ba64f 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -384,7 +384,9 @@ public function getExistingImages(array $bunch) foreach ($this->connection->fetchAll($select) as $image) { $storeId = $image['store_id']; unset($image['store_id']); - $result[$storeId][$image['sku']][$image['value']] = $image; + $sku = mb_strtolower($image['sku']); + $value = ltrim($image['value'], '/\\'); + $result[$storeId][$sku][$value] = $image; } return $result; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/StatusProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/StatusProcessor.php index 1c6d679848216..27869cf1d771b 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/StatusProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/StatusProcessor.php @@ -101,7 +101,8 @@ public function loadOldStatus(array $linkIdBySku): void $select = $connection->select() ->from($this->getAttribute()->getBackend()->getTable()) ->columns([$linkId, 'store_id', 'value']) - ->where(sprintf('%s IN (?)', $linkId), array_values($linkIdBySku)); + ->where(sprintf('%s IN (?)', $linkId), array_values($linkIdBySku)) + ->where('attribute_id = ?', $this->getAttribute()->getId()); $skuByLinkId = array_flip($linkIdBySku); foreach ($connection->fetchAll($select) as $item) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index d87c3d8477556..6571b16c87565 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -33,6 +33,7 @@ abstract class AbstractType * Maintain a list of invisible attributes * * @var array + * @since 100.2.5 */ public static $invAttributesCache = []; diff --git a/app/code/Magento/CatalogImportExport/Model/StockItemImporterInterface.php b/app/code/Magento/CatalogImportExport/Model/StockItemImporterInterface.php index bc314d825ba3e..6ee0e536c0ae8 100644 --- a/app/code/Magento/CatalogImportExport/Model/StockItemImporterInterface.php +++ b/app/code/Magento/CatalogImportExport/Model/StockItemImporterInterface.php @@ -11,6 +11,7 @@ * Interface StockItemImporterInterface * * @api + * @since 101.0.0 */ interface StockItemImporterInterface { @@ -22,6 +23,7 @@ interface StockItemImporterInterface * @throws \Magento\Framework\Exception\CouldNotSaveException * @throws \Magento\Framework\Exception\InputException * @throws \Magento\Framework\Validation\ValidationException + * @since 101.0.0 */ public function import(array $stockData); } diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml index bdb562ab0205d..785d19c000af0 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml @@ -79,14 +79,6 @@ <requiredEntity createDataKey="createBundleOptionWithAttribute"/> <requiredEntity createDataKey="secondSimpleProductForFixedWithAttribute"/> </createData> - - <!-- Start message queue for export consumer --> - <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> - <argument name="consumerName" value="{{AdminExportMessageConsumerData.consumerName}}"/> - <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> - </actionGroup> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitForPageLoaded"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -100,9 +92,8 @@ <deleteData createDataKey="firstSimpleProductForFixedWithAttribute" stepKey="deleteFirstSimpleProductForFixedWithAttribute"/> <deleteData createDataKey="secondSimpleProductForFixedWithAttribute" stepKey="deleteSecondSimpleProductForFixedWithAttribute"/> <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> - + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="cron:run --group=index" stepKey="reindexInvalidatedIndices"/> </after> <!-- Go to export page --> @@ -111,7 +102,12 @@ <!-- Export created below products --> <actionGroup ref="ExportAllProductsActionGroup" stepKey="exportCreatedProducts"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <!-- Start message queue for export consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminExportMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> + </actionGroup> + <reloadPage stepKey="refreshPage"/> <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml index 5ae94f050eb30..9f8d65968d741 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml @@ -55,7 +55,7 @@ <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> <!-- Log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -73,8 +73,7 @@ <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> </actionGroup> <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitForPageLoaded"/> - + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Download product --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml index e0dfb8250c738..f0e6e12204a9e 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml @@ -148,11 +148,11 @@ <deleteData createDataKey="createConfigChildProduct" stepKey="deleteConfigChildProduct"/> <deleteData createDataKey="createConfigProductAttr" stepKey="deleteConfigProductAttr"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> <!-- Admin logout--> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> <!-- Go to System > Export --> @@ -170,8 +170,7 @@ <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> </actionGroup> <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitForPageLoaded"/> - + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Save exported file: file successfully downloaded --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml index c82451eb9dbb5..94478e63aa92a 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml @@ -83,9 +83,8 @@ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Go to export page --> @@ -104,8 +103,7 @@ <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> </actionGroup> <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitForPageLoaded"/> - + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Download product --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml index dc556a6d0a899..95cfe2c87bffb 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml @@ -99,9 +99,8 @@ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Go to export page --> @@ -119,8 +118,7 @@ <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> </actionGroup> <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitForPageLoaded"/> - + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Download product --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml index 65f9ff80f7e39..2f57d94113d38 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml @@ -85,9 +85,8 @@ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Go to export page --> @@ -103,8 +102,7 @@ <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> </actionGroup> <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitForPageLoaded"/> - + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Download product --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml index e684f80d8bd05..dac97a61a967b 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml @@ -37,7 +37,7 @@ <deleteData createDataKey="createSimpleProductWithCustomAttributeSet" stepKey="deleteSimpleProductWithCustomAttributeSet"/> <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> <!-- Log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -55,7 +55,7 @@ <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> </actionGroup> <reloadPage stepKey="pageReload" /> - <waitForPageLoad stepKey="waitForPageLoaded" /> + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Download product --> diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Export/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Export/ProductTest.php index bf1d3772b92a0..1ad82497119ba 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Export/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Export/ProductTest.php @@ -8,12 +8,14 @@ namespace Magento\CatalogImportExport\Test\Unit\Model\Export; use Magento\Catalog\Model\Product\LinkTypeProvider; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\CatalogImportExport\Model\Export\Product; use Magento\CatalogImportExport\Model\Export\Product\Type\Factory; use Magento\CatalogImportExport\Model\Export\RowCustomizer\Composite; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Collection\AbstractCollection; use Magento\Eav\Model\Entity\Type; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory as AttributeSetCollectionFactory; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Logger\Monolog; @@ -83,7 +85,7 @@ class ProductTest extends TestCase protected $attrSetColFactory; /** - * @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory|MockObject + * @var CategoryCollectionFactory|MockObject */ protected $categoryColFactory; @@ -174,15 +176,14 @@ protected function setUp(): void ->onlyMethods(['create']) ->getMock(); - $this->attrSetColFactory = $this->getMockBuilder( - \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory::class - )->addMethods(['setEntityTypeFilter']) + $this->attrSetColFactory = $this->getMockBuilder(AttributeSetCollectionFactory::class) + ->disableOriginalConstructor() + ->addMethods(['setEntityTypeFilter']) ->onlyMethods(['create']) ->getMock(); - $this->categoryColFactory = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory::class - )->addMethods(['addNameToResult']) + $this->categoryColFactory = $this->getMockBuilder(CategoryCollectionFactory::class) + ->disableOriginalConstructor()->addMethods(['addNameToResult']) ->onlyMethods(['create']) ->getMock(); diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php index 9ae22e5e1a364..07b8429ddf188 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php @@ -16,7 +16,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php index 087fae6e6568a..581081f2924ea 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php index 59a1f58b74c2c..2d5f980a57039 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php @@ -16,7 +16,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php index e2e8a744c4bcd..759cc9883be9f 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php index 1cc045745a0c1..a7d70a943d405 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php index 66b639fb088d1..35e56b0e3e7bb 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php index 6fd1e7466970d..ddb3fce22a853 100644 --- a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php +++ b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php @@ -13,9 +13,10 @@ /** * @api * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html + * @since 100.3.0 */ interface RegisterProductSaleInterface { @@ -29,6 +30,7 @@ interface RegisterProductSaleInterface * @param int $websiteId * @return StockItemInterface[] * @throws LocalizedException + * @since 100.3.0 */ public function registerProductsSale($items, $websiteId = null); } diff --git a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php index 552e30da89235..83f7d73deaed9 100644 --- a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php +++ b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php @@ -10,9 +10,10 @@ /** * @api * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html + * @since 100.3.0 */ interface RevertProductSaleInterface { @@ -24,6 +25,7 @@ interface RevertProductSaleInterface * @param string[] $items * @param int $websiteId * @return bool + * @since 100.3.0 */ public function revertProductsSale($items, $websiteId = null); } diff --git a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php index 5019e86b7af40..ab52580988c5e 100644 --- a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php index eb6fb2e812f2e..92f2290ec08ad 100644 --- a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php index 18bab6571c209..24dbaf5bb6d5f 100644 --- a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php index 1d2cabbb48a11..b72289ee09278 100644 --- a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php index eecf6cbe07632..4269569f9da1a 100644 --- a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php index 8796953e32fd0..3c1c7ea137c89 100644 --- a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php index 5478f90fb7d9f..bab5f9b457c45 100644 --- a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php index 3cfdf45506340..a7d64ec9eedb3 100644 --- a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php index 8be7f5be79f27..d404e885d78df 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php index 99ad7005d9da4..be1c9642826a7 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php index d29171f557f05..91efd55761335 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php index ffcb758dcbd66..bc63114d99801 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php @@ -13,7 +13,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php index 5378801b6c24b..3c1a6e7982708 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php @@ -15,7 +15,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php index a12b72cd0a971..dd8c987fe5da4 100644 --- a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php +++ b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php @@ -15,7 +15,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php index 5a3a3ca6ee983..c19dc5fb34bf6 100644 --- a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php +++ b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Helper/Stock.php b/app/code/Magento/CatalogInventory/Helper/Stock.php index 798ac4074c188..87a0e3c32ad09 100644 --- a/app/code/Magento/CatalogInventory/Helper/Stock.php +++ b/app/code/Magento/CatalogInventory/Helper/Stock.php @@ -19,9 +19,10 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html + * @since 100.0.2 */ class Stock { diff --git a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php index 145b0d1454ae2..04e54acad5c0e 100644 --- a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php @@ -21,7 +21,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ 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/Indexer/Stock/Plugin/StoreGroup.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Plugin/StoreGroup.php index c171e9bda2612..29a8b1e5404fa 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Plugin/StoreGroup.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Plugin/StoreGroup.php @@ -3,19 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogInventory\Model\Indexer\Stock\Plugin; +use Magento\CatalogInventory\Model\Indexer\Stock\Processor; +use Magento\Framework\Model\AbstractModel; +use Magento\Store\Model\ResourceModel\Group; + class StoreGroup { /** - * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor + * @var Processor */ protected $_indexerProcessor; /** - * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $indexerProcessor + * @param Processor $indexerProcessor */ - public function __construct(\Magento\CatalogInventory\Model\Indexer\Stock\Processor $indexerProcessor) + public function __construct(Processor $indexerProcessor) { $this->_indexerProcessor = $indexerProcessor; } @@ -23,18 +28,19 @@ public function __construct(\Magento\CatalogInventory\Model\Indexer\Stock\Proces /** * Before save handler * - * @param \Magento\Store\Model\ResourceModel\Group $subject - * @param \Magento\Framework\Model\AbstractModel $object + * @param Group $subject + * @param Group $result + * @param AbstractModel $object * - * @return void + * @return Group * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeSave( - \Magento\Store\Model\ResourceModel\Group $subject, - \Magento\Framework\Model\AbstractModel $object - ) { - if (!$object->getId() || $object->dataHasChangedFor('website_id')) { + public function afterSave(Group $subject, Group $result, AbstractModel $object) + { + if ($object->isObjectNew() || $object->dataHasChangedFor('website_id')) { $this->_indexerProcessor->markIndexerAsInvalid(); } + + return $result; } } diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php b/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php deleted file mode 100644 index c061c459bfb49..0000000000000 --- a/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogInventory\Model\Plugin; - -use Magento\CatalogInventory\Model\ResourceModel\Stock\Item; -use Magento\Catalog\Model\Indexer\Product\Price\Processor; -use Magento\Framework\Model\AbstractModel; - -/** - * Update product price index after product stock status changed. - */ -class PriceIndexUpdater -{ - /** - * @var Processor - */ - private $priceIndexProcessor; - - /** - * @param Processor $priceIndexProcessor - */ - public function __construct(Processor $priceIndexProcessor) - { - $this->priceIndexProcessor = $priceIndexProcessor; - } - - /** - * @param Item $subject - * @param Item $result - * @param AbstractModel $model - * @return Item - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterSave(Item $subject, Item $result, AbstractModel $model): Item - { - $fields = [ - 'is_in_stock', - 'use_config_manage_stock', - 'manage_stock', - ]; - foreach ($fields as $field) { - if ($model->dataHasChangedFor($field)) { - $this->priceIndexProcessor->reindexRow($model->getProductId()); - break; - } - } - - return $result; - } - - /** - * @param Item $subject - * @param mixed $result - * @param int $websiteId - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterUpdateSetOutOfStock(Item $subject, $result, int $websiteId) - { - $this->priceIndexProcessor->markIndexerAsInvalid(); - } - - /** - * @param Item $subject - * @param mixed $result - * @param int $websiteId - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterUpdateSetInStock(Item $subject, $result, int $websiteId) - { - $this->priceIndexProcessor->markIndexerAsInvalid(); - } -} diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php index e67568b80898e..2ccb726f2c625 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php @@ -26,7 +26,7 @@ * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php index c5644060c689f..c151e5897abd5 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php @@ -19,7 +19,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php index 115002b237645..665ebf2db2f30 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php @@ -12,7 +12,7 @@ * @api * @since 100.1.0 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php index 24ed496372817..9a1945d5aefac 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php index 0ee162e429f40..49e4889c8edee 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php @@ -13,7 +13,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php index 31b2ada809823..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', @@ -223,7 +223,7 @@ protected function _initConfig() /** * Set items out of stock basing on their quantities and config settings * - * @deprecated + * @deprecated 100.2.5 * @see \Magento\CatalogInventory\Model\ResourceModel\Stock\Item::updateSetOutOfStock * @param string|int $website * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -260,7 +260,7 @@ public function updateSetOutOfStock($website = null) /** * Set items in stock basing on their quantities and config settings * - * @deprecated + * @deprecated 100.2.5 * @see \Magento\CatalogInventory\Model\ResourceModel\Stock\Item::updateSetInStock * @param int|string $website * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -295,7 +295,7 @@ public function updateSetInStock($website) /** * Update items low stock date basing on their quantities and config settings * - * @deprecated + * @deprecated 100.2.5 * @see \Magento\CatalogInventory\Model\ResourceModel\Stock\Item::updateLowStockDate * @param int|string $website * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php index edccad60231ec..dc9233e77d3a9 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php @@ -5,14 +5,13 @@ */ namespace Magento\CatalogInventory\Model\ResourceModel\Stock; -use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexProcessor; use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Model\Stock; use Magento\CatalogInventory\Model\Indexer\Stock\Processor; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Framework\DB\Select; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\DateTime\DateTime; /** @@ -42,27 +41,33 @@ class Item extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ private $dateTime; + /** + * @var PriceIndexProcessor + */ + private $priceIndexProcessor; + /** * @param Context $context * @param Processor $processor - * @param string $connectionName * @param StockConfigurationInterface $stockConfiguration * @param DateTime $dateTime + * @param PriceIndexProcessor $priceIndexProcessor + * @param string $connectionName */ public function __construct( Context $context, Processor $processor, - $connectionName = null, - StockConfigurationInterface $stockConfiguration = null, - DateTime $dateTime = null + StockConfigurationInterface $stockConfiguration, + DateTime $dateTime, + PriceIndexProcessor $priceIndexProcessor, + $connectionName = null ) { $this->stockIndexerProcessor = $processor; parent::__construct($context, $connectionName); - $this->stockConfiguration = $stockConfiguration ?? - ObjectManager::getInstance()->get(StockConfigurationInterface::class); - $this->dateTime = $dateTime ?? - ObjectManager::getInstance()->get(DateTime::class); + $this->stockConfiguration = $stockConfiguration; + $this->dateTime = $dateTime; + $this->priceIndexProcessor = $priceIndexProcessor; } /** @@ -144,10 +149,25 @@ protected function _prepareDataForTable(\Magento\Framework\DataObject $object, $ protected function _afterSave(AbstractModel $object) { parent::_afterSave($object); - /** @var StockItemInterface $object */ + + $productId = $object->getProductId(); if ($this->processIndexEvents) { - $this->stockIndexerProcessor->reindexRow($object->getProductId()); + $this->stockIndexerProcessor->reindexRow($productId); } + $fields = [ + 'is_in_stock', + 'use_config_manage_stock', + 'manage_stock', + ]; + foreach ($fields as $field) { + if ($object->dataHasChangedFor($field)) { + $this->addCommitCallback(function () use ($productId) { + $this->priceIndexProcessor->reindexRow($productId); + }); + break; + } + } + return $this; } @@ -196,6 +216,7 @@ public function updateSetOutOfStock(int $websiteId) $connection->update($this->getMainTable(), $values, $where); $this->stockIndexerProcessor->markIndexerAsInvalid(); + $this->priceIndexProcessor->markIndexerAsInvalid(); } /** @@ -228,6 +249,7 @@ public function updateSetInStock(int $websiteId) $connection->update($this->getMainTable(), $values, $where); $this->stockIndexerProcessor->markIndexerAsInvalid(); + $this->priceIndexProcessor->markIndexerAsInvalid(); } /** diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php index 402ce5f2f611e..02e443d09b228 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php @@ -5,25 +5,35 @@ */ 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 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html + * @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; @@ -31,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; @@ -46,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 ) { @@ -127,6 +137,7 @@ public function saveProductStatus( /** * Retrieve product status + * * Return array as key product id, value - stock status * * @param int[] $productIds @@ -150,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)); } @@ -185,6 +197,7 @@ public function getProductsType($productIds) /** * Retrieve Product part Collection array + * * Return array as key product id, value product type * * @param int $lastEntityId @@ -206,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( @@ -224,9 +237,12 @@ 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 + * @since 100.0.6 */ public function addStockDataToCollection($collection, $isFilterInStock) { @@ -287,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) @@ -301,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 @@ -313,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 { @@ -335,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/CatalogInventory/Model/ResourceModel/StockStatusFilter.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilter.php new file mode 100644 index 0000000000000..e9497a1d44861 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilter.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Model\ResourceModel; + +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Model\Stock; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; + +/** + * Generic in-stock status filter + */ +class StockStatusFilter implements StockStatusFilterInterface +{ + private const TABLE_NAME = 'cataloginventory_stock_status'; + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @param ResourceConnection $resource + * @param StockConfigurationInterface $stockConfiguration + */ + public function __construct( + ResourceConnection $resource, + StockConfigurationInterface $stockConfiguration + ) { + $this->resource = $resource; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * @inheritDoc + */ + public function execute( + Select $select, + string $productTableAlias, + string $stockStatusTableAlias = self::TABLE_ALIAS, + ?int $websiteId = null + ): Select { + $stockStatusTable = $this->resource->getTableName(self::TABLE_NAME); + $joinCondition = [ + "{$stockStatusTableAlias}.product_id = {$productTableAlias}.entity_id", + $select->getConnection()->quoteInto( + "{$stockStatusTableAlias}.website_id = ?", + $this->stockConfiguration->getDefaultScopeId() + ), + $select->getConnection()->quoteInto( + "{$stockStatusTableAlias}.stock_id = ?", + Stock::DEFAULT_STOCK_ID + ) + ]; + $select->join( + [$stockStatusTableAlias => $stockStatusTable], + implode(' AND ', $joinCondition), + [] + ); + $select->where("{$stockStatusTableAlias}.stock_status = ?", StockStatusInterface::STATUS_IN_STOCK); + + return $select; + } +} diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilterInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilterInterface.php new file mode 100644 index 0000000000000..26eb4b0fa38eb --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilterInterface.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Model\ResourceModel; + +use Magento\Framework\DB\Select; + +/** + * In stock status filter interface. + */ +interface StockStatusFilterInterface +{ + public const TABLE_ALIAS = 'stock_status'; + + /** + * Add in-stock status constraint to the select. + * + * @param Select $select + * @param string $productTableAliasAlias + * @param string $stockStatusTableAlias + * @param int|null $websiteId + * @return Select + */ + public function execute( + Select $select, + string $productTableAliasAlias, + string $stockStatusTableAlias = self::TABLE_ALIAS, + ?int $websiteId = null + ): Select; +} diff --git a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php index 0bffb9a9888cd..d28da4e5b3497 100644 --- a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php +++ b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/Source/Stock.php b/app/code/Magento/CatalogInventory/Model/Source/Stock.php index 9661fc83ce275..69e80658ecd74 100644 --- a/app/code/Magento/CatalogInventory/Model/Source/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/Source/Stock.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ @@ -38,6 +38,7 @@ public function getAllOptions() * @param string $dir * * @return $this + * @since 100.2.4 */ public function addValueSortToCollection($collection, $dir = \Magento\Framework\Data\Collection::SORT_ORDER_DESC) { diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php index 0fa4b919c40fa..b2dfe532ffbe0 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php @@ -8,7 +8,7 @@ /** * Interface StockRegistryProviderInterface * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.2 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php index 30f703b5b928f..5bb78e1489b39 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php @@ -10,7 +10,7 @@ /** * Interface StockStateProviderInterface * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.2 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/StockState.php b/app/code/Magento/CatalogInventory/Model/StockState.php index ac6dc16366798..e28a2096942d4 100644 --- a/app/code/Magento/CatalogInventory/Model/StockState.php +++ b/app/code/Magento/CatalogInventory/Model/StockState.php @@ -9,8 +9,11 @@ use Magento\CatalogInventory\Api\StockStateInterface; use Magento\CatalogInventory\Model\Spi\StockRegistryProviderInterface; use Magento\CatalogInventory\Model\Spi\StockStateProviderInterface; +use Magento\Framework\DataObject; /** + * Provides functionality for stock state information + * * Interface StockState */ class StockState implements StockStateInterface @@ -31,6 +34,8 @@ class StockState implements StockStateInterface protected $stockConfiguration; /** + * StockState constructor + * * @param StockStateProviderInterface $stockStateProvider * @param StockRegistryProviderInterface $stockRegistryProvider * @param StockConfigurationInterface $stockConfiguration @@ -46,30 +51,32 @@ public function __construct( } /** + * Verify stock by product id + * * @param int $productId * @param int $scopeId * @return bool */ public function verifyStock($productId, $scopeId = null) { - // if ($scopeId === null) { - $scopeId = $this->stockConfiguration->getDefaultScopeId(); - // } + $scopeId = $this->stockConfiguration->getDefaultScopeId(); $stockItem = $this->stockRegistryProvider->getStockItem($productId, $scopeId); + return $this->stockStateProvider->verifyStock($stockItem); } /** + * Verify notification by product id + * * @param int $productId * @param int $scopeId * @return bool */ public function verifyNotification($productId, $scopeId = null) { - // if ($scopeId === null) { - $scopeId = $this->stockConfiguration->getDefaultScopeId(); - // } + $scopeId = $this->stockConfiguration->getDefaultScopeId(); $stockItem = $this->stockRegistryProvider->getStockItem($productId, $scopeId); + return $this->stockStateProvider->verifyNotification($stockItem); } @@ -84,16 +91,14 @@ public function verifyNotification($productId, $scopeId = null) */ public function checkQty($productId, $qty, $scopeId = null) { - // if ($scopeId === null) { - $scopeId = $this->stockConfiguration->getDefaultScopeId(); - // } + $scopeId = $this->stockConfiguration->getDefaultScopeId(); $stockItem = $this->stockRegistryProvider->getStockItem($productId, $scopeId); + return $this->stockStateProvider->checkQty($stockItem, $qty); } /** - * Returns suggested qty that satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions - * or original qty if such value does not exist + * Returns suggested qty that satisfies qty increments/minQty/maxQty/minSaleQty/maxSaleQty else returns original qty * * @param int $productId * @param float $qty @@ -102,10 +107,9 @@ public function checkQty($productId, $qty, $scopeId = null) */ public function suggestQty($productId, $qty, $scopeId = null) { - // if ($scopeId === null) { - $scopeId = $this->stockConfiguration->getDefaultScopeId(); - // } + $scopeId = $this->stockConfiguration->getDefaultScopeId(); $stockItem = $this->stockRegistryProvider->getStockItem($productId, $scopeId); + return $this->stockStateProvider->suggestQty($stockItem, $qty); } @@ -118,29 +122,31 @@ public function suggestQty($productId, $qty, $scopeId = null) */ public function getStockQty($productId, $scopeId = null) { - // if ($scopeId === null) { - $scopeId = $this->stockConfiguration->getDefaultScopeId(); - // } + $scopeId = $this->stockConfiguration->getDefaultScopeId(); $stockItem = $this->stockRegistryProvider->getStockItem($productId, $scopeId); + return $this->stockStateProvider->getStockQty($stockItem); } /** + * Check qty increments by product id + * * @param int $productId * @param float $qty * @param int $websiteId - * @return \Magento\Framework\DataObject + * @return DataObject */ public function checkQtyIncrements($productId, $qty, $websiteId = null) { - // if ($websiteId === null) { - $websiteId = $this->stockConfiguration->getDefaultScopeId(); - // } + $websiteId = $this->stockConfiguration->getDefaultScopeId(); $stockItem = $this->stockRegistryProvider->getStockItem($productId, $websiteId); + return $this->stockStateProvider->checkQtyIncrements($stockItem, $qty); } /** + * Check quote item qty + * * @param int $productId * @param float $itemQty * @param float $qtyToCheck @@ -150,10 +156,9 @@ public function checkQtyIncrements($productId, $qty, $websiteId = null) */ public function checkQuoteItemQty($productId, $itemQty, $qtyToCheck, $origQty, $scopeId = null) { - // if ($scopeId === null) { - $scopeId = $this->stockConfiguration->getDefaultScopeId(); - // } + $scopeId = $this->stockConfiguration->getDefaultScopeId(); $stockItem = $this->stockRegistryProvider->getStockItem($productId, $scopeId); + return $this->stockStateProvider->checkQuoteItemQty($stockItem, $itemQty, $qtyToCheck, $origQty); } } diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml index 2cdb2413122bd..e7387ddd5d674 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml @@ -106,8 +106,7 @@ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToShoppingCartPage"/> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> @@ -116,20 +115,15 @@ <actionGroup ref="StorefrontSignOutActionGroup" stepKey="StorefrontSignOutActionGroup"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> <!-- Reset admin order filter --> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask5"/> <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="searchOrderNum"/> <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearch"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoice"/> - <waitForPageLoad stepKey="waitForNewInvoicePageToLoad"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForPageLoad stepKey="waitForNewInvoiceToBeCreated"/> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShip"/> <waitForLoadingMaskToDisappear stepKey="waitForShipLoadingMask"/> diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php index 9e2b7f29ce0fa..0e2b6b2f329c1 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php @@ -12,7 +12,6 @@ use Magento\CatalogInventory\Model\Indexer\Stock\Plugin\StoreGroup; use Magento\CatalogInventory\Model\Indexer\Stock\Processor; -use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Model\AbstractModel; use Magento\Store\Model\ResourceModel\Group; use PHPUnit\Framework\MockObject\MockObject; @@ -23,24 +22,24 @@ class StoreGroupTest extends TestCase /** * @var StoreGroup */ - protected $_model; + private $model; /** - * @var IndexerInterface|MockObject + * @var Processor|MockObject */ - protected $_indexerMock; + private $indexerProcessorMock; protected function setUp(): void { - $this->_indexerMock = $this->createMock(Processor::class); - $this->_model = new StoreGroup($this->_indexerMock); + $this->indexerProcessorMock = $this->createMock(Processor::class); + $this->model = new StoreGroup($this->indexerProcessorMock); } /** * @param array $data - * @dataProvider beforeSaveDataProvider + * @dataProvider afterSaveDataProvider */ - public function testBeforeSave(array $data) + public function testAfterSave(array $data): void { $subjectMock = $this->createMock(Group::class); $objectMock = $this->createPartialMock( @@ -55,16 +54,19 @@ public function testBeforeSave(array $data) ->with('website_id') ->willReturn($data['has_website_id_changed']); - $this->_indexerMock->expects($this->once()) + $this->indexerProcessorMock->expects($this->once()) ->method('markIndexerAsInvalid'); - $this->_model->beforeSave($subjectMock, $objectMock); + $this->assertSame( + $subjectMock, + $this->model->afterSave($subjectMock, $subjectMock, $objectMock) + ); } /** * @return array */ - public function beforeSaveDataProvider() + public function afterSaveDataProvider(): array { return [ [ diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index 78a0c2b734315..d2807249cf574 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -32,12 +32,13 @@ <preference for="Magento\CatalogInventory\Model\Spi\StockStateProviderInterface" type="Magento\CatalogInventory\Model\StockStateProvider" /> <preference for="Magento\CatalogInventory\Model\ResourceModel\QtyCounterInterface" type="Magento\CatalogInventory\Model\ResourceModel\Stock" /> + <preference for="Magento\CatalogInventory\Model\ResourceModel\StockStatusFilterInterface" type="Magento\CatalogInventory\Model\ResourceModel\StockStatusFilter" /> <type name="Magento\Catalog\Model\Product\Attribute\Repository"> <plugin name="filterCustomAttribute" type="Magento\CatalogInventory\Model\Plugin\FilterCustomAttribute" /> </type> <type name="Magento\Catalog\Model\FilterProductCustomAttribute"> <arguments> - <argument name="blackList" xsi:type="array"> + <argument name="excludedList" xsi:type="array"> <item name="quantity_and_stock_status" xsi:type="string">quantity_and_stock_status</item> </argument> </arguments> @@ -129,9 +130,6 @@ </argument> </arguments> </type> - <type name="Magento\CatalogInventory\Model\ResourceModel\Stock\Item"> - <plugin name="priceIndexUpdater" type="Magento\CatalogInventory\Model\Plugin\PriceIndexUpdater" /> - </type> <type name="Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save"> <plugin name="massAction" type="Magento\CatalogInventory\Plugin\MassUpdateProductAttribute" /> </type> diff --git a/app/code/Magento/CatalogInventory/etc/webapi.xml b/app/code/Magento/CatalogInventory/etc/webapi.xml index c172b9c971500..af9c70bf59c36 100644 --- a/app/code/Magento/CatalogInventory/etc/webapi.xml +++ b/app/code/Magento/CatalogInventory/etc/webapi.xml @@ -10,25 +10,25 @@ <route url="/V1/stockItems/:productSku" method="GET"> <service class="Magento\CatalogInventory\Api\StockRegistryInterface" method="getStockItemBySku"/> <resources> - <resource ref="Magento_CatalogInventory::cataloginventory"/> + <resource ref="Magento_Catalog::catalog_inventory"/> </resources> </route> <route url="/V1/products/:productSku/stockItems/:itemId" method="PUT"> <service class="Magento\CatalogInventory\Api\StockRegistryInterface" method="updateStockItemBySku"/> <resources> - <resource ref="Magento_CatalogInventory::cataloginventory"/> + <resource ref="Magento_Catalog::catalog_inventory"/> </resources> </route> <route url="/V1/stockItems/lowStock/" method="GET"> <service class="Magento\CatalogInventory\Api\StockRegistryInterface" method="getLowStockItems"/> <resources> - <resource ref="Magento_CatalogInventory::cataloginventory"/> + <resource ref="Magento_Catalog::catalog_inventory"/> </resources> </route> <route url="/V1/stockStatuses/:productSku" method="GET"> <service class="Magento\CatalogInventory\Api\StockRegistryInterface" method="getStockStatusBySku"/> <resources> - <resource ref="Magento_CatalogInventory::cataloginventory"/> + <resource ref="Magento_Catalog::catalog_inventory"/> </resources> </route> </routes> diff --git a/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php b/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php index 7f584fb1154e0..116a4529a8e60 100644 --- a/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php +++ b/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php @@ -6,19 +6,34 @@ namespace Magento\CatalogRule\Cron; +use Magento\CatalogRule\Model\Indexer\PartialIndex; +use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; + +/** + * Daily update catalog price rule by cron + */ class DailyCatalogUpdate { /** - * @var \Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor + * @var RuleProductProcessor */ protected $ruleProductProcessor; /** - * @param \Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor $ruleProductProcessor + * @var PartialIndex */ - public function __construct(\Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor $ruleProductProcessor) - { + private $partialIndex; + + /** + * @param RuleProductProcessor $ruleProductProcessor + * @param PartialIndex $partialIndex + */ + public function __construct( + RuleProductProcessor $ruleProductProcessor, + PartialIndex $partialIndex + ) { $this->ruleProductProcessor = $ruleProductProcessor; + $this->partialIndex = $partialIndex; } /** @@ -31,6 +46,8 @@ public function __construct(\Magento\CatalogRule\Model\Indexer\Rule\RuleProductP */ public function execute() { - $this->ruleProductProcessor->markIndexerAsInvalid(); + $this->ruleProductProcessor->isIndexerScheduled() + ? $this->partialIndex->partialUpdateCatalogRuleProductPrice() + : $this->ruleProductProcessor->markIndexerAsInvalid(); } } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index 1fc53c78985fb..df167d171e001 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -30,7 +30,7 @@ class IndexBuilder /** * @var \Magento\Framework\EntityManager\MetadataPool - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @since 100.1.0 */ protected $metadataPool; @@ -41,7 +41,7 @@ class IndexBuilder * This array contain list of CatalogRuleGroupWebsite table columns * * @var array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_catalogRuleGroupWebsiteColumnsList = ['rule_id', 'customer_group_id', 'website_id']; @@ -446,7 +446,7 @@ private function assignProductToRule(Rule $rule, int $productEntityId, array $we * @param Product $product * @return $this * @throws \Exception - * @deprecated + * @deprecated 101.1.5 * @see ReindexRuleProduct::execute * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -500,7 +500,7 @@ protected function getTable($tableName) * * @param Rule $rule * @return $this - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see ReindexRuleProduct::execute */ protected function updateRuleProductData(Rule $rule) @@ -528,7 +528,7 @@ protected function updateRuleProductData(Rule $rule) * @param Product|null $product * @throws \Exception * @return $this - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see ReindexRuleProductPrice::execute * @see ReindexRuleGroupWebsite::execute */ @@ -543,7 +543,7 @@ protected function applyAllRules(Product $product = null) * Update CatalogRuleGroupWebsite data * * @return $this - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see ReindexRuleGroupWebsite::execute */ protected function updateCatalogRuleGroupWebsiteData() @@ -569,7 +569,7 @@ protected function deleteOldData() * @param array $ruleData * @param array $productData * @return float - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see ProductPriceCalculator::calculate */ protected function calcRuleProductPrice($ruleData, $productData = null) @@ -584,7 +584,7 @@ protected function calcRuleProductPrice($ruleData, $productData = null) * @param Product|null $product * @return \Zend_Db_Statement_Interface * @throws \Magento\Framework\Exception\LocalizedException - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see RuleProductsSelectBuilder::build */ protected function getRuleProductsStmt($websiteId, Product $product = null) @@ -598,7 +598,7 @@ protected function getRuleProductsStmt($websiteId, Product $product = null) * @param array $arrData * @return $this * @throws \Exception - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see RuleProductPricesPersistor::execute */ protected function saveRuleProductPrices($arrData) diff --git a/app/code/Magento/CatalogRule/Model/Indexer/PartialIndex.php b/app/code/Magento/CatalogRule/Model/Indexer/PartialIndex.php new file mode 100644 index 0000000000000..12a77f81826d6 --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/Indexer/PartialIndex.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogRule\Model\Indexer; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\App\ResourceConnection; + +/** + * Catalog rule partial index + * + * This class triggers the dependent index "catalog_product_price", + * and the cache is cleared only for the matched products for partial indexing. + */ +class PartialIndex +{ + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * @var IndexBuilder + */ + private $indexBuilder; + + /** + * @param ResourceConnection $resource + * @param IndexBuilder $indexBuilder + */ + public function __construct( + ResourceConnection $resource, + IndexBuilder $indexBuilder + ) { + $this->resource = $resource; + $this->connection = $resource->getConnection(); + $this->indexBuilder = $indexBuilder; + } + + /** + * Synchronization replica table with original table "catalogrule_product_price" + * + * Used replica table for correctly working MySQL trigger + * + * @return void + */ + public function partialUpdateCatalogRuleProductPrice(): void + { + $this->indexBuilder->reindexFull(); + $indexTableName = $this->resource->getTableName('catalogrule_product_price'); + $select = $this->connection->select()->from( + ['crp' => $indexTableName], + 'product_id' + ); + $selectFields = $this->connection->select()->from( + ['crp' => $indexTableName], + [ + 'rule_date', + 'customer_group_id', + 'product_id', + 'rule_price', + 'website_id', + 'latest_start_date', + 'earliest_end_date', + ] + ); + $where = ['product_id' .' NOT IN (?)' => $select]; + //remove products that are no longer used in indexing + $this->connection->delete($this->resource->getTableName('catalogrule_product_price_replica'), $where); + //add updated products to indexing + $this->connection->query( + $this->connection->insertFromSelect( + $selectFields, + $this->resource->getTableName('catalogrule_product_price_replica'), + [ + 'rule_date', + 'customer_group_id', + 'product_id', + 'rule_price', + 'website_id', + 'latest_start_date', + 'earliest_end_date', + ], + AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } +} 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/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index cd24201963f25..f2e8e54d34665 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -3,8 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); - namespace Magento\CatalogRule\Model; use Magento\Catalog\Model\Product; @@ -15,7 +13,6 @@ use Magento\CatalogRule\Helper\Data; use Magento\CatalogRule\Model\Data\Condition\Converter; use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; -use Magento\CatalogRule\Model\ResourceModel\Product\ConditionsToCollectionApplier; use Magento\CatalogRule\Model\ResourceModel\Rule as RuleResourceModel; use Magento\CatalogRule\Model\Rule\Action\CollectionFactory as RuleCollectionFactory; use Magento\CatalogRule\Model\Rule\Condition\CombineFactory; @@ -36,6 +33,7 @@ use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\CatalogRule\Model\ResourceModel\Product\ConditionsToCollectionApplier; /** * Catalog Rule data model @@ -501,8 +499,7 @@ public function calcProductPriceRule(Product $product, $price) } else { $customerGroupId = $this->_customerSession->getCustomerGroupId(); } - $currentDateTime = new \DateTime(); - $dateTs = $currentDateTime->getTimestamp(); + $dateTs = $this->_localeDate->scopeTimeStamp($storeId); $cacheKey = date('Y-m-d', $dateTs) . "|{$websiteId}|{$customerGroupId}|{$productId}|{$price}"; if (!array_key_exists($cacheKey, self::$_priceRulesData)) { @@ -898,12 +895,4 @@ public function getIdentities() { return ['price']; } - - /** - * Clear price rules cache. - */ - public function clearPriceRulesData(): void - { - self::$_priceRulesData = []; - } } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml index 1170b08b1add9..27edab962033e 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml @@ -16,7 +16,7 @@ <!-- It sometimes is loading too long for default 10s --> <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> - <helper class="\Magento\CatalogRule\Test\Mftf\Helper\CatalogPriceRuleHelper" method="deleteAllCatalogPriceRules" stepKey="deleteAllCatalogPriceRulesOneByOne"> + <helper class="\Magento\Rule\Test\Mftf\Helper\RuleHelper" method="deleteAllRulesOneByOne" stepKey="deleteAllRulesOneByOne"> <argument name="firstNotEmptyRow">{{AdminDataGridTableSection.firstNotEmptyRow}}</argument> <argument name="modalAcceptButton">{{AdminConfirmationModalSection.ok}}</argument> <argument name="deleteButton">{{AdminMainActionsSection.delete}}</argument> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminFillCatalogRuleConditionWithSelectAttributeActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminFillCatalogRuleConditionWithSelectAttributeActionGroup.xml new file mode 100644 index 0000000000000..08e3e58632101 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminFillCatalogRuleConditionWithSelectAttributeActionGroup.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="AdminFillCatalogRuleConditionWithSelectAttributeActionGroup" extends="AdminFillCatalogRuleConditionActionGroup"> + <annotations> + <description>EXTENDS: AdminFillCatalogRuleConditionActionGroup. Clicks on the Conditions tab. Fills in the provided condition with attribute type select.</description> + </annotations> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.activeValueInput}}" userInput="{{conditionValue}}" stepKey="fillConditionValue"/> + <remove keyForRemoval="clickApply"/> + <remove keyForRemoval="waitForApplyButtonInvisibility"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml new file mode 100644 index 0000000000000..547ef356f099d --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml @@ -0,0 +1,198 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminApplyCatalogPriceRuleByProductAttributeTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Catalog price rule"/> + <title value="Admin should be able to apply the catalog price rule by product attribute"/> + <description value="Admin should be able to apply the catalog price rule by product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-25351"/> + <group value="catalogRule"/> + </annotations> + <before> + <createData entity="productDropDownAttribute" stepKey="createDropdownAttribute"/> + <!--Create attribute options--> + <createData entity="ProductAttributeOption7" stepKey="createProductAttributeOptionGreen"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + <createData entity="ProductAttributeOption8" stepKey="createProductAttributeOptionRed"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + <!--Add attribute to default attribute set--> + <createData entity="AddToDefaultSet" stepKey="addAttributeToDefaultSet"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createFirstProduct"> + <field key="price">40.00</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createSecondProduct"> + <field key="price">40.00</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createFirstConfigProductAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createSecondConfigProductAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstConfigAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getSecondConfigAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> + <field key="price">60.00</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getFirstConfigAttributeOption"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> + <field key="price">60.00</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getSecondConfigAttributeOption"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getFirstConfigAttributeOption"/> + <requiredEntity createDataKey="getSecondConfigAttributeOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createFirstConfigProductAddChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createSecondConfigProductAddChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdmin"/> + <!-- Update first simple product --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openFirstSimpleProductForEdit"> + <argument name="productId" value="$createFirstProduct.id$"/> + </actionGroup> + <selectOption selector="{{AdminProductFormSection.customSelectField($createDropdownAttribute.attribute[attribute_code]$)}}" + userInput="$createProductAttributeOptionGreen.option[store_labels][0][label]$" stepKey="setAttributeValueForFirstSimple"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveFirstSimpleProduct"/> + <!-- Update second simple product --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openSecondSimpleProductForEdit"> + <argument name="productId" value="$createSecondProduct.id$"/> + </actionGroup> + <selectOption selector="{{AdminProductFormSection.customSelectField($createDropdownAttribute.attribute[attribute_code]$)}}" + userInput="$createProductAttributeOptionRed.option[store_labels][0][label]$" stepKey="setAttributeValueForSecondSimple"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondSimpleProduct"/> + <!-- Update first child of configurable product --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openFirstChildProductForEdit"> + <argument name="productId" value="$createConfigFirstChildProduct.id$"/> + </actionGroup> + <selectOption selector="{{AdminProductFormSection.customSelectField($createDropdownAttribute.attribute[attribute_code]$)}}" + userInput="$createProductAttributeOptionGreen.option[store_labels][0][label]$" stepKey="setAttributeValueForFirstChildProduct"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveFirstChildProduct"/> + <!-- Update second child of configurable product --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openSecondChildProductForEdit"> + <argument name="productId" value="$createConfigSecondChildProduct.id$"/> + </actionGroup> + <selectOption selector="{{AdminProductFormSection.customSelectField($createDropdownAttribute.attribute[attribute_code]$)}}" + userInput="$createProductAttributeOptionGreen.option[store_labels][0][label]$" stepKey="setAttributeValueForSecondChildProduct"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondChildProduct"/> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </before> + <after> + <!-- Delete created data --> + <deleteData createDataKey="createDropdownAttribute" stepKey="deleteDropdownAttribute"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetCatalogRulesGridFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + <!-- Create Catalog Price Rule --> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="startCreatingFirstPriceRule"/> + <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForFirstPriceRule"> + <argument name="groups" value="'NOT LOGGED IN'"/> + </actionGroup> + <actionGroup ref="AdminFillCatalogRuleConditionWithSelectAttributeActionGroup" stepKey="createCatalogPriceRule"> + <argument name="condition" value="$createDropdownAttribute.default_frontend_label$"/> + <argument name="conditionValue" value="$createProductAttributeOptionGreen.option[store_labels][0][label]$"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForCatalogPriceRule"> + <argument name="discountAmount" value="{{SimpleCatalogPriceRule.discount_amount}}"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> + <!-- Run cron --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalogrule_rule"/> + </actionGroup> + <!-- Open first simple product page on storefront --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openFirstSimpleProductPage"> + <argument name="productUrlKey" value="$createFirstProduct.custom_attributes[url_key]$"/> + </actionGroup> + <!-- Verify price for simple product with attribute option green=$20 --> + <actionGroup ref="AssertStorefrontProductPricesActionGroup" stepKey="assertFirstSimpleProductPrices"> + <argument name="productPrice" value="$createFirstProduct.price$"/> + <argument name="productFinalPrice" value="$20.00"/> + </actionGroup> + + <!-- Open the configurable product page on storefront --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openConfigurableProductPage"> + <argument name="productUrlKey" value="$createConfigProduct.custom_attributes[url_key]$"/> + </actionGroup> + <!-- Verify price for configurable product with attribute option green=$30 --> + <selectOption selector="{{AdminCustomerActivitiesConfigureSection.addAttribute}}" userInput="option1" stepKey="selectFirstOptionOfConfigProduct"/> + <actionGroup ref="AssertStorefrontProductPricesActionGroup" stepKey="assertConfigProductWithFirstOptionPrices"> + <argument name="productPrice" value="$createConfigFirstChildProduct.price$"/> + <argument name="productFinalPrice" value="$30.00"/> + </actionGroup> + <!-- Verify price for configurable product with attribute option green=$30 --> + <selectOption selector="{{AdminCustomerActivitiesConfigureSection.addAttribute}}" userInput="option2" stepKey="selectSecondOptionOfConfigProduct"/> + <actionGroup ref="AssertStorefrontProductPricesActionGroup" stepKey="assertConfigProductWithSecondOptionPrices"> + <argument name="productPrice" value="$createConfigSecondChildProduct.price$"/> + <argument name="productFinalPrice" value="$30.00"/> + </actionGroup> + + <!-- Open the second simple product page on storefront --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openSecondSimpleProductPage"> + <argument name="productUrlKey" value="$createSecondProduct.custom_attributes[url_key]$"/> + </actionGroup> + <!-- Verify Price for second simple product with specialColor red=$40 --> + <actionGroup ref="AssertStorefrontProductPricesActionGroup" stepKey="assertSecondSimpleProductPrices"> + <argument name="productPrice" value="$createSecondProduct.price$"/> + <argument name="productFinalPrice" value="$createSecondProduct.price$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml index d1f9ebd4c99a4..e6b825ff3cf70 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml @@ -36,18 +36,17 @@ <deleteData createDataKey="createSimpleProductTwo" stepKey="deleteSimpleProductTwo"/> <!-- Delete the catalog price rule --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> <argument name="name" value="{{_defaultCatalogRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!-- 1. Begin creating a new catalog price rule --> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> - <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <click selector="{{AdminGridMainControls.add}}" stepKey="addNewRule"/> <waitForPageLoad stepKey="waitForIndividualRulePage"/> <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{_defaultCatalogRule.name}}" stepKey="fillName"/> @@ -78,8 +77,12 @@ <!-- 3. Save and apply the new catalog price rule --> <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- 4. Verify the storefront --> <amOnPage url="$$createCategoryOne.name$$.html" stepKey="goToCategoryOne"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml index 882a92a2ee433..1de036d1026dd 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml @@ -127,8 +127,12 @@ <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> <!-- Reindex and flash cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Open Storefront product page and assert created configurable product --> <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml index fcae0065f1b53..d45c3af1c2da0 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml @@ -25,8 +25,12 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- log in and create the price rule --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -42,7 +46,7 @@ <argument name="name" value="{{_defaultCatalogRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> @@ -61,8 +65,7 @@ <see stepKey="seeNewPrice2" selector="{{StorefrontProductInfoMainSection.updatedPrice}}" userInput="$110.70"/> <!-- Add the product to cart and check that the price is correct there --> - <click stepKey="addToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> - <waitForPageLoad stepKey="waitForAddedToCart"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCheckout"/> <see stepKey="seeNewPriceInCart" selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$110.70"/> </test> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml index c3132e5c46cc9..fb218297b646d 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml @@ -41,12 +41,21 @@ </after> <!-- Create a catalog rule for the NOT LOGGED IN customer group --> - <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="createNewPriceRule"/> - <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForPriceRule"> - <argument name="groups" value="'NOT LOGGED IN'"/> + <actionGroup ref="NewCatalogPriceRuleByUIActionGroup" stepKey="createNewPriceRule"/> + <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <conditionalClick selector="{{AdminNewCatalogPriceRule.active}}" dependentSelector="{{AdminNewCatalogPriceRule.activeIsEnabled}}" visible="false" stepKey="enableActiveBtn"/> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> + + <!--<click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/>--> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccess"/> + + <!-- Perform reindex and flush cache --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> </actionGroup> - <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsPriceRule"/> - <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyFPriceRule"/> <!-- As a NOT LOGGED IN user, go to the storefront category page and should see the discount --> <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToCategory1"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml index 90a0835508b06..77228dde8797f 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml @@ -20,7 +20,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <actionGroup ref="NewCatalogPriceRuleWithInvalidDataActionGroup" stepKey="createNewPriceRule"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml index 6b34fd1e67e9b..7247d61bea87c 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml @@ -72,8 +72,12 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> @@ -105,8 +109,7 @@ </after> <!-- Delete the simple product and catalog price rule --> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> - <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage1"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule1"> <argument name="name" value="{{DeleteActiveCatalogPriceRuleWithConditions.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -115,8 +118,12 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the rule." stepKey="seeDeletedRuleMessage1"/> <!-- Reindex --> - <magentoCLI command="cache:flush" stepKey="flushCache1"/> - <magentoCLI command="indexer:reindex" stepKey="reindex1"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Assert that the rule isn't present on the Category page --> <amOnPage url="$$createCategory1.name$$.html" stepKey="goToStorefrontCategoryPage1"/> @@ -131,9 +138,7 @@ <!-- Assert that the rule isn't present in the Shopping Cart --> <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="option1" stepKey="selectOption1"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart1"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad4"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <see selector="{{StorefrontMessagesSection.success}}" userInput="You added $$createConfigProduct1.name$ to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="openMiniShoppingCart1"/> <see selector="{{StorefrontMinicartSection.productPriceByName($$createConfigProduct1.name$$)}}" userInput="$$createConfigProduct1.price$$" stepKey="seeCorrectProductPrice1"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml index 59fa4fde1c88a..a3b1729102390 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml @@ -49,8 +49,7 @@ </after> <!-- Delete the simple product and catalog price rule --> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> - <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage1"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule1"> <argument name="name" value="{{DeleteActiveCatalogPriceRuleWithConditions.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -61,8 +60,12 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the rule." stepKey="seeDeletedRuleMessage1"/> <!-- Reindex --> - <magentoCLI command="cache:flush" stepKey="flushCache1"/> - <magentoCLI command="indexer:reindex" stepKey="reindex1"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Assert that the rule isn't present on the Category page --> <amOnPage url="$$createCategory1.name$$.html" stepKey="goToStorefrontCategoryPage1"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml index 69508490774dd..64fe4d8a130a7 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml @@ -61,17 +61,21 @@ <!-- Verify that the simple product page shows the discount --> <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="goToSimpleProductPage1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeCorrectName1"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createSimpleProduct.sku$$" stepKey="seeCorrectSku1"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku1"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$110.70" stepKey="seeCorrectPrice1"/> <!-- Verify that the configurable product page the catalog price rule discount --> <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToConfigurableProductPage1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="seeCorrectName2"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="seeCorrectSku2"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku2"> + <argument name="productSku" value="{{_defaultProduct.sku}}"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$0.90" stepKey="seeCorrectPrice2"/> <!-- Delete the rule --> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> <argument name="name" value="{{_defaultCatalogRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -79,8 +83,12 @@ <!-- Apply and flush the cache --> <click selector="{{AdminCatalogPriceRuleGrid.applyRules}}" stepKey="clickApplyRules"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Verify that category page shows the original prices --> <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage2"/> @@ -92,13 +100,17 @@ <!-- Verify that the simple product page shows the original price --> <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="goToSimpleProductPage2"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeCorrectName3"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createSimpleProduct.sku$$" stepKey="seeCorrectSku3"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku3"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$123.00" stepKey="seeCorrectPrice3"/> <!-- Verify that the configurable product page shows the original price --> <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToConfigurableProductPage2"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="seeCorrectName4"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="seeCorrectSku4"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku4"> + <argument name="productSku" value="{{_defaultProduct.sku}}"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$1.00" stepKey="seeCorrectPrice4"/> </test> </tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml index 9d7607d7521c9..745025073dceb 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml @@ -49,7 +49,7 @@ <after> <!--Delete created data--> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToCatalogPriceRulePage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -68,8 +68,7 @@ </after> <!--Create catalog price rule--> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> - <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup ref="CreateCatalogPriceRuleActionGroup" stepKey="createCatalogPriceRule"> <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> </actionGroup> @@ -80,9 +79,12 @@ <argument name="targetSelectValue" value="is undefined"/> </actionGroup> <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <magentoCLI command="cache:flush" stepKey="flushCache3"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Check Catalog Price Rule for first product--> <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToFirstProductPage"/> @@ -104,7 +106,7 @@ <!--Delete previous attribute and Catalog Price Rule--> <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToCatalogPriceRulePage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -116,8 +118,7 @@ </createData> <!--Create new Catalog Price Rule--> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> - <waitForPageLoad stepKey="waitForPriceRulePage1"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage1"/> <actionGroup ref="CreateCatalogPriceRuleActionGroup" stepKey="createCatalogPriceRule1"> <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> </actionGroup> @@ -128,9 +129,12 @@ <argument name="targetSelectValue" value="is undefined"/> </actionGroup> <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules1"/> - <magentoCLI command="indexer:reindex" stepKey="reindex1"/> - <magentoCLI command="cache:flush" stepKey="flushCache1"/> - <magentoCLI command="cache:flush" stepKey="flushCache2"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexSecondTime"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheSecondTime"> + <argument name="tags" value=""/> + </actionGroup> <!--Check Catalog Price Rule for third product--> <amOnPage url="{{StorefrontProductPage.url($$createThirdProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToThirdProductPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml index 1919f7d5cc544..6de7bba59c340 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml @@ -8,16 +8,16 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ApplyCatalogPriceRuleByProductAttributeTest"> + <test name="ApplyCatalogPriceRuleByProductAttributeTest" deprecated="Use AdminApplyCatalogPriceRuleByProductAttributeTest"> <annotations> <stories value="Catalog price rule"/> - <title value="Admin should be able to apply the catalog price rule by product attribute"/> + <title value="DEPRECATED. Admin should be able to apply the catalog price rule by product attribute"/> <description value="Admin should be able to apply the catalog price rule by product attribute"/> <severity value="CRITICAL"/> <testCaseId value="MC-148"/> <group value="CatalogRule"/> <skip> - <issueId value="MC-22577"/> + <issueId value="DEPRECATED">Use AdminApplyCatalogPriceRuleByProductAttributeTest instead.</issueId> </skip> </annotations> <before> @@ -97,7 +97,7 @@ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToCatalogPriceRulePage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> <argument name="name" value="{{SimpleCatalogPriceRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -232,10 +232,11 @@ <see userInput="You saved the rule." selector="{{ContentManagementSection.StoreConfigurationPageSuccessMessage}}" stepKey="seeMessage"/> <see userInput="Updated rules applied." selector="{{ContentManagementSection.StoreConfigurationPageSuccessMessage}}" stepKey="seeSuccessMessage"/> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- Run cron --> + <magentoCron stepKey="runAllCronJobs"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Go to Frontend and open the simple product --> <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.sku$$)}}" stepKey="amOnSimpleProductPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml index 23fc7e1a9ffba..1651f8425ec1c 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml @@ -81,7 +81,7 @@ </before> <after> <!-- Delete the catalog price rule --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> <argument name="name" value="{{_defaultCatalogRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -112,8 +112,12 @@ <!-- Save and apply the new catalog price rule --> <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml index dfd34181108b8..8103e6b115950 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml @@ -41,7 +41,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Delete the catalog price rule --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> <argument name="name" value="{{CatalogRuleByFixed.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -61,8 +61,12 @@ <!-- Save and apply the new catalog price rule --> <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml index 25351ca650db9..b90cc66a10d68 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml @@ -46,7 +46,7 @@ <deleteData createDataKey="customerGroup" stepKey="deleteCustomerGroup"/> <!-- Delete the catalog price rule --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> <argument name="name" value="{{CatalogRuleByFixed.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -69,7 +69,9 @@ <!-- Save and apply the new catalog price rule --> <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml index 59976fbac1724..d9b62ef8fc913 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml @@ -49,7 +49,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Delete the catalog price rule --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> <argument name="name" value="{{_defaultCatalogRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -68,8 +68,12 @@ <!-- Save and apply the new catalog price rule --> <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml index 6ac9f713e2844..b678e379a603d 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml @@ -41,7 +41,9 @@ <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="clickSaveAndApplyRule"/> <!-- Perform reindex --> - <magentoCLI command="indexer:reindex" arguments="catalogrule_rule" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalogrule_rule"/> + </actionGroup> </before> <after> <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml index 2df891b24223b..264c55ba43390 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml @@ -34,7 +34,9 @@ <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForThirdPriceRule"/> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyFirstPriceRule"/> <!-- Perform reindex --> - <magentoCLI command="indexer:reindex" arguments="catalogrule_rule" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalogrule_rule"/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/CatalogRule/etc/mview.xml b/app/code/Magento/CatalogRule/etc/mview.xml index 9e5a1c866a842..9f793d5c8c393 100644 --- a/app/code/Magento/CatalogRule/etc/mview.xml +++ b/app/code/Magento/CatalogRule/etc/mview.xml @@ -26,6 +26,7 @@ <view id="catalog_product_price" class="Magento\Catalog\Model\Indexer\Product\Price" group="indexer"> <subscriptions> <table name="catalogrule_product_price" entity_column="product_id" /> + <table name="catalogrule_product_price_replica" entity_column="product_id" /> </subscriptions> </view> </config> diff --git a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml index 1c3eedf43b264..e1229dc56cfbf 100644 --- a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml +++ b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml @@ -5,14 +5,16 @@ */ /**@var \Magento\Backend\Block\Widget\Form\Renderer\Fieldset $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_element = $block->getElement() ?> <?php $_jsObjectName = $block->getFieldSetId() != null ? $block->getFieldSetId() : $_element->getHtmlId() ?> <div class="rule-tree"> - <fieldset id="<?= $block->escapeHtmlAttr($_jsObjectName) ?>" <?= /* @noEscape */ $_element->serialize(['class']) ?> class="fieldset"> +<fieldset id="<?= $block->escapeHtmlAttr($_jsObjectName) ?>" <?= /* @noEscape */ $_element->serialize(['class']) ?> + class="fieldset"> <legend class="legend"><span><?= $block->escapeHtml($_element->getLegend()) ?></span></legend> <br> - <?php if ($_element->getComment()) : ?> + <?php if ($_element->getComment()): ?> <div class="messages"> <div class="message message-notice"><?= $block->escapeHtml($_element->getComment()) ?></div> </div> @@ -22,16 +24,21 @@ </div> </fieldset> </div> -<script> + +<?php $scriptString = <<<script + require([ - "Magento_Rule/rules", - "prototype" + 'Magento_Rule/rules', + 'prototype' ], function(VarienRulesForm){ -window.<?= /* @noEscape */ $_jsObjectName ?> = new VarienRulesForm('<?= /* @noEscape */ $_jsObjectName ?>', '<?= /* @noEscape */ $block->getNewChildUrl() ?>'); -<?php if ($_element->getReadonly()) : ?> - <?= /* @noEscape */ $_element->getHtmlId() ?>.setReadonly(true); -<?php endif; ?> +script; +$scriptString .= 'window.' . /* @noEscape */ $_jsObjectName . ' = new VarienRulesForm(\'' . + /* @noEscape */ $_jsObjectName . '\', \'' . /* @noEscape */ $block->getNewChildUrl() . '\');'; +if ($_element->getReadonly()): + $scriptString .= /* @noEscape */ $_element->getHtmlId() . '.setReadonly(true);' . PHP_EOL; +endif; -}); -</script> +$scriptString .= '});' . PHP_EOL; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> 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/Block/Advanced/Form.php b/app/code/Magento/CatalogSearch/Block/Advanced/Form.php index 681b7ecfb02dc..f8c159f5d6d73 100644 --- a/app/code/Magento/CatalogSearch/Block/Advanced/Form.php +++ b/app/code/Magento/CatalogSearch/Block/Advanced/Form.php @@ -9,11 +9,13 @@ use Magento\CatalogSearch\Model\Advanced; use Magento\Directory\Model\CurrencyFactory; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Collection\AbstractDb as DbCollection; use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\View\Element\BlockInterface; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Element\Template\Context; +use Magento\CatalogSearch\Helper\Data as CatalogSearchHelper; /** * Advanced search form @@ -42,15 +44,19 @@ class Form extends Template * @param Advanced $catalogSearchAdvanced * @param CurrencyFactory $currencyFactory * @param array $data + * @param CatalogSearchHelper|null $catalogSearchHelper */ public function __construct( Context $context, Advanced $catalogSearchAdvanced, CurrencyFactory $currencyFactory, - array $data = [] + array $data = [], + ?CatalogSearchHelper $catalogSearchHelper = null ) { $this->_catalogSearchAdvanced = $catalogSearchAdvanced; $this->_currencyFactory = $currencyFactory; + $data['catalogSearchHelper'] = $catalogSearchHelper ?? + ObjectManager::getInstance()->get(CatalogSearchHelper::class); parent::__construct($context, $data); } @@ -185,7 +191,7 @@ public function getCurrency($attribute) * Retrieve attribute input type * * @param AbstractAttribute $attribute - * @return string + * @return string */ public function getAttributeInputType($attribute) { diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php index 8ce2e0140f528..5143762a07e08 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced.php @@ -67,7 +67,7 @@ class Advanced extends \Magento\Framework\Model\AbstractModel /** * Initialize dependencies * - * @deprecated + * @deprecated 101.0.2 * @var Config */ protected $_catalogConfig; diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index ca6ff0720023f..e226bdc6900e6 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -126,6 +126,7 @@ public function execute($entityIds) * @inheritdoc * * @throws \InvalidArgumentException + * @since 101.0.0 */ public function executeByDimensions(array $dimensions, \Traversable $entityIds = null) { 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 28624c667e42b..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) { @@ -572,11 +573,11 @@ public function prepareProductIndex($indexData, $productData, $storeId) foreach ($indexData as $entityId => $attributeData) { foreach ($attributeData as $attributeId => $attributeValues) { $value = $this->getAttributeValue($attributeId, $attributeValues, $storeId); - if (!empty($value)) { + if ($value !== null && $value !== false && $value !== '') { if (!isset($index[$attributeId])) { $index[$attributeId] = []; } - $index[$attributeId][$entityId] = $value; + $index[$attributeId][$entityId] = $value; } } } 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 bd63e1e79989c..3ce8a96fb5070 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php @@ -41,7 +41,7 @@ class Full * Index values separator * * @var string - * @deprecated 100.1.6 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @deprecated 100.1.0 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$separator */ protected $separator = ' | '; @@ -50,7 +50,7 @@ class Full * Array of \DateTime objects per store * * @var \DateTime[] - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $dates = []; @@ -58,7 +58,7 @@ class Full * Product Type Instances cache * * @var array - * @deprecated 100.1.6 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @deprecated 100.1.0 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productTypes */ protected $productTypes = []; @@ -67,7 +67,7 @@ class Full * Product Emulators cache * * @var array - * @deprecated 100.1.6 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @deprecated 100.1.0 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productEmulators */ protected $productEmulators = []; @@ -95,7 +95,7 @@ class Full * Catalog product type * * @var \Magento\Catalog\Model\Product\Type - * @deprecated 100.1.6 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @deprecated 100.1.0 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$catalogProductType */ protected $catalogProductType; @@ -111,7 +111,7 @@ class Full * Core store config * * @var \Magento\Framework\App\Config\ScopeConfigInterface - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $scopeConfig; @@ -119,7 +119,7 @@ class Full * Store manager * * @var \Magento\Store\Model\StoreManagerInterface - * @deprecated 100.1.6 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @deprecated 100.1.0 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$storeManager */ protected $storeManager; @@ -131,25 +131,25 @@ class Full /** * @var \Magento\Framework\Indexer\SaveHandler\IndexerInterface - * @deprecated 100.1.6 As part of self::cleanIndex() + * @deprecated 100.1.0 As part of self::cleanIndex() */ protected $indexHandler; /** * @var \Magento\Framework\Stdlib\DateTime - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $dateTime; /** * @var \Magento\Framework\Locale\ResolverInterface - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $localeResolver; /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $localeDate; @@ -160,19 +160,19 @@ class Full /** * @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $fulltextResource; /** * @var \Magento\Framework\Search\Request\Config - * @deprecated 100.1.6 As part of self::reindexAll() + * @deprecated 100.1.0 As part of self::reindexAll() */ protected $searchRequestConfig; /** * @var \Magento\Framework\Search\Request\DimensionFactory - * @deprecated 100.1.6 As part of self::cleanIndex() + * @deprecated 100.1.0 As part of self::cleanIndex() */ private $dimensionFactory; @@ -301,7 +301,7 @@ protected function getTable($table) /** * Get parents IDs of product IDs to be re-indexed * - * @deprecated as it not used in the class anymore and duplicates another API method + * @deprecated 100.2.3 as it not used in the class anymore and duplicates another API method * @see \Magento\CatalogSearch\Model\ResourceModel\Fulltext::getRelationsByChild() * * @param int[] $entityIds @@ -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/Indexer/Fulltext/Plugin/Store/Group.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Store/Group.php index 73a79f7c87239..e7bbf72bdd7e3 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Store/Group.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Store/Group.php @@ -5,47 +5,29 @@ */ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Store; +use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; use Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\AbstractPlugin as AbstractIndexerPlugin; -use Magento\Store\Model\ResourceModel\Group as StoreGroupResourceModel; use Magento\Framework\Model\AbstractModel; -use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; +use Magento\Store\Model\ResourceModel\Group as StoreGroupResourceModel; /** * Plugin for Magento\Store\Model\ResourceModel\Group */ class Group extends AbstractIndexerPlugin { - /** - * @var bool - */ - private $needInvalidation; - - /** - * Check if indexer requires invalidation after store group save - * - * @param StoreGroupResourceModel $subject - * @param AbstractModel $group - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforeSave(StoreGroupResourceModel $subject, AbstractModel $group) - { - $this->needInvalidation = !$group->isObjectNew() && $group->dataHasChangedFor('website_id'); - } - /** * Invalidate indexer on store group save * * @param StoreGroupResourceModel $subject * @param StoreGroupResourceModel $result + * @param AbstractModel $group * @return StoreGroupResourceModel * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function afterSave(StoreGroupResourceModel $subject, StoreGroupResourceModel $result) + public function afterSave(StoreGroupResourceModel $subject, StoreGroupResourceModel $result, AbstractModel $group) { - if ($this->needInvalidation) { + if (!$group->isObjectNew() && $group->dataHasChangedFor('website_id')) { $this->indexerRegistry->get(FulltextIndexer::INDEXER_ID)->invalidate(); } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Store/View.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Store/View.php index 7f0c5fdae6d42..242a4f3f0c36b 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Store/View.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Store/View.php @@ -5,47 +5,29 @@ */ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Store; +use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; use Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\AbstractPlugin as AbstractIndexerPlugin; -use Magento\Store\Model\ResourceModel\Store as StoreResourceModel; use Magento\Framework\Model\AbstractModel; -use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; +use Magento\Store\Model\ResourceModel\Store as StoreResourceModel; /** * Plugin for Magento\Store\Model\ResourceModel\Store */ class View extends AbstractIndexerPlugin { - /** - * @var bool - */ - private $needInvalidation; - - /** - * Check if indexer requires invalidation after store view save - * - * @param StoreResourceModel $subject - * @param AbstractModel $store - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforeSave(StoreResourceModel $subject, AbstractModel $store) - { - $this->needInvalidation = $store->isObjectNew(); - } - /** * Invalidate indexer on store view save * * @param StoreResourceModel $subject * @param StoreResourceModel $result + * @param AbstractModel $store * @return StoreResourceModel * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function afterSave(StoreResourceModel $subject, StoreResourceModel $result) + public function afterSave(StoreResourceModel $subject, StoreResourceModel $result, AbstractModel $store) { - if ($this->needInvalidation) { + if ($store->isObjectNew()) { $this->indexerRegistry->get(FulltextIndexer::INDEXER_ID)->invalidate(); } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php index 8722cd52b618a..c79d876c1127d 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php @@ -13,7 +13,7 @@ * * @api * @since 100.2.0 - * @deprecated + * @deprecated 101.0.0 * @see \Magento\ElasticSearch */ class UnknownStateException extends LocalizedException diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php index 332bb991bf29f..b2aaa054ebc34 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php @@ -176,15 +176,16 @@ public function getCurrencyRate() * * @param float|string $fromPrice * @param float|string $toPrice + * @param boolean $isLast * @return float|\Magento\Framework\Phrase */ - protected function _renderRangeLabel($fromPrice, $toPrice) + protected function _renderRangeLabel($fromPrice, $toPrice, $isLast = false) { $fromPrice = empty($fromPrice) ? 0 : $fromPrice * $this->getCurrencyRate(); $toPrice = empty($toPrice) ? $toPrice : $toPrice * $this->getCurrencyRate(); $formattedFromPrice = $this->priceCurrency->format($fromPrice); - if ($toPrice === '') { + if ($isLast) { return __('%1 and above', $formattedFromPrice); } elseif ($fromPrice == $toPrice && $this->dataProvider->getOnePriceIntervalValue()) { return $formattedFromPrice; @@ -215,12 +216,15 @@ protected function _getItemsData() $data = []; if (count($facets) > 1) { // two range minimum + $lastFacet = array_key_last($facets); foreach ($facets as $key => $aggregation) { $count = $aggregation['count']; if (strpos($key, '_') === false) { continue; } - $data[] = $this->prepareData($key, $count, $data); + + $isLast = $lastFacet === $key; + $data[] = $this->prepareData($key, $count, $isLast); } } @@ -264,18 +268,13 @@ protected function getFrom($from) * * @param string $key * @param int $count + * @param boolean $isLast * @return array */ - private function prepareData($key, $count) + private function prepareData($key, $count, $isLast = false) { - list($from, $to) = explode('_', $key); - if ($from == '*') { - $from = $this->getFrom($to); - } - if ($to == '*') { - $to = $this->getTo($to); - } - $label = $this->_renderRangeLabel($from, $to); + [$from, $to] = explode('_', $key); + $label = $this->_renderRangeLabel($from, $to, $isLast); $value = $from . '-' . $to . $this->dataProvider->getAdditionalRequestData(); $data = [ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index 291fad1e16ebf..47160bff1d571 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -236,6 +236,7 @@ public function addFieldsToFilter($fields) /** * @inheritdoc + * @since 101.0.2 */ public function setOrder($attribute, $dir = Select::SQL_DESC) { @@ -253,6 +254,7 @@ public function setOrder($attribute, $dir = Select::SQL_DESC) /** * @inheritdoc + * @since 101.0.2 */ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) { @@ -272,6 +274,7 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) /** * @inheritdoc + * @since 101.0.2 */ public function setVisibility($visibility) { @@ -338,6 +341,7 @@ protected function _renderFiltersBefore() /** * @inheritDoc + * @since 101.0.4 */ public function clear() { @@ -347,6 +351,7 @@ public function clear() /** * @inheritDoc + * @since 101.0.4 */ protected function _reset() { @@ -356,6 +361,7 @@ protected function _reset() /** * @inheritdoc + * @since 101.0.4 */ public function _loadEntities($printQuery = false, $logQuery = false) { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php index d1259159606d3..d0456ff011027 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php @@ -29,7 +29,7 @@ class EngineProvider /** * @var \Magento\Framework\App\Config\ScopeConfigInterface - * @deprecated since it is not used anymore + * @deprecated 101.0.0 since it is not used anymore */ protected $scopeConfig; diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php index 0835fb66f876a..ad6d37c296012 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php @@ -62,7 +62,7 @@ protected function _construct() * Reset search results * * @return $this - * @deprecated Not used anymore + * @deprecated 101.0.0 Not used anymore * @see Fulltext::resetSearchResultsByStore */ public function resetSearchResults() @@ -78,6 +78,7 @@ public function resetSearchResults() * * @param int $storeId * @return $this + * @since 101.0.0 */ public function resetSearchResultsByStore($storeId) { @@ -115,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/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index e63ed1bd3d72f..06dcc69ef60f5 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -337,6 +337,7 @@ public function addFieldToFilter($field, $condition = null) /** * @inheritDoc + * @since 101.0.4 */ public function clear() { @@ -348,6 +349,7 @@ public function clear() /** * @inheritDoc + * @since 101.0.4 */ protected function _reset() { @@ -359,6 +361,7 @@ protected function _reset() /** * @inheritdoc + * @since 101.0.4 */ public function _loadEntities($printQuery = false, $logQuery = false) { @@ -429,6 +432,7 @@ public function setOrder($attribute, $dir = Select::SQL_DESC) * @param string $attribute * @param string $dir * @return $this + * @since 101.0.2 */ public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) { @@ -555,6 +559,7 @@ private function getSearchResultApplier(SearchResultInterface $searchResult): Se /** * @inheritdoc + * @since 100.2.3 */ protected function _beforeLoad() { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php index b396437fc66c7..a7e9c237f58c3 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php @@ -8,7 +8,7 @@ /** * This class add in backward compatibility purposes to check if need to apply old strategy for filter prepare process. - * @deprecated + * @deprecated 101.0.2 */ class DefaultFilterStrategyApplyChecker implements DefaultFilterStrategyApplyCheckerInterface { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php index a067767775393..d9e41af658089 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php @@ -8,7 +8,7 @@ /** * Added in backward compatibility purposes to check if need to apply old strategy for filter prepare process. - * @deprecated + * @deprecated 101.0.2 */ interface DefaultFilterStrategyApplyCheckerInterface { 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/Model/Search/ReaderPlugin.php b/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php index 916e03f471493..2f6a402b20406 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php +++ b/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php @@ -6,7 +6,7 @@ namespace Magento\CatalogSearch\Model\Search; /** - * @deprecated + * @deprecated 101.0.0 * @see \Magento\ElasticSearch */ class ReaderPlugin diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php index 68ca546b81919..aa3bd1f149c16 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php @@ -9,7 +9,7 @@ /** * @api * @since 100.1.6 - * @deprecated + * @deprecated 101.0.0 * @see \Magento\ElasticSearch */ class GeneratorResolver 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/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml index a6e3dfd7eaad4..1dc57cd083435 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml @@ -25,7 +25,6 @@ <see userInput="{{MinMaxQueryLength.Hint}}" selector="{{AdminCatalogSearchConfigurationSection.maxQueryLengthHint}}" stepKey="seeHint2"/> <uncheckOption selector="{{AdminCatalogSearchConfigurationSection.minQueryLengthInherit}}" stepKey="uncheckSystemValue"/> <fillField selector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" userInput="{{minLength}}" stepKey="setMinQueryLength"/> - <click selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="collapseTab"/> <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> <waitForPageLoad stepKey="waitForConfigSaved"/> <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml index d23663d43dcd0..c02ef4957ad3d 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml @@ -13,8 +13,12 @@ <group value="CatalogSearch"/> </annotations> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByDescriptionActionGroup" stepKey="search"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml index 0b3fb2fa42532..0c8e192f9366e 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml @@ -14,8 +14,12 @@ </annotations> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameActionGroup" stepKey="search"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml index 517e200f8ce11..99c09b5ba93a5 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml @@ -14,8 +14,12 @@ </annotations> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="search"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml index 0bd08d31e8ffa..1e18c5ea4d0a9 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml @@ -14,8 +14,12 @@ </annotations> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByShortDescriptionActionGroup" stepKey="search"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml index d273f9828dc95..34e0a73e91fe0 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml @@ -14,8 +14,12 @@ </annotations> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductSkuActionGroup" stepKey="search"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml index 44bfc66a466a8..d6a5aa8b93572 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml @@ -36,7 +36,7 @@ </after> <actionGroup ref="SetMinimalQueryLengthActionGroup" stepKey="setMinQueryLength"/> <comment userInput="Go to Storefront and search for product" stepKey="searchProdUsingMinQueryLength"/> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> <comment userInput="Quick search by single character and avoid using ES stopwords" stepKey="commentQuickSearch"/> <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="B" stepKey="fillAttribute"/> <waitForPageLoad stepKey="waitForSearchTextBox"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml index 49fce41fddf05..09f7ee455ebb5 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml @@ -44,8 +44,12 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> @@ -53,7 +57,7 @@ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createBundleProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml index 4b0a5c84ac360..98d1b3412360c 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml @@ -55,8 +55,12 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> @@ -67,7 +71,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <comment userInput="$simpleProduct1.name$" stepKey="asdf"/> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createBundleProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml index 35db90363b1ae..3298eff34759b 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml @@ -26,8 +26,12 @@ </actionGroup> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> @@ -37,7 +41,7 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="{{_defaultProduct.name}}"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml index 79a2fc8646c04..85e3c46654502 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml @@ -28,15 +28,19 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml index cf30e4d06e8e7..3488a63140809 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml @@ -28,15 +28,19 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteGroupedProduct" createDataKey="createProduct"/> <deleteData stepKey="deleteSimpleProduct" createDataKey="simple1"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value=""$createProduct.name$""/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml index ba6fa813367c3..26f4cd77b60bc 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml @@ -24,14 +24,18 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createSimpleProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml index b71388f5f409b..9277baba94aa8 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml @@ -24,14 +24,18 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createVirtualProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createVirtualProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml index 566b4d204751d..a6654db91effb 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml @@ -25,15 +25,19 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="ThisShouldn'tReturnAnything"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml index 814e27182799f..83436ebb44c6d 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml @@ -24,14 +24,18 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createSimpleProduct.sku$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchTwoProductsWithSameWeightTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchTwoProductsWithSameWeightTest.xml index e1488f4d000eb..b28a810344e5c 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchTwoProductsWithSameWeightTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchTwoProductsWithSameWeightTest.xml @@ -77,7 +77,7 @@ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="{{_defaultProduct.name}}"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml index 968435747bdbb..14ae988e6ce79 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml @@ -32,8 +32,12 @@ </after> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- 1. Navigate to Frontend --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml index 67e9fdd43f5fe..cceac0475aa78 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml @@ -13,6 +13,7 @@ <stories value="Use Advanced Search"/> <title value="Unable negative price use to advanced search"/> <description value="Check unable negative price use to advanced search by price from and price to"/> + <severity value="MAJOR"/> </annotations> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> <actionGroup ref="StorefrontOpenAdvancedSearchActionGroup" stepKey="openAdvancedSearch"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml index 6f510fa315d7d..67e8bc6bf183c 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml @@ -32,7 +32,7 @@ <!-- Assign attribute to set --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="GoToAttributeGridPageActionGroup" stepKey="goToAttributeSetPage"/> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="goToAttributeSetPage"/> <actionGroup ref="GoToAttributeSetByNameActionGroup" stepKey="openAttributeSetByName"> <argument name="name" value="$createAttributeSet.attribute_set_name$"/> </actionGroup> @@ -73,7 +73,9 @@ </createData> <!-- Perform reindex --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createConfigurableProduct" stepKey="deleteConfigurableProduct"/> @@ -83,7 +85,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createConfigurableProduct.name$"/> </actionGroup> @@ -98,7 +100,7 @@ <actionGroup ref="ToggleProductEnabledActionGroup" stepKey="disableProduct"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePageAgain"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePageAgain"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefrontAgain"> <argument name="phrase" value="$createConfigurableProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml index 8a0d91ae05b34..26280ed67d183 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml @@ -26,8 +26,12 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnStorefrontPage1"/> </before> <after> @@ -35,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"/> @@ -49,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$$"/> @@ -63,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/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Store/GroupTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Store/GroupTest.php index b4a27a4350131..302dd0f1e2684 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Store/GroupTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Store/GroupTest.php @@ -11,7 +11,6 @@ use Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Store\Group as StoreGroupIndexerPlugin; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Store\Model\Group as StoreGroup; use Magento\Store\Model\ResourceModel\Group as StoreGroupResourceModel; use PHPUnit\Framework\MockObject\MockObject; @@ -24,11 +23,6 @@ class GroupTest extends TestCase */ private $plugin; - /** - * @var ObjectManagerHelper - */ - private $objectManagerHelper; - /** * @var IndexerRegistry|MockObject */ @@ -64,11 +58,7 @@ protected function setUp(): void ->setMethods(['dataHasChangedFor', 'isObjectNew']) ->getMock(); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->plugin = $this->objectManagerHelper->getObject( - StoreGroupIndexerPlugin::class, - ['indexerRegistry' => $this->indexerRegistryMock] - ); + $this->plugin = new StoreGroupIndexerPlugin($this->indexerRegistryMock); } /** @@ -76,9 +66,9 @@ protected function setUp(): void * @param bool $websiteChanged * @param int $invalidateCounter * @return void - * @dataProvider beforeAfterSaveDataProvider + * @dataProvider afterSaveDataProvider */ - public function testBeforeAfterSave($isObjectNew, $websiteChanged, $invalidateCounter) + public function testAfterSave(bool $isObjectNew, bool $websiteChanged, int $invalidateCounter): void { $this->prepareIndexer($invalidateCounter); $this->storeGroupMock->expects(static::any()) @@ -91,14 +81,16 @@ public function testBeforeAfterSave($isObjectNew, $websiteChanged, $invalidateCo $this->indexerMock->expects(static::exactly($invalidateCounter)) ->method('invalidate'); - $this->plugin->beforeSave($this->subjectMock, $this->storeGroupMock); - $this->assertSame($this->subjectMock, $this->plugin->afterSave($this->subjectMock, $this->subjectMock)); + $this->assertSame( + $this->subjectMock, + $this->plugin->afterSave($this->subjectMock, $this->subjectMock, $this->storeGroupMock) + ); } /** * @return array */ - public function beforeAfterSaveDataProvider() + public function afterSaveDataProvider(): array { return [ [false, false, 0], @@ -108,13 +100,16 @@ public function beforeAfterSaveDataProvider() ]; } - public function testAfterDelete() + public function testAfterDelete(): void { $this->prepareIndexer(1); $this->indexerMock->expects(static::once()) ->method('invalidate'); - $this->assertSame($this->subjectMock, $this->plugin->afterDelete($this->subjectMock, $this->subjectMock)); + $this->assertSame( + $this->subjectMock, + $this->plugin->afterDelete($this->subjectMock, $this->subjectMock) + ); } /** @@ -123,7 +118,7 @@ public function testAfterDelete() * @param int $invalidateCounter * @return void */ - private function prepareIndexer($invalidateCounter) + private function prepareIndexer(int $invalidateCounter): void { $this->indexerRegistryMock->expects(static::exactly($invalidateCounter)) ->method('get') diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Store/ViewTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Store/ViewTest.php index f778c9340cbd7..23e1c44c5f7c8 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Store/ViewTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Store/ViewTest.php @@ -11,7 +11,6 @@ use Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Store\View as StoreViewIndexerPlugin; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Store\Model\ResourceModel\Store as StoreResourceModel; use Magento\Store\Model\Store; use PHPUnit\Framework\MockObject\MockObject; @@ -24,11 +23,6 @@ class ViewTest extends TestCase */ private $plugin; - /** - * @var ObjectManagerHelper - */ - private $objectManagerHelper; - /** * @var IndexerRegistry|MockObject */ @@ -64,20 +58,16 @@ protected function setUp(): void ->setMethods(['isObjectNew']) ->getMock(); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->plugin = $this->objectManagerHelper->getObject( - StoreViewIndexerPlugin::class, - ['indexerRegistry' => $this->indexerRegistryMock] - ); + $this->plugin = new StoreViewIndexerPlugin($this->indexerRegistryMock); } /** * @param bool $isObjectNew * @param int $invalidateCounter * - * @dataProvider beforeAfterSaveDataProvider + * @dataProvider afterSaveDataProvider */ - public function testBeforeAfterSave($isObjectNew, $invalidateCounter) + public function testAfterSave(bool $isObjectNew, int $invalidateCounter): void { $this->prepareIndexer($invalidateCounter); $this->storeMock->expects(static::once()) @@ -86,14 +76,16 @@ public function testBeforeAfterSave($isObjectNew, $invalidateCounter) $this->indexerMock->expects(static::exactly($invalidateCounter)) ->method('invalidate'); - $this->plugin->beforeSave($this->subjectMock, $this->storeMock); - $this->assertSame($this->subjectMock, $this->plugin->afterSave($this->subjectMock, $this->subjectMock)); + $this->assertSame( + $this->subjectMock, + $this->plugin->afterSave($this->subjectMock, $this->subjectMock, $this->storeMock) + ); } /** * @return array */ - public function beforeAfterSaveDataProvider() + public function afterSaveDataProvider(): array { return [ [false, 0], @@ -101,7 +93,7 @@ public function beforeAfterSaveDataProvider() ]; } - public function testAfterDelete() + public function testAfterDelete(): void { $this->prepareIndexer(1); $this->indexerMock->expects(static::once()) @@ -116,7 +108,7 @@ public function testAfterDelete() * @param int $invalidateCounter * @return void */ - private function prepareIndexer($invalidateCounter) + private function prepareIndexer(int $invalidateCounter): void { $this->indexerRegistryMock->expects(static::exactly($invalidateCounter)) ->method('get') diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml index f158cebf41aae..bec3e57b44798 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml @@ -4,21 +4,23 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -// @codingStandardsIgnoreFile -?> -<?php /** * Catalog advanced search form * * @var $block \Magento\CatalogSearch\Block\Advanced\Form + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php $maxQueryLength = $this->helper(\Magento\CatalogSearch\Helper\Data::class)->getMaxQueryLength();?> -<form class="form search advanced" action="<?= $block->escapeUrl($block->getSearchPostUrl()) ?>" method="get" id="form-validate"> + +<?php +/** @var \Magento\CatalogSearch\Helper\Data $catalogSearchHelper */ +$catalogSearchHelper = $block->getData('catalogSearchHelper'); ?> +<?php $maxQueryLength = $catalogSearchHelper->getMaxQueryLength();?> +<form class="form search advanced" action="<?= $block->escapeUrl($block->getSearchPostUrl()) ?>" method="get" + id="form-validate"> <fieldset class="fieldset"> <legend class="legend"><span><?= $block->escapeHtml(__('Search Settings')) ?></span></legend><br /> - <?php foreach ($block->getSearchableAttributes() as $_attribute) : ?> + <?php foreach ($block->getSearchableAttributes() as $_attribute): ?> <?php $_code = $_attribute->getAttributeCode() ?> <div class="field <?= $block->escapeHtmlAttr($_code) ?>"> <label class="label" for="<?= $block->escapeHtmlAttr($_code) ?>"> @@ -26,7 +28,7 @@ </label> <div class="control"> <?php - switch ($block->getAttributeInputType($_attribute)) : + switch ($block->getAttributeInputType($_attribute)): case 'number': ?> <div class="range fields group group-2"> @@ -39,7 +41,8 @@ title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" class="input-text" maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" - data-validate="{number:true, 'less-than-equals-to':'#<?= $block->escapeHtmlAttr($_code) ?>_to'}" /> + data-validate="{number:true, 'less-than-equals-to':'#<?= + $block->escapeHtmlAttr($_code) ?>_to'}" /> </div> </div> <div class="field no-label"> @@ -51,7 +54,8 @@ title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" class="input-text" maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" - data-validate="{number:true, 'greater-than-equals-to':'#<?= $block->escapeHtmlAttr($_code) ?>'}" /> + data-validate="{number:true, 'greater-than-equals-to':'#<?= + $block->escapeHtmlAttr($_code) ?>'}" /> </div> </div> </div> @@ -126,7 +130,7 @@ id="<?= $block->escapeHtmlAttr($_code) ?>" value="<?= $block->escapeHtml($block->getAttributeValue($_attribute)) ?>" title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" - class="input-text <?= $block->escapeHtmlAttr($block->getAttributeValidationClass($_attribute)) ?>" + class="input-text <?= $block->escapeHtmlAttr($block->getAttributeValidationClass($_attribute))?>" maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" /> <?php endswitch; ?> </div> @@ -143,7 +147,7 @@ </div> </div> </form> -<script> +<?php $scriptString = <<<script require([ "jquery", "mage/mage", @@ -159,9 +163,11 @@ require([ } }, messages: { - 'price[to]': {'greater-than-equals-to': '<?= $block->escapeJs(__('Please enter a valid price range.')) ?>'}, - 'price[from]': {'less-than-equals-to': '<?= $block->escapeJs(__('Please enter a valid price range.')) ?>'} + 'price[to]': {'greater-than-equals-to': '{$block->escapeJs(__('Please enter a valid price range.'))}'}, + 'price[from]': {'less-than-equals-to': '{$block->escapeJs(__('Please enter a valid price range.'))}'} } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php index f8d9ddf0c4ad9..5a339670bbb81 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php @@ -27,13 +27,13 @@ class CurrentUrlRewritesRegenerator /** * @var \Magento\Catalog\Model\Category - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $category; /** * @var \Magento\UrlRewrite\Model\UrlFinderInterface - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $urlFinder; diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php index 217349cb5940c..31bb630718e55 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php @@ -3,14 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogUrlRewrite\Model\Category\Plugin\Store; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\CategoryFactory; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductFactory; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\Framework\Model\AbstractModel; +use Magento\Store\Model\ResourceModel\Store; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; @@ -23,17 +27,17 @@ class View { /** - * @var \Magento\UrlRewrite\Model\UrlPersistInterface + * @var UrlPersistInterface */ protected $urlPersist; /** - * @var \Magento\Catalog\Model\CategoryFactory + * @var CategoryFactory */ protected $categoryFactory; /** - * @var \Magento\Catalog\Model\ProductFactory + * @var ProductFactory */ protected $productFactory; @@ -43,7 +47,7 @@ class View protected $categoryUrlRewriteGenerator; /** - * @var \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator + * @var ProductUrlRewriteGenerator */ protected $productUrlRewriteGenerator; @@ -76,70 +80,62 @@ public function __construct( /** * Setter for Orig Store data * - * @param \Magento\Store\Model\ResourceModel\Store $object + * @param Store $object * @param AbstractModel $store * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeSave( - \Magento\Store\Model\ResourceModel\Store $object, + Store $object, AbstractModel $store - ) { + ): void { $this->origStore = $store; } /** * Regenerate urls on store after save * - * @param \Magento\Store\Model\ResourceModel\Store $object - * @param \Magento\Store\Model\ResourceModel\Store $store - * @return \Magento\Store\Model\ResourceModel\Store + * @param Store $object + * @param Store $store + * @return Store * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterSave( - \Magento\Store\Model\ResourceModel\Store $object, - \Magento\Store\Model\ResourceModel\Store $store - ) { + Store $object, + Store $store + ): Store { if ($this->origStore->isObjectNew() || $this->origStore->dataHasChangedFor('group_id')) { $categoryRewriteUrls = $this->generateCategoryUrls( - $this->origStore->getRootCategoryId(), - $this->origStore->getId() + (int)$this->origStore->getRootCategoryId(), + (int)$this->origStore->getId() ); $this->urlPersist->replace($categoryRewriteUrls); $this->urlPersist->replace( - $this->generateProductUrls( - $this->origStore->getWebsiteId(), - $this->origStore->getOrigData('website_id'), - $this->origStore->getId() - ) + $this->generateProductUrls((int)$this->origStore->getId()) ); } + return $store; } /** - * Generate url rewrites for products assigned to website + * Generate url rewrites for products assigned to store * - * @param int $websiteId - * @param int $originWebsiteId * @param int $storeId * @return array */ - protected function generateProductUrls($websiteId, $originWebsiteId, $storeId) + protected function generateProductUrls(int $storeId): array { $urls = []; - $websiteIds = $websiteId != $originWebsiteId && $originWebsiteId !== null - ? [$websiteId, $originWebsiteId] - : [$websiteId]; $collection = $this->productFactory->create() ->getCollection() ->addCategoryIds() ->addAttributeToSelect(['name', 'url_path', 'url_key', 'visibility']) - ->addWebsiteFilter($websiteIds); + ->addStoreFilter($storeId); foreach ($collection as $product) { - /** @var \Magento\Catalog\Model\Product $product */ + /** @var Product $product */ $product->setStoreId($storeId); $urls[] = $this->productUrlRewriteGenerator->generate($product); } @@ -149,13 +145,13 @@ protected function generateProductUrls($websiteId, $originWebsiteId, $storeId) } /** - * Generate url rewrites for categories + * Generate url rewrites for categories assigned to store * * @param int $rootCategoryId * @param int $storeId * @return array */ - protected function generateCategoryUrls($rootCategoryId, $storeId) + protected function generateCategoryUrls(int $rootCategoryId, int $storeId): array { $urls = []; $categories = $this->categoryFactory->create()->getCategories($rootCategoryId, 1, false, true, false); @@ -173,17 +169,17 @@ protected function generateCategoryUrls($rootCategoryId, $storeId) /** * Delete unused url rewrites * - * @param \Magento\Store\Model\ResourceModel\Store $subject - * @param \Magento\Store\Model\ResourceModel\Store $result + * @param Store $subject + * @param Store $result * @param AbstractModel $store - * @return \Magento\Store\Model\ResourceModel\Store + * @return Store * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterDelete( - \Magento\Store\Model\ResourceModel\Store $subject, - \Magento\Store\Model\ResourceModel\Store $result, + Store $subject, + Store $result, AbstractModel $store - ) { + ): Store { $this->urlPersist->deleteByData([UrlRewrite::STORE_ID => $store->getId()]); return $result; diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php index a86604672e2b4..d48bcd446fcfd 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php @@ -30,7 +30,7 @@ class CategoryUrlRewriteGenerator /** * @var \Magento\Catalog\Model\Category - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $category; diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php index 42d3fd9cb40e1..628615803f6e8 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php @@ -26,19 +26,19 @@ class CurrentUrlRewritesRegenerator { /** * @var Product - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $product; /** * @var ObjectRegistry - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $productCategories; /** * @var UrlFinderInterface - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $urlFinder; diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php index 868c417b5ff52..f5e6ae9a6d615 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php @@ -26,49 +26,49 @@ class ProductUrlRewriteGenerator const ENTITY_TYPE = 'product'; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService */ protected $storeViewService; /** * @var \Magento\Catalog\Model\Product - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $product; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator */ protected $currentUrlRewritesRegenerator; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator */ protected $categoriesUrlRewriteGenerator; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator */ protected $canonicalUrlRewriteGenerator; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory */ protected $objectRegistryFactory; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry */ protected $productCategories; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\Store\Model\StoreManagerInterface */ protected $storeManager; diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/Attributes.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/Attributes.php index ca514be51d99b..8816b2816b797 100644 --- a/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/Attributes.php +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/Attributes.php @@ -3,38 +3,90 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Plugin\Catalog\Block\Adminhtml\Category\Tab; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category\DataProvider; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\CatalogUrlRewrite\Block\UrlKeyRenderer; +use Magento\Store\Model\ScopeInterface; + /** - * Class Attributes + * Category tab attributes */ class Attributes { /** - * @param \Magento\Catalog\Model\Category\DataProvider $subject - * @param array $result + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Adds attributes meta if url_key exist * + * @param DataProvider $subject + * @param array $result * @return array */ - public function afterGetAttributesMeta( - \Magento\Catalog\Model\Category\DataProvider $subject, - $result - ) { - /** @var \Magento\Catalog\Model\Category $category */ + public function afterGetAttributesMeta(DataProvider $subject, $result) + { + if (!isset($result['url_key'])) { + return $result; + } + $category = $subject->getCurrentCategory(); - if (isset($result['url_key'])) { - if ($category && $category->getId()) { - if ($category->getLevel() == 1) { - $result['url_key_group']['componentDisabled'] = true; - } else { - $result['url_key_create_redirect']['valueMap']['true'] = $category->getUrlKey(); - $result['url_key_create_redirect']['value'] = $category->getUrlKey(); - $result['url_key_create_redirect']['disabled'] = true; - } + if ($category && $category->getId()) { + if ((int) $category->getLevel() === 1) { + $result['url_key_group']['componentDisabled'] = true; } else { - $result['url_key_create_redirect']['visible'] = false; + $result['url_key_create_redirect'] = $this->getUrlRewriteMeta($category); } + } else { + $result['url_key_create_redirect']['visible'] = false; } + return $result; } + + /** + * Returns url rewrite meta + * + * @param CategoryInterface $category + * @return array + */ + private function getUrlRewriteMeta(CategoryInterface $category): array + { + return [ + 'value' => $this->isSaveRewriteHistory($category->getStoreId()) ? $category->getUrlKey() : '', + 'valueMap' => [ + 'false' => '', + 'true' => $category->getUrlKey() + ], + 'disabled' => true, + ]; + } + + /** + * Returns Create Permanent Redirect for URLs if changed config enabled + * + * @param int $storeId + * @return bool + */ + private function isSaveRewriteHistory(int $storeId): bool + { + return $this->scopeConfig->isSetFlag( + UrlKeyRenderer::XML_PATH_SEO_SAVE_HISTORY, + ScopeInterface::SCOPE_STORE, + $storeId + ); + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/GenerateCategoryProductUrlRewriteConfigData.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/GenerateCategoryProductUrlRewriteConfigData.xml index 9ce6d397a551b..b4b391326a36f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/GenerateCategoryProductUrlRewriteConfigData.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/GenerateCategoryProductUrlRewriteConfigData.xml @@ -27,4 +27,12 @@ <data key="path">catalog/seo/product_use_categories</data> <data key="value">0</data> </entity> + <entity name="EnableCreatePermanentRedirect"> + <data key="path">catalog/seo/save_rewrites_history</data> + <data key="value">1</data> + </entity> + <entity name="DisableCreatePermanentRedirect"> + <data key="path">catalog/seo/save_rewrites_history</data> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml index 9e4689bd8aa4f..0e4ee26a462e6 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml @@ -17,7 +17,9 @@ <before> <magentoCLI command="config:set {{EnableCategoriesPathProductUrls.path}} {{EnableCategoriesPathProductUrls.value}}" stepKey="enableUseCategoriesPath"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> @@ -36,7 +38,9 @@ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="defaultCategory" stepKey="deleteNewRootCategory"/> <magentoCLI command="config:set {{DisableCategoriesPathProductUrls.path}} {{DisableCategoriesPathProductUrls.value}}" stepKey="disableUseCategoriesPath"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="navigateToCreatedDefaultCategory"> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml index 329f5e8cae3f6..ad426c4bc6c4c 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml @@ -56,8 +56,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Select product and go toUpdate Attribute page--> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="GoToCatalogPageChangingView"/> - <waitForPageLoad stepKey="WaitForPageToLoadFullyChangingView"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToCatalogPageChangingView"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterBundleProductOptionsDownToName"> <argument name="product" value="ApiSimpleProduct"/> </actionGroup> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml new file mode 100644 index 0000000000000..d529c6dd3ecc3 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest"> + <annotations> + <features value="CatalogUrlRewrite"/> + <stories value="Url rewrites"/> + <title value="Verify checkbox is disabled 'Create Permanent Redirect' set 'No'"/> + <description value="Verify checkbox is disabled 'Create Permanent Redirect' set 'No' on category and product edit page."/> + <severity value="AVERAGE"/> + <testCaseId value="MC-35589"/> + </annotations> + <before> + <magentoCLI command="config:set {{DisableCreatePermanentRedirect.path}} {{DisableCreatePermanentRedirect.value}}" stepKey="enableCreatePermanentRedirect"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createDefaultCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <magentoCLI command="config:set {{EnableCreatePermanentRedirect.path}} {{EnableCreatePermanentRedirect.value}}" stepKey="disableCreatePermanentRedirect"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$$createSimpleProduct.id$$"/> + </actionGroup> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="scrollToSeoSection" x="0" y="-120" /> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <grabValueFrom selector="{{AdminProductSEOSection.urlKeyRedirectCheckbox}}" stepKey="grabValue"/> + <assertEmpty stepKey="checkUrlKeyRedirectCheckbox"> + <actualResult type="string">$grabValue</actualResult> + </assertEmpty> + + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedSubCategory"> + <argument name="Category" value="$$createDefaultCategory$$"/> + </actionGroup> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSeoSection1" x="0" y="-120" /> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection1"/> + <grabValueFrom selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="grabValue1"/> + <assertEmpty stepKey="checkUrlKeyRedirectCheckbox1"> + <actualResult type="string">$grabValue1</actualResult> + </assertEmpty> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryAccessibleWhenSuffixIsNullTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryAccessibleWhenSuffixIsNullTest.xml index 99037a5c89af1..4880d438373f4 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryAccessibleWhenSuffixIsNullTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryAccessibleWhenSuffixIsNullTest.xml @@ -21,7 +21,9 @@ <magentoCLI command="config:set catalog/seo/category_url_suffix ''" stepKey="setCategoryUrlSuffix"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="setCategoryProductRewrites"/> - <magentoCLI command="cache:flush" stepKey="flushCacheBefore"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBefore"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="_defaultCategory" stepKey="createCategory"/> </before> <after> @@ -30,7 +32,9 @@ stepKey="restoreCategoryUrlSuffix"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="restoreCategoryProductRewrites"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfter"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfter"> + <argument name="tags" value=""/> + </actionGroup> </after> <amOnPage url="/$$createCategory.name$$" stepKey="onCategoryPage"/> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php index b9f95f4eeb530..61557db883aa1 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php @@ -125,7 +125,7 @@ protected function setUp(): void ->getMock(); $this->productCollectionMock = $this->getMockBuilder(ProductCollection::class) ->disableOriginalConstructor() - ->setMethods(['addCategoryIds', 'addAttributeToSelect', 'addWebsiteFilter', 'getIterator']) + ->setMethods(['addCategoryIds', 'addAttributeToSelect', 'getIterator', 'addStoreFilter']) ->getMock(); $this->productMock = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() @@ -190,7 +190,7 @@ public function testAfterSave(): void ->method('addAttributeToSelect') ->willReturn($this->productCollectionMock); $this->productCollectionMock->expects($this->once()) - ->method('addWebsiteFilter') + ->method('addStoreFilter') ->willReturn($this->productCollectionMock); $iterator = new \ArrayIterator([$this->productMock]); $this->productCollectionMock->expects($this->once()) diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php new file mode 100644 index 0000000000000..8134ecef3db6d --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Plugin\Catalog\Block\Adminhtml\Category\Tab; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Category\DataProvider as CategoryDataProvider; +use Magento\CatalogUrlRewrite\Plugin\Catalog\Block\Adminhtml\Category\Tab\Attributes; +use Magento\Framework\App\Config\ScopeConfigInterface; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\CatalogUrlRewrite\Plugin\Catalog\Block\Adminhtml\Category\Tab\Attributes. + */ +class AttributesTest extends TestCase +{ + private const STUB_CATEGORY_META = ['url_key' => 'url_key_test']; + private const STUB_URL_KEY = 'url_key_777'; + + /** + * @var Attributes + */ + private $model; + + /** + * @var Category|MockObject + */ + private $categoryMock; + + /** + * @var CategoryDataProvider|MockObject + */ + private $dataProviderMock; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->categoryMock = $this->createMock(Category::class); + $this->dataProviderMock = $this->createMock(CategoryDataProvider::class); + $this->dataProviderMock->expects($this->any()) + ->method('getCurrentCategory') + ->willReturn($this->categoryMock); + + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->model = $objectManager->getObject(Attributes::class, ['scopeConfig' => $this->scopeConfigMock]); + } + + /** + * Test get attributes meta + * + * @dataProvider attributesMetaDataProvider + * + * @param bool $configEnabled + * @param string $expectedValue + * @param string $expectedValueMap + * @return void + */ + public function testGetAttributesMeta(bool $configEnabled, string $expectedValue, string $expectedValueMap): void + { + $this->categoryMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + $this->categoryMock->expects($this->once()) + ->method('getLevel') + ->willReturn(2); + $this->categoryMock->expects($this->atMost(2)) + ->method('getUrlKey') + ->willReturn(self::STUB_URL_KEY); + $this->scopeConfigMock->expects($this->once()) + ->method('isSetFlag') + ->willReturn($configEnabled); + $this->categoryMock->expects($this->once()) + ->method('getStoreId') + ->willReturn(1); + + $result = $this->model->afterGetAttributesMeta($this->dataProviderMock, self::STUB_CATEGORY_META); + + $this->assertArrayHasKey('url_key_create_redirect', $result); + + $this->assertArrayHasKey('value', $result['url_key_create_redirect']); + $this->assertEquals($expectedValue, $result['url_key_create_redirect']['value']); + + $this->assertArrayHasKey('valueMap', $result['url_key_create_redirect']); + $this->assertArrayHasKey('true', $result['url_key_create_redirect']['valueMap']); + $this->assertEquals($expectedValueMap, $result['url_key_create_redirect']['valueMap']['true']); + + $this->assertArrayHasKey('disabled', $result['url_key_create_redirect']); + $this->assertTrue($result['url_key_create_redirect']['disabled']); + } + + /** + * DataProvider for testGetAttributesMeta + * + * @return array + */ + public function attributesMetaDataProvider(): array + { + return [ + 'save rewrite history config enabled' => [true, self::STUB_URL_KEY, self::STUB_URL_KEY], + 'save rewrite history config disabled' => [false, '', 'url_key_777'] + ]; + } + + /** + * Test get category without id attributes meta + * + * @return void + */ + public function testGetAttributesMetaWithoutCategoryId(): void + { + $this->categoryMock->expects($this->once()) + ->method('getId') + ->willReturn(null); + + $result = $this->model->afterGetAttributesMeta($this->dataProviderMock, self::STUB_CATEGORY_META); + + $this->assertArrayHasKey('url_key_create_redirect', $result); + $this->assertArrayHasKey('visible', $result['url_key_create_redirect']); + $this->assertFalse($result['url_key_create_redirect']['visible']); + } + + /** + * Test get root category attributes meta + * + * @return void + */ + public function testGetAttributesMetaRootCategory(): void + { + $this->categoryMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + $this->categoryMock->expects($this->once()) + ->method('getLevel') + ->willReturn(1); + + $result = $this->model->afterGetAttributesMeta($this->dataProviderMock, self::STUB_CATEGORY_META); + + $this->assertArrayHasKey('url_key_group', $result); + $this->assertArrayHasKey('componentDisabled', $result['url_key_group']); + $this->assertTrue($result['url_key_group']['componentDisabled']); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml b/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml index 75d395473f969..ccd077e615221 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml @@ -32,7 +32,7 @@ <label>Generate "category/product" URL Rewrites</label> <backend_model>Magento\CatalogUrlRewrite\Model\TableCleaner</backend_model> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <comment><![CDATA[<strong style="color:red">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them.]]></comment> + <comment><![CDATA[<strong class="colorRed">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them.]]></comment> <frontend_class>generate_category_product_rewrites</frontend_class> </field> </group> diff --git a/app/code/Magento/CatalogUrlRewrite/etc/csp_whitelist.xml b/app/code/Magento/CatalogUrlRewrite/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..0af163606fcaf --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/etc/csp_whitelist.xml @@ -0,0 +1,30 @@ +<?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="script-src"> + <values> + <value id="authorize_net_direct" type="host">secure.authorize.net</value> + <value id="authorize_net_direct_test" type="host">test.authorize.net</value> + </values> + </policy> + <policy id="frame-src"> + <values> + <value id="authorize_net_direct" type="host">secure.authorize.net</value> + <value id="authorize_net_direct_test" type="host">test.authorize.net</value> + </values> + </policy> + <policy id="form-action"> + <values> + <value id="authorize_net_direct" type="host">secure.authorize.net</value> + <value id="authorize_net_direct_test" type="host">test.authorize.net</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv index 7f1e1cd086408..0def4f6de32eb 100644 --- a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv +++ b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv @@ -9,4 +9,4 @@ "URL key ""%1"" matches a reserved endpoint name (%2). Use another URL key.","URL key ""%1"" matches a reserved endpoint name (%2). Use another URL key." "Invalid URL key. The ""%1"" URL key can not be used to generate Latin URL key. Please use Latin letters and numbers to avoid generating URL key issues.","Invalid URL key. The ""%1"" URL key can not be used to generate Latin URL key. Please use Latin letters and numbers to avoid generating URL key issues." "Invalid URL key. The ""%1"" category name can not be used to generate Latin URL key. Please add URL key or change category name using Latin letters and numbers to avoid generating URL key issues.","Invalid URL key. The ""%1"" category name can not be used to generate Latin URL key. Please add URL key or change category name using Latin letters and numbers to avoid generating URL key issues." -"<strong style=""color:red"">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them.","<strong style=""color:red"">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them." +"<strong class=""colorRed"">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them.","<strong style=""color:red"">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them." diff --git a/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml b/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml index 800ecfd8a6e2f..9708f89f6377f 100644 --- a/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml +++ b/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml @@ -3,20 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script> + +<?php $scriptString = <<<script require([ "jquery", "Magento_Ui/js/modal/confirm", "mage/translate", - ], function(jQuery, confirmation, $t) { + ], function(jQuery, confirmation, _t) { //confirmation for removing category/product URL rewrites jQuery('select.generate_category_product_rewrites').on('change', function () { if (this.value == 0) { confirmation({ - title: $t('Turn off "category/products" URL rewrites?'), - content: $t('Turning off automatic generation of "category/products" URL rewrites will result in permanent removal of all the currently existing “category/product” type URL rewrites without an ability to restore them back. ' + - 'This may potentially cause unresolved “category/product” type URL conflicts which you have to resolve by updating URL key manually.'), + title: _t('Turn off "category/products" URL rewrites?'), + content: _t('Turning off automatic generation of "category/products" URL rewrites will result in ' + + 'permanent removal of all the currently existing “category/product” type URL rewrites without ' + + 'an ability to restore them back. ' + + 'This may potentially cause unresolved “category/product” type URL conflicts which you have ' + + 'to resolve by updating URL key manually.'), actions: { cancel: function () { jQuery('select.generate_category_product_rewrites').val(1); @@ -27,4 +35,6 @@ } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> 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/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml index 59f0cd7437f44..c40071f4ef263 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml @@ -71,8 +71,7 @@ <conditionalClick selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" dependentSelector="{{AdminCategoryDisplaySettingsSection.displayMode}}" visible="false" stepKey="openDisplaySettingsSection"/> <waitForPageLoad stepKey="waitForDisplaySettingsLoad"/> <selectOption stepKey="selectStaticBlockOnlyOption" userInput="Static block only" selector="{{AdminCategoryDisplaySettingsSection.displayMode}}"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategoryWithProducts"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="seeSuccessMessage"/> <!--Go to Storefront > category--> diff --git a/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml b/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml index 0e21f9e42c995..4b1750eba9f19 100644 --- a/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml +++ b/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml @@ -5,6 +5,7 @@ */ /** @var \Magento\CatalogWidget\Block\Product\Widget\Conditions $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $element = $block->getElement(); $fieldId = $element->getHtmlContainerId() ? ' id="' . $block->escapeHtmlAttr($element->getHtmlContainerId()) . '"' : ''; @@ -25,12 +26,14 @@ $fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' </div> </div> - -<script> +<?php $scriptString = <<<script require([ "Magento_Rule/rules", "prototype" ], function(VarienRulesForm){ - window.<?= $block->escapeJs($block->getHtmlId()) ?> = new VarienRulesForm('<?= $block->escapeJs($block->getHtmlId()) ?>', '<?= $block->escapeUrl($block->getNewChildUrl()) ?>'); + window.{$block->escapeJs($block->getHtmlId())} = new VarienRulesForm('{$block->escapeJs($block->getHtmlId())}', + '{$block->escapeUrl($block->getNewChildUrl())}'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Checkout/Api/AgreementsValidatorInterface.php b/app/code/Magento/Checkout/Api/AgreementsValidatorInterface.php index d97492f31a79d..22d4fdf502f7c 100644 --- a/app/code/Magento/Checkout/Api/AgreementsValidatorInterface.php +++ b/app/code/Magento/Checkout/Api/AgreementsValidatorInterface.php @@ -8,6 +8,7 @@ /** * Interface AgreementsValidatorInterface * @api + * @since 100.0.2 */ interface AgreementsValidatorInterface { diff --git a/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php b/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php index cad1c100c7e5b..361c50cdcfe86 100644 --- a/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php +++ b/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php @@ -8,6 +8,7 @@ /** * Interface PaymentDetailsInterface * @api + * @since 100.0.2 */ interface PaymentDetailsInterface extends \Magento\Framework\Api\ExtensibleDataInterface { diff --git a/app/code/Magento/Checkout/Api/Data/ShippingInformationInterface.php b/app/code/Magento/Checkout/Api/Data/ShippingInformationInterface.php index e4032066a6f10..188d2987e5daf 100644 --- a/app/code/Magento/Checkout/Api/Data/ShippingInformationInterface.php +++ b/app/code/Magento/Checkout/Api/Data/ShippingInformationInterface.php @@ -8,6 +8,7 @@ /** * Interface ShippingInformationInterface * @api + * @since 100.0.2 */ interface ShippingInformationInterface extends \Magento\Framework\Api\CustomAttributesDataInterface { diff --git a/app/code/Magento/Checkout/Api/Data/TotalsInformationInterface.php b/app/code/Magento/Checkout/Api/Data/TotalsInformationInterface.php index a9dd05856b72f..c8234bb560cba 100644 --- a/app/code/Magento/Checkout/Api/Data/TotalsInformationInterface.php +++ b/app/code/Magento/Checkout/Api/Data/TotalsInformationInterface.php @@ -8,6 +8,7 @@ /** * Interface TotalsInformationInterface * @api + * @since 100.0.2 */ interface TotalsInformationInterface extends \Magento\Framework\Api\CustomAttributesDataInterface { diff --git a/app/code/Magento/Checkout/Api/GuestPaymentInformationManagementInterface.php b/app/code/Magento/Checkout/Api/GuestPaymentInformationManagementInterface.php index 63296081ab97c..80c2bb7752b73 100644 --- a/app/code/Magento/Checkout/Api/GuestPaymentInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/GuestPaymentInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for managing guest payment information * @api + * @since 100.0.2 */ interface GuestPaymentInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Api/GuestShippingInformationManagementInterface.php b/app/code/Magento/Checkout/Api/GuestShippingInformationManagementInterface.php index def7442ad4672..6ac5ec9442b6f 100644 --- a/app/code/Magento/Checkout/Api/GuestShippingInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/GuestShippingInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for managing guest shipping address information * @api + * @since 100.0.2 */ interface GuestShippingInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Api/GuestTotalsInformationManagementInterface.php b/app/code/Magento/Checkout/Api/GuestTotalsInformationManagementInterface.php index d2d7dfad609cb..c98d193534d36 100644 --- a/app/code/Magento/Checkout/Api/GuestTotalsInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/GuestTotalsInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for guest quote totals calculation * @api + * @since 100.0.2 */ interface GuestTotalsInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Api/PaymentInformationManagementInterface.php b/app/code/Magento/Checkout/Api/PaymentInformationManagementInterface.php index f80deca1acc5a..b025dc4c7c4a4 100644 --- a/app/code/Magento/Checkout/Api/PaymentInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/PaymentInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for managing quote payment information * @api + * @since 100.0.2 */ interface PaymentInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Api/ShippingInformationManagementInterface.php b/app/code/Magento/Checkout/Api/ShippingInformationManagementInterface.php index 0d22e1485c099..ee8fb42a581c0 100644 --- a/app/code/Magento/Checkout/Api/ShippingInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/ShippingInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for managing customer shipping address information * @api + * @since 100.0.2 */ interface ShippingInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Api/TotalsInformationManagementInterface.php b/app/code/Magento/Checkout/Api/TotalsInformationManagementInterface.php index 60fd254eb199e..f3ecf957f3e06 100644 --- a/app/code/Magento/Checkout/Api/TotalsInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/TotalsInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for quote totals calculation * @api + * @since 100.0.2 */ interface TotalsInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Block/Cart.php b/app/code/Magento/Checkout/Block/Cart.php index 7940c37917624..76bf917f02d8b 100644 --- a/app/code/Magento/Checkout/Block/Cart.php +++ b/app/code/Magento/Checkout/Block/Cart.php @@ -11,6 +11,7 @@ * Shopping cart block * * @api + * @since 100.0.2 */ class Cart extends \Magento\Checkout\Block\Cart\AbstractCart { @@ -239,7 +240,7 @@ public function getItemsCount() * Render pagination HTML * * @return string - * @since 100.2.0 + * @since 100.1.7 */ public function getPagerHtml() { diff --git a/app/code/Magento/Checkout/Block/Cart/Additional/Info.php b/app/code/Magento/Checkout/Block/Cart/Additional/Info.php index 196992cbaf9c8..9bf8c8c8e9b51 100644 --- a/app/code/Magento/Checkout/Block/Cart/Additional/Info.php +++ b/app/code/Magento/Checkout/Block/Cart/Additional/Info.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class Info extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Cart/Coupon.php b/app/code/Magento/Checkout/Block/Cart/Coupon.php index acf3c0922f3c9..98707e7e7c694 100644 --- a/app/code/Magento/Checkout/Block/Cart/Coupon.php +++ b/app/code/Magento/Checkout/Block/Cart/Coupon.php @@ -11,6 +11,7 @@ * Block with apply-coupon form. * * @api + * @since 100.0.2 */ class Coupon extends \Magento\Checkout\Block\Cart\AbstractCart { @@ -44,6 +45,7 @@ public function getCouponCode() /** * @inheritDoc + * @since 100.3.2 */ protected function _prepareLayout() { diff --git a/app/code/Magento/Checkout/Block/Cart/Crosssell.php b/app/code/Magento/Checkout/Block/Cart/Crosssell.php index 99408003b981b..07b95c0769f3a 100644 --- a/app/code/Magento/Checkout/Block/Cart/Crosssell.php +++ b/app/code/Magento/Checkout/Block/Cart/Crosssell.php @@ -25,6 +25,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Crosssell extends AbstractProduct diff --git a/app/code/Magento/Checkout/Block/Cart/Grid.php b/app/code/Magento/Checkout/Block/Cart/Grid.php index bfe4b6ceed9d0..db5d90ecddc16 100644 --- a/app/code/Magento/Checkout/Block/Cart/Grid.php +++ b/app/code/Magento/Checkout/Block/Cart/Grid.php @@ -13,7 +13,7 @@ * custom_items weren't set to cart block * * @api - * @since 100.2.0 + * @since 100.1.7 */ class Grid extends \Magento\Checkout\Block\Cart { @@ -56,7 +56,6 @@ class Grid extends \Magento\Checkout\Block\Cart * @param \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory $itemCollectionFactory * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor * @param array $data - * @since 100.2.0 */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -89,7 +88,7 @@ public function __construct( * Configuration path is Store->Configuration->Sales->Checkout->Shopping Cart->Number of items to display pager * * @return void - * @since 100.2.0 + * @since 100.1.7 */ protected function _construct() { @@ -103,7 +102,7 @@ protected function _construct() /** * {@inheritdoc} - * @since 100.2.0 + * @since 100.1.7 */ protected function _prepareLayout() { @@ -128,7 +127,7 @@ protected function _prepareLayout() * Prepare quote items collection for pager * * @return \Magento\Quote\Model\ResourceModel\Quote\Item\Collection - * @since 100.2.0 + * @since 100.1.7 */ public function getItemsForGrid() { @@ -147,7 +146,7 @@ public function getItemsForGrid() /** * {@inheritdoc} - * @since 100.2.0 + * @since 100.1.7 */ public function getItems() { diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Configure.php b/app/code/Magento/Checkout/Block/Cart/Item/Configure.php index 086518a312f71..c5c3af1d3c8c9 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Configure.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Configure.php @@ -11,6 +11,7 @@ * * @api * @module Checkout + * @since 100.0.2 */ class Configure extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php index c99c9041941b1..830191bd13c40 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php @@ -24,6 +24,7 @@ * @method \Magento\Checkout\Block\Cart\Item\Renderer setDeleteUrl(string) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @since 100.0.2 */ class Renderer extends \Magento\Framework\View\Element\Template implements \Magento\Framework\DataObject\IdentityInterface diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions.php index 3be4f76d8d67e..b2d4ef28347a5 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class Actions extends Text { diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Edit.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Edit.php index 4542f19c4670a..fd34cdc4314f5 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Edit.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Edit.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class Edit extends Generic { diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Remove.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Remove.php index d50eeb1b0a263..b52c7dc4c2131 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Remove.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Remove.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class Remove extends Generic { diff --git a/app/code/Magento/Checkout/Block/Cart/Shipping.php b/app/code/Magento/Checkout/Block/Cart/Shipping.php index 712ee84afd232..749f64ed83a65 100644 --- a/app/code/Magento/Checkout/Block/Cart/Shipping.php +++ b/app/code/Magento/Checkout/Block/Cart/Shipping.php @@ -20,6 +20,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class Shipping extends \Magento\Checkout\Block\Cart\AbstractCart { diff --git a/app/code/Magento/Checkout/Block/Cart/Sidebar.php b/app/code/Magento/Checkout/Block/Cart/Sidebar.php index 147782e501ae4..51f25a41971b1 100644 --- a/app/code/Magento/Checkout/Block/Cart/Sidebar.php +++ b/app/code/Magento/Checkout/Block/Cart/Sidebar.php @@ -11,6 +11,7 @@ * Cart sidebar block * * @api + * @since 100.0.2 */ class Sidebar extends AbstractCart { diff --git a/app/code/Magento/Checkout/Block/Cart/Totals.php b/app/code/Magento/Checkout/Block/Cart/Totals.php index 131e5b157c77a..a0ca67f52d73f 100644 --- a/app/code/Magento/Checkout/Block/Cart/Totals.php +++ b/app/code/Magento/Checkout/Block/Cart/Totals.php @@ -14,6 +14,7 @@ * Totals cart block. * * @api + * @since 100.0.2 */ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart { diff --git a/app/code/Magento/Checkout/Block/Cart/ValidationMessages.php b/app/code/Magento/Checkout/Block/Cart/ValidationMessages.php index 0ec2982b83c01..1429eeb04995d 100644 --- a/app/code/Magento/Checkout/Block/Cart/ValidationMessages.php +++ b/app/code/Magento/Checkout/Block/Cart/ValidationMessages.php @@ -12,6 +12,7 @@ * Shopping cart validation messages block * * @api + * @since 100.0.2 */ class ValidationMessages extends \Magento\Framework\View\Element\Messages { diff --git a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php index 0e7931146b4c4..a566d1f606ba0 100644 --- a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php +++ b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php @@ -412,7 +412,7 @@ protected function getFieldOptions($attributeCode, array $attributeConfig) * * @param array $countryOptions * @return array - * @deprecated 100.2.0 + * @deprecated 100.1.7 */ protected function orderCountryOptions(array $countryOptions) { diff --git a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessorInterface.php b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessorInterface.php index ad14f2a45426d..31a744c7d4d48 100644 --- a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessorInterface.php +++ b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessorInterface.php @@ -13,6 +13,7 @@ * @see \Magento\Checkout\Block\Onepage * * @api + * @since 100.0.2 */ interface LayoutProcessorInterface { diff --git a/app/code/Magento/Checkout/Block/Item/Price/Renderer.php b/app/code/Magento/Checkout/Block/Item/Price/Renderer.php index 2210b1cd9243e..b0f5a6b51a158 100644 --- a/app/code/Magento/Checkout/Block/Item/Price/Renderer.php +++ b/app/code/Magento/Checkout/Block/Item/Price/Renderer.php @@ -12,6 +12,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Renderer extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Onepage.php b/app/code/Magento/Checkout/Block/Onepage.php index e01d5835b4cf0..c335b3909d5fd 100644 --- a/app/code/Magento/Checkout/Block/Onepage.php +++ b/app/code/Magento/Checkout/Block/Onepage.php @@ -9,6 +9,7 @@ * Onepage checkout block * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class Onepage extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Onepage/Failure.php b/app/code/Magento/Checkout/Block/Onepage/Failure.php index 46e56d24a7fa0..70f445173567a 100644 --- a/app/code/Magento/Checkout/Block/Onepage/Failure.php +++ b/app/code/Magento/Checkout/Block/Onepage/Failure.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class Failure extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Onepage/Link.php b/app/code/Magento/Checkout/Block/Onepage/Link.php index b8f3926baa58a..de26fe68287de 100644 --- a/app/code/Magento/Checkout/Block/Onepage/Link.php +++ b/app/code/Magento/Checkout/Block/Onepage/Link.php @@ -10,6 +10,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Link extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Onepage/Success.php b/app/code/Magento/Checkout/Block/Onepage/Success.php index e7cfaf68cc789..f8e286ca14bc8 100644 --- a/app/code/Magento/Checkout/Block/Onepage/Success.php +++ b/app/code/Magento/Checkout/Block/Onepage/Success.php @@ -12,6 +12,7 @@ * One page checkout success page * * @api + * @since 100.0.2 */ class Success extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php index 3b2f1604fae44..27910277617dd 100644 --- a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php +++ b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php @@ -11,6 +11,7 @@ * Displays buttons on shopping cart page * * @api + * @since 100.0.2 */ class QuoteShortcutButtons extends \Magento\Catalog\Block\ShortcutButtons { diff --git a/app/code/Magento/Checkout/Block/Registration.php b/app/code/Magento/Checkout/Block/Registration.php index e880230f50a74..75bc3fa467ad6 100644 --- a/app/code/Magento/Checkout/Block/Registration.php +++ b/app/code/Magento/Checkout/Block/Registration.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class Registration extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Total/DefaultTotal.php b/app/code/Magento/Checkout/Block/Total/DefaultTotal.php index ef113ad73fcc1..a351d73005fe7 100644 --- a/app/code/Magento/Checkout/Block/Total/DefaultTotal.php +++ b/app/code/Magento/Checkout/Block/Total/DefaultTotal.php @@ -5,6 +5,10 @@ */ namespace Magento\Checkout\Block\Total; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Model\ConfigInterface; +use Magento\Checkout\Helper\Data as CheckoutHelper; + /** * Default Total Row Renderer */ @@ -21,11 +25,32 @@ class DefaultTotal extends \Magento\Checkout\Block\Cart\Totals protected $_store; /** - * @return void + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Checkout\Model\Session $checkoutSession + * @param ConfigInterface $salesConfig + * @param array $layoutProcessors + * @param array $data + * @param CheckoutHelper $checkoutHelper */ - protected function _construct() - { - parent::_construct(); + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Customer\Model\Session $customerSession, + \Magento\Checkout\Model\Session $checkoutSession, + ConfigInterface $salesConfig, + array $layoutProcessors = [], + array $data = [], + ?CheckoutHelper $checkoutHelper = null + ) { + $data['checkoutHelper'] = $checkoutHelper ?? ObjectManager::getInstance()->get(CheckoutHelper::class); + parent::__construct( + $context, + $customerSession, + $checkoutSession, + $salesConfig, + $layoutProcessors, + $data + ); $this->_store = $this->_storeManager->getStore(); } @@ -40,6 +65,8 @@ public function getStyle() } /** + * Set Total value. + * * @param float $total * @return $this */ @@ -53,6 +80,8 @@ public function setTotal($total) } /** + * Return store. + * * @return \Magento\Store\Model\Store */ public function getStore() diff --git a/app/code/Magento/Checkout/Controller/Account/Create.php b/app/code/Magento/Checkout/Controller/Account/Create.php index dae0bb98be453..21706186d803d 100644 --- a/app/code/Magento/Checkout/Controller/Account/Create.php +++ b/app/code/Magento/Checkout/Controller/Account/Create.php @@ -10,7 +10,7 @@ use Magento\Framework\Exception\NoSuchEntityException; /** - * @deprecated + * @deprecated 100.2.5 * @see DelegateCreate */ class Create extends \Magento\Framework\App\Action\Action diff --git a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php index 7eb9362031258..66be0c483ed72 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php +++ b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php @@ -104,13 +104,25 @@ private function addOrderItem(Item $item) if ($orderCustomerId == $currentCustomerId) { $this->cart->addOrderItem($item, 1); if (!$this->cart->getQuote()->getHasError()) { - $message = __( - 'You added %1 to your shopping cart.', - $this->escaper->escapeHtml($item->getName()) + $this->messageManager->addComplexSuccessMessage( + 'addCartSuccessMessage', + [ + 'product_name' => $item->getName(), + 'cart_url' => $this->getCartUrl() + ] ); - $this->messageManager->addSuccessMessage($message); } } } } + + /** + * Returns cart url + * + * @return string + */ + private function getCartUrl() + { + return $this->_url->getUrl('checkout/cart', ['_secure' => true]); + } } diff --git a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php index f50d8843a5f9d..239fdce499ffe 100644 --- a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php +++ b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php @@ -19,6 +19,9 @@ use Magento\Framework\Exception\LocalizedException; use Psr\Log\LoggerInterface; +/** + * Controller for removing quote item from shopping cart. + */ class RemoveItem extends Action implements HttpPostActionInterface { /** @@ -96,6 +99,9 @@ public function execute() $this->sidebar->removeQuoteItem($itemId); } catch (LocalizedException $e) { $error = $e->getMessage(); + } catch (\Zend_Db_Exception $e) { + $this->logger->critical($e); + $error = __('An unspecified error occurred. Please contact us for assistance.'); } catch (Exception $e) { $this->logger->critical($e); $error = $e->getMessage(); diff --git a/app/code/Magento/Checkout/CustomerData/AbstractItem.php b/app/code/Magento/Checkout/CustomerData/AbstractItem.php index 9c2e3a32ef901..e5ed511924a7b 100644 --- a/app/code/Magento/Checkout/CustomerData/AbstractItem.php +++ b/app/code/Magento/Checkout/CustomerData/AbstractItem.php @@ -12,6 +12,7 @@ * Abstract item * * @api + * @since 100.0.2 */ abstract class AbstractItem implements ItemInterface { diff --git a/app/code/Magento/Checkout/CustomerData/ItemInterface.php b/app/code/Magento/Checkout/CustomerData/ItemInterface.php index fb8bd831f1ccd..fc8d954387b89 100644 --- a/app/code/Magento/Checkout/CustomerData/ItemInterface.php +++ b/app/code/Magento/Checkout/CustomerData/ItemInterface.php @@ -12,6 +12,7 @@ * Item interface * * @api + * @since 100.0.2 */ interface ItemInterface { diff --git a/app/code/Magento/Checkout/Exception.php b/app/code/Magento/Checkout/Exception.php index 4957e6be9b5da..6297041e065aa 100644 --- a/app/code/Magento/Checkout/Exception.php +++ b/app/code/Magento/Checkout/Exception.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class Exception extends \Magento\Framework\Exception\LocalizedException { diff --git a/app/code/Magento/Checkout/Model/Cart.php b/app/code/Magento/Checkout/Model/Cart.php index cec99909dc999..3c1a70ef7a3d6 100644 --- a/app/code/Magento/Checkout/Model/Cart.php +++ b/app/code/Magento/Checkout/Model/Cart.php @@ -18,8 +18,10 @@ * @api * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @deprecated 100.1.0 Use \Magento\Quote\Model\Quote instead * @see \Magento\Quote\Api\Data\CartInterface + * @since 100.0.2 */ class Cart extends DataObject implements CartInterface { @@ -272,6 +274,10 @@ public function addOrderItem($orderItem, $qtyFlag = null) * with the same id may have different sets of order attributes. */ $product = $this->productRepository->getById($orderItem->getProductId(), false, $storeId, true); + if ($orderItem->getOrderId() !== null) { + //reorder existing order + $product->setSkipCheckRequiredOption(true); + } } catch (NoSuchEntityException $e) { return $this; } @@ -282,7 +288,14 @@ public function addOrderItem($orderItem, $qtyFlag = null) } else { $info->setQty(1); } - + $productOptions = $orderItem->getProductOptions(); + if ($productOptions !== null && !empty($productOptions['options'])) { + $formattedOptions = []; + foreach ($productOptions['options'] as $option) { + $formattedOptions[$option['option_id']] = $option['option_value']; + } + $info->setData('options', $formattedOptions); + } $this->addProduct($product, $info); } return $this; @@ -291,8 +304,8 @@ public function addOrderItem($orderItem, $qtyFlag = null) /** * Get product object based on requested product information * - * @param Product|int|string $productInfo - * @return Product + * @param Product|int|string $productInfo + * @return Product * @throws \Magento\Framework\Exception\LocalizedException */ protected function _getProduct($productInfo) @@ -332,8 +345,8 @@ protected function _getProduct($productInfo) /** * Get request for product add to cart procedure * - * @param \Magento\Framework\DataObject|int|array $requestInfo - * @return \Magento\Framework\DataObject + * @param \Magento\Framework\DataObject|int|array $requestInfo + * @return \Magento\Framework\DataObject * @throws \Magento\Framework\Exception\LocalizedException */ protected function _getProductRequest($requestInfo) diff --git a/app/code/Magento/Checkout/Model/Cart/CartInterface.php b/app/code/Magento/Checkout/Model/Cart/CartInterface.php index 40aff1980e787..d8264e5535497 100644 --- a/app/code/Magento/Checkout/Model/Cart/CartInterface.php +++ b/app/code/Magento/Checkout/Model/Cart/CartInterface.php @@ -14,6 +14,7 @@ * @author Magento Core Team <core@magentocommerce.com> * @deprecated 100.1.0 Use \Magento\Quote\Api\Data\CartInterface instead * @see \Magento\Quote\Api\Data\CartInterface + * @since 100.0.2 */ interface CartInterface { diff --git a/app/code/Magento/Checkout/Model/Cart/ImageProvider.php b/app/code/Magento/Checkout/Model/Cart/ImageProvider.php index cdadf3573c8ec..bc409357bf409 100644 --- a/app/code/Magento/Checkout/Model/Cart/ImageProvider.php +++ b/app/code/Magento/Checkout/Model/Cart/ImageProvider.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class ImageProvider { @@ -20,12 +21,15 @@ class ImageProvider /** * @var \Magento\Checkout\CustomerData\ItemPoolInterface - * @deprecated No need for the pool as images are resolved in the default item implementation + * @deprecated 100.2.7 No need for the pool as images are resolved in the default item implementation * @see \Magento\Checkout\CustomerData\DefaultItem::getProductForThumbnail */ protected $itemPool; - /** @var \Magento\Checkout\CustomerData\DefaultItem */ + /** + * @var \Magento\Checkout\CustomerData\DefaultItem + * @since 100.2.7 + */ protected $customerDataItem; /** diff --git a/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php b/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php index f38e15dd628fd..ee68ef9d275b1 100644 --- a/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php +++ b/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php @@ -20,7 +20,6 @@ class RequestInfoFilterComposite implements RequestInfoFilterInterface /** * @param RequestInfoFilter[] $filters - * @since 100.1.2 */ public function __construct( $filters = [] diff --git a/app/code/Magento/Checkout/Model/CompositeConfigProvider.php b/app/code/Magento/Checkout/Model/CompositeConfigProvider.php index 3577b1a145403..7c6d04f2947a0 100644 --- a/app/code/Magento/Checkout/Model/CompositeConfigProvider.php +++ b/app/code/Magento/Checkout/Model/CompositeConfigProvider.php @@ -10,6 +10,7 @@ * * @see \Magento\Checkout\Model\ConfigProviderInterface * @api + * @since 100.0.2 */ class CompositeConfigProvider implements ConfigProviderInterface { diff --git a/app/code/Magento/Checkout/Model/ConfigProviderInterface.php b/app/code/Magento/Checkout/Model/ConfigProviderInterface.php index 9e15027e26927..58bbc02485642 100644 --- a/app/code/Magento/Checkout/Model/ConfigProviderInterface.php +++ b/app/code/Magento/Checkout/Model/ConfigProviderInterface.php @@ -8,6 +8,7 @@ /** * Interface ConfigProviderInterface * @api + * @since 100.0.2 */ interface ConfigProviderInterface { diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index 1d15a5dd7f176..8b8d2602fbfc7 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -152,7 +152,7 @@ public function getPaymentInformation($cartId) * Get logger instance * * @return \Psr\Log\LoggerInterface - * @deprecated 100.2.0 + * @deprecated 100.1.8 */ private function getLogger() { diff --git a/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php b/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php index ca577ed714a6e..a670482cb98d6 100644 --- a/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php +++ b/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php @@ -17,6 +17,7 @@ * * phpcs:disable Magento2.Classes.AbstractApi * @api + * @since 100.0.2 */ abstract class AbstractTotalsProcessor { diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index 1f7931d7d3e6a..2f68aba5ec6ae 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -17,7 +17,7 @@ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInfor { /** * @var \Magento\Quote\Api\BillingAddressManagementInterface - * @deprecated 100.2.0 This call was substituted to eliminate extra quote::save call + * @deprecated 100.1.0 This call was substituted to eliminate extra quote::save call */ protected $billingAddressManagement; @@ -152,7 +152,7 @@ public function getPaymentInformation($cartId) * Get logger instance * * @return \Psr\Log\LoggerInterface - * @deprecated 100.2.0 + * @deprecated 100.1.8 */ private function getLogger() { diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php index 7af00f1df8e95..0addbf069cba3 100644 --- a/app/code/Magento/Checkout/Model/Session.php +++ b/app/code/Magento/Checkout/Model/Session.php @@ -20,6 +20,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.TooManyFields) + * @since 100.0.2 */ class Session extends \Magento\Framework\Session\SessionManager { @@ -290,6 +291,7 @@ public function getQuote() } } else { $quote->setIsCheckoutCart(true); + $quote->setCustomerIsGuest(1); $this->_eventManager->dispatch('checkout_quote_init', ['quote' => $quote]); } } @@ -381,8 +383,10 @@ public function loadCustomerQuote() if ($customerQuote->getId() && $this->getQuoteId() != $customerQuote->getId()) { if ($this->getQuoteId()) { + $quote = $this->getQuote(); + $quote->setCustomerIsGuest(0); $this->quoteRepository->save( - $customerQuote->merge($this->getQuote())->collectTotals() + $customerQuote->merge($quote)->collectTotals() ); $newQuote = $this->quoteRepository->get($customerQuote->getId()); $this->quoteRepository->save( @@ -401,6 +405,7 @@ public function loadCustomerQuote() $this->getQuote()->getBillingAddress(); $this->getQuote()->getShippingAddress(); $this->getQuote()->setCustomer($this->_customerSession->getCustomerDataObject()) + ->setCustomerIsGuest(0) ->setTotalsCollectedFlag(false) ->collectTotals(); $this->quoteRepository->save($this->getQuote()); diff --git a/app/code/Magento/Checkout/Model/Session/SuccessValidator.php b/app/code/Magento/Checkout/Model/Session/SuccessValidator.php index 5858dcba8b902..6bfab606445fb 100644 --- a/app/code/Magento/Checkout/Model/Session/SuccessValidator.php +++ b/app/code/Magento/Checkout/Model/Session/SuccessValidator.php @@ -9,6 +9,7 @@ * Test if checkout session valid for success action * * @api + * @since 100.0.2 */ class SuccessValidator { diff --git a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php index cbbbd9a9b4d01..f397a8ddc9cf1 100644 --- a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php +++ b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php @@ -209,8 +209,11 @@ public function saveAddressInformation( if (!$quote->getIsVirtual() && !$shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()) ) { - throw new NoSuchEntityException( + $errorMessage = $methodCode ? __('Carrier with such method not found: %1, %2', $carrierCode, $methodCode) + : __('The shipping method is missing. Select the shipping method and try again.'); + throw new NoSuchEntityException( + $errorMessage ); } diff --git a/app/code/Magento/Checkout/Model/TotalsInformationManagement.php b/app/code/Magento/Checkout/Model/TotalsInformationManagement.php index efb638d299864..7328f8845545c 100644 --- a/app/code/Magento/Checkout/Model/TotalsInformationManagement.php +++ b/app/code/Magento/Checkout/Model/TotalsInformationManagement.php @@ -6,7 +6,7 @@ namespace Magento\Checkout\Model; /** - * Class TotalsInformationManagement + * Class for management of totals information. */ class TotalsInformationManagement implements \Magento\Checkout\Api\TotalsInformationManagementInterface { @@ -38,7 +38,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritDoc */ public function calculate( $cartId, @@ -52,9 +52,11 @@ public function calculate( $quote->setBillingAddress($addressInformation->getAddress()); } else { $quote->setShippingAddress($addressInformation->getAddress()); - $quote->getShippingAddress()->setCollectShippingRates(true)->setShippingMethod( - $addressInformation->getShippingCarrierCode() . '_' . $addressInformation->getShippingMethodCode() - ); + if ($addressInformation->getShippingCarrierCode() && $addressInformation->getShippingMethodCode()) { + $quote->getShippingAddress()->setCollectShippingRates(true)->setShippingMethod( + $addressInformation->getShippingCarrierCode().'_'.$addressInformation->getShippingMethodCode() + ); + } } $quote->collectTotals(); @@ -62,6 +64,8 @@ public function calculate( } /** + * Check if quote have items. + * * @param \Magento\Quote\Model\Quote $quote * @throws \Magento\Framework\Exception\LocalizedException * @return void diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminOpenSalesCheckoutConfigPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminOpenSalesCheckoutConfigPageActionGroup.xml new file mode 100644 index 0000000000000..4e76e3113bdb8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminOpenSalesCheckoutConfigPageActionGroup.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="AdminOpenSalesCheckoutConfigPageActionGroup"> + <annotations> + <description>Goes to the Store Configuration > Sales > Checkout configuration page in admin.</description> + </annotations> + <arguments> + <argument name="tabGroupAnchor" type="string" defaultValue=""/> + </arguments> + <amOnPage url="{{AdminCheckoutConfigPage.url(tabGroupAnchor)}}" stepKey="openCheckoutConfigPage"/> + <waitForPageLoad stepKey="waitForCheckoutConfigPageLoad"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminSelectClearShoppingCartConfigurationActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminSelectClearShoppingCartConfigurationActionGroup.xml new file mode 100644 index 0000000000000..7d9cc0ca90d4e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminSelectClearShoppingCartConfigurationActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSelectClearShoppingCartConfigurationActionGroup"> + <annotations> + <description>Enable/Disable clear shopping cart store configuration using UI.</description> + </annotations> + <arguments> + <argument name="value" type="string" defaultValue="{{EnableClearShoppingCart.textValue}}"/> + </arguments> + <waitForElementVisible selector="{{AdminCheckoutConfigSection.clearShoppingCartEnabledInherit}}" stepKey="waitForClearShoppingCartEnabledInherit" /> + <uncheckOption selector="{{AdminCheckoutConfigSection.clearShoppingCartEnabledInherit}}" stepKey="uncheckUseSystem" /> + <waitForElementVisible selector="{{AdminCheckoutConfigSection.clearShoppingCartEnabled}}" stepKey="waitForClearShoppingCartEnabled" /> + <selectOption selector="{{AdminCheckoutConfigSection.clearShoppingCartEnabled}}" userInput="{{value}}" stepKey="fillClearShoppingCartEnabled" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml index c3f3865ef4549..c81540382c86f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml @@ -16,7 +16,8 @@ <argument name="selector" type="string"/> <argument name="userInput" type="string"/> </arguments> - + + <waitForElementVisible selector="{{selector}}" time="60" stepKey="waitForElementVisible"/> <see selector="{{selector}}" userInput="{{userInput}}" stepKey="assertElement"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml index e2d4fd2e89c2f..daa27b9918e47 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml @@ -19,6 +19,7 @@ <argument name="qty" type="string"/> </arguments> + <waitForElementVisible selector="{{CheckoutCartProductSection.productName}}" time="60" stepKey="waitForProductNameVisible"/> <see selector="{{CheckoutCartProductSection.productName}}" userInput="{{productName}}" stepKey="seeProductNameInCheckoutSummary"/> <see selector="{{CheckoutCartProductSection.ProductPriceByName(productName)}}" userInput="{{productPrice}}" stepKey="seeProductPriceInCart"/> <see selector="{{CheckoutCartProductSection.productSubtotalByName(productName)}}" userInput="{{subtotal}}" stepKey="seeSubtotalPrice"/> 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/CheckoutFillEstimateShippingAndTaxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutFillEstimateShippingAndTaxActionGroup.xml index f564e14989e75..49b950fd51fdc 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutFillEstimateShippingAndTaxActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutFillEstimateShippingAndTaxActionGroup.xml @@ -13,10 +13,11 @@ <argument name="address" defaultValue="US_Address_TX" type="entity"/> </arguments> <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.estimateShippingAndTaxSummary}}" visible="false" stepKey="openShippingDetails"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.country}}" stepKey="waitForSummarySectionLoad"/> <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="{{address.country_id}}" stepKey="selectCountry"/> <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{address.state}}" stepKey="selectState"/> <waitForElementVisible selector="{{CheckoutCartSummarySection.postcode}}" stepKey="waitForPostCodeVisible"/> <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{address.postcode}}" stepKey="selectPostCode"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDiappear"/> </actionGroup> -</actionGroups> \ No newline at end of file +</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/ActionGroup/StorefrontCheckoutClickNextButtonActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextButtonActionGroup.xml new file mode 100644 index 0000000000000..7b089bd26ca0b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextButtonActionGroup.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="StorefrontCheckoutClickNextButtonActionGroup"> + <annotations> + <description>Clicks on the 'Next' button on checkout.</description> + </annotations> + + <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickOnNextButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClearShoppingCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClearShoppingCartActionGroup.xml new file mode 100644 index 0000000000000..2582cba5a6871 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClearShoppingCartActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClearShoppingCartActionGroup"> + <annotations> + <description>Clicks the Clear Shopping Cart button on the storefront on the shopping cart page and verifies shopping cart gets emptied.</description> + </annotations> + + <waitForElementVisible selector="{{CheckoutCartProductSection.emptyCartButton}}" stepKey="waitForEmptyCartButton"/> + <click selector="{{CheckoutCartProductSection.emptyCartButton}}" stepKey="clickEmptyCartButton"/> + <waitForElementVisible selector="{{CheckoutCartProductSection.modalMessage}}" stepKey="waitForModalMessage"/> + <waitForText selector="{{CheckoutCartProductSection.modalMessage}}" userInput="Are you sure you want to remove all items from your shopping cart?" stepKey="waitForTextModalMessage"/> + <waitForElementVisible selector="{{CheckoutCartProductSection.modalConfirmButton}}" stepKey="waitForModalConfirmButton"/> + <click selector="{{CheckoutCartProductSection.modalConfirmButton}}" stepKey="clickModalConfirmButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeCurrentUrlEquals url="{{_ENV.MAGENTO_BASE_URL}}checkout/cart" stepKey="seeCurrentUrlEqualsCartPage"/> + <waitForText selector="{{CheckoutCartMessageSection.emptyCartMessage}}" userInput="You have no items in your shopping cart." stepKey="waitForEmptyCartMessage"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml index bb47a2fcc3070..216f01a95e890 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml @@ -81,6 +81,13 @@ <data key="label">All Allowed Countries</data> <data key="value">0</data> </entity> + <entity name="EnableFlatRateShowMethodNoApplicableConfigData"> + <data key="path">carriers/flatrate/showmethod</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">Show Method if Not Applicable</data> + <data key="value">1</data> + </entity> <entity name="DisableFlatRateConfigData"> <data key="path">carriers/flatrate/active</data> <data key="scope">carriers</data> @@ -100,4 +107,17 @@ <data key="label">Display number of items in cart</data> <data key="value">0</data> </entity> + + <entity name="EnableClearShoppingCart"> + <data key="path">checkout/cart/enable_clear_shopping_cart</data> + <data key="label">Display clear shopping cart button on the cart page</data> + <data key="value">1</data> + <data key="textValue">Yes</data> + </entity> + <entity name="DisableClearShoppingCart"> + <data key="path">checkout/cart/enable_clear_shopping_cart</data> + <data key="label">Do not display clear shopping cart button on the cart page</data> + <data key="value">0</data> + <data key="textValue">No</data> + </entity> </entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/AdminCheckoutConfigPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/AdminCheckoutConfigPage.xml new file mode 100644 index 0000000000000..21d69a1ad93c7 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Page/AdminCheckoutConfigPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCheckoutConfigPage" url="admin/system_config/edit/section/checkout/{{tabLink}}" area="admin" parameterized="true" module="Magento_Checkout"> + <section name="AdminCheckoutConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutConfigSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutConfigSection.xml new file mode 100644 index 0000000000000..72cba8349ec0b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutConfigSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCheckoutConfigSection"> + <element name="clearShoppingCartEnabled" type="select" selector="#checkout_cart_enable_clear_shopping_cart" timeout="30"/> + <element name="clearShoppingCartEnabledInherit" type="select" selector="#checkout_cart_enable_clear_shopping_cart_inherit" timeout="30"/> + </section> +</sections> 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/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index af9d81249e8ac..84f9a7930d40b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -48,6 +48,9 @@ <element name="checkoutCartProductPrice" type="text" selector="//td[@class='col price']//span[@class='price']"/> <element name="checkoutCartSubtotal" type="text" selector="//td[@class='col subtotal']//span[@class='price']"/> <element name="emptyCart" selector=".cart-empty" type="text"/> + <element name="emptyCartButton" type="button" selector="#empty_cart_button" timeout="30"/> + <element name="modalMessage" type="text" selector=".modal-popup.confirm._show .modal-content" timeout="30"/> + <element name="modalConfirmButton" type="button" selector=".modal-popup.confirm._show .action-accept" timeout="30"/> <!-- Required attention section --> <element name="removeProductBySku" type="button" selector="//div[contains(., '{{sku}}')]/ancestor::tbody//button" parameterized="true" timeout="30"/> <element name="failedItemBySku" type="block" selector="//div[contains(.,'{{sku}}')]/ancestor::tbody" parameterized="true" timeout="30"/> 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/Section/CheckoutShippingMethodsSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml index 2f49e4f422a6e..233cc539e08a6 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml @@ -20,5 +20,6 @@ <element name="shippingMethodLoader" type="button" selector="//div[contains(@class, 'checkout-shipping-method')]/following-sibling::div[contains(@class, 'loading-mask')]"/> <element name="freeShippingShippingMethod" type="input" selector="#s_method_freeshipping_freeshipping" timeout="30"/> <element name="noQuotesMsg" type="text" selector="#checkout-step-shipping_method div"/> + <element name="price" type="text" selector="//*[@id='checkout-shipping-method-load']//td[@class='col col-price']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 29a1b72947c06..3f3d9faf3f17d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -9,7 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontProductInfoMainSection"> - <element name="AddToCart" type="button" selector="#product-addtocart-button"/> - <element name="updateCart" type="button" selector="#product-updatecart-button" timeout="30"/> + <element name="AddToCart" type="button" selector="button#product-addtocart-button"/> + <element name="updateCart" type="button" selector="button#product-updatecart-button" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index 52a69307550c5..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"/> @@ -55,8 +55,7 @@ <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{UK_Address.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml index dd454d7aca10b..7c4b18e1aab89 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml @@ -23,8 +23,12 @@ <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -34,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/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml index ab0453e1faa18..a1065daedd4f8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml @@ -84,7 +84,9 @@ <actionGroup ref="AdminChangeFlatRateShippingMethodStatusActionGroup" stepKey="enableFlatRateShippingStatus"/> <!-- Flush cache --> - <magentoCLI command="cache:flush" stepKey="cacheFlush"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Back to the Checkout and refresh the page --> <switchToPreviousTab stepKey="switchToPreviousTab"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml index febeaa05be43e..3b15b9b4e0449 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml @@ -23,7 +23,22 @@ </before> <after> + <!--Cancel orders--> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="goToOrdersPage"/> + <actionGroup ref="AdminGridColumnShowActionGroup" stepKey="showCustomerEmailColumn"> + <argument name="columnLabel" value="Customer Email"/> + </actionGroup> + <actionGroup ref="AdminGridFilterFillInputFieldActionGroup" stepKey="filterOrdersByCustomerEmail"> + <argument name="filterInputName" value="customer_email"/> + <argument name="filterValue" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="AdminGridFilterApplyActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminGridBulkActionGroup" stepKey="cancelOrders"> + <argument name="actionLabel" value="Cancel"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsRegisterCustomerTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsRegisterCustomerTest.xml index a8bdde867a445..8836d54187cbb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsRegisterCustomerTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsRegisterCustomerTest.xml @@ -26,10 +26,25 @@ </before> <after> + <!--Cancel orders--> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="goToOrdersPage"/> + <actionGroup ref="AdminGridColumnShowActionGroup" stepKey="showCustomerEmailColumn"> + <argument name="columnLabel" value="Customer Email"/> + </actionGroup> + <actionGroup ref="AdminGridFilterFillInputFieldActionGroup" stepKey="filterOrdersByCustomerEmail"> + <argument name="filterInputName" value="customer_email"/> + <argument name="filterValue" value="$$createSimpleUsCustomer.email$$"/> + </actionGroup> + <actionGroup ref="AdminGridFilterApplyActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminGridBulkActionGroup" stepKey="cancelOrders"> + <argument name="actionLabel" value="Cancel"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <!--Logout from customer account--> <amOnPage url="{{StorefrontCustomerLogoutPage.url}}" stepKey="logoutCustomerOne"/> <waitForPageLoad stepKey="waitLogoutCustomerOne"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> </after> @@ -52,8 +67,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <!--Click Place Order button--> @@ -78,8 +92,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2"/> <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod2"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton2"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext2"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext2"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> @@ -103,8 +116,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart3"/> <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod3"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton3"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext3"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext3"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment3"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml new file mode 100644 index 0000000000000..92a4b9563ab3d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.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="ClearShoppingCartEnableDisableConfigurationTest"> + <annotations> + <features value="Checkout"/> + <stories value="Shopping Cart"/> + <title value="Enable and Disable Clear Shopping Cart Configuration"/> + <description value="Verify that disabling the clear shopping cart store configuration will remove the clear shopping cart configuration button from the storefront's shopping cart page. Verify that enabling the configuration will add the button to the page and that the button functions as expected"/> + <group value="shoppingCart"/> + <severity value="MAJOR"/> + </annotations> + <before> + <!-- Create simple products and category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + + <!-- Disable clear shopping cart --> + <magentoCLI command="config:set {{DisableClearShoppingCart.path}} {{DisableClearShoppingCart.value}}" stepKey="disableClearShoppingCart"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Navigate to sales checkout cart configuration --> + <actionGroup ref="AdminOpenSalesCheckoutConfigPageActionGroup" stepKey="openSalesCheckoutCartConfig1"> + <argument name="tabGroupAnchor" value="#checkout_cart-link"/> + </actionGroup> + + <!-- Enable clear shopping cart button --> + <actionGroup ref="AdminSelectClearShoppingCartConfigurationActionGroup" stepKey="enableClearShoppingCartButton"/> + <actionGroup ref="SaveStoreConfigurationActionGroup" stepKey="saveStoreConfiguration1"/> + + <!-- Open product 1 and add to cart --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct1Page1"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product1AddToCart"/> + + <!-- Open product 2 and add to cart --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct2Page"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product2AddToCart"/> + + <!-- Go to shopping cart page --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage1"/> + + <!-- Clear shopping cart --> + <actionGroup ref="StorefrontClearShoppingCartActionGroup" stepKey="clearShoppingCart"/> + <actionGroup ref="AssertMiniCartEmptyActionGroup" stepKey="assertMiniCartEmpty"/> + + <!-- Return to Admin to disable clear shopping cart --> + <actionGroup ref="AdminOpenSalesCheckoutConfigPageActionGroup" stepKey="openSalesCheckoutCartConfig2"/> + <actionGroup ref="AdminSelectClearShoppingCartConfigurationActionGroup" stepKey="disableClearShoppingCartButton"> + <argument name="value" value="{{DisableClearShoppingCart.textValue}}"/> + </actionGroup> + <actionGroup ref="SaveStoreConfigurationActionGroup" stepKey="saveStoreConfiguration2"/> + + <!-- Open product 1 page and add to cart --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct1Page2"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product1AddToCart2"/> + + <!-- Go to shopping cart and assert clear shopping cart button is not rendered in UI --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage2"/> + <dontSeeElementInDOM selector="{{CheckoutCartProductSection.emptyCartButton}}" stepKey="dontSeeElementEmptyCartButton"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml index 6e3df1c4ed724..c395f32c164bb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml @@ -71,8 +71,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> <!-- Customer placed order with payment method save --> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> <!-- Fill Paypal card data --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml index af7718eae69ed..370964a973432 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml @@ -55,7 +55,7 @@ <!--Select Shipping Rate "Flat Rate" and click "Next" button--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> <!--Verify that "My billing and shipping address are the same" is unchecked and billing address is preselected--> <dontSeeCheckboxIsChecked selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="shippingAndBillingAddressIsSameUnchecked"/> <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" stepKey="assertBillingAddress"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml index 5fd201290655a..96a236336993f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml @@ -38,7 +38,9 @@ <requiredEntity createDataKey="bundleOption"/> <requiredEntity createDataKey="createSimpleProduct"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete category --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml index 603ee1ecea4df..b64b59ef6109c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml @@ -33,7 +33,9 @@ <requiredEntity createDataKey="createBundleOption"/> <requiredEntity createDataKey="createSimpleProduct"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete bundle product data --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml index c61545e51d535..3d53fffcfa492 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml @@ -26,8 +26,12 @@ </createData> <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> <!--Clear cache and reindex--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -71,7 +75,7 @@ <!-- Go to *Next* --> <scrollTo selector="{{CheckoutShippingMethodsSection.next}}" stepKey="scrollToButtonNext"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="goNext"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="goNext"/> <!-- Select payment solution --> <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution" /> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml index ceaf72fff83bb..bf942e70cfa36 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml @@ -26,8 +26,12 @@ <field key="price">100.00</field> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> @@ -36,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/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml index 3215503b3205a..e90e1bf5a2e82 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml @@ -85,8 +85,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Open created order in backend --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml index 76a998fec8adc..d867b00310761 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml @@ -73,8 +73,7 @@ <waitForPageLoad stepKey="waitForAddressSaving"/> <!-- Click next button to open payment section --> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForPageLoad stepKey="waitForShipmentPageLoad"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!-- Change the address --> <uncheckOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution"/> @@ -98,8 +97,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Open created order in backend --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml index 340ff4159900a..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 --> @@ -86,8 +87,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Open created order in backend --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonExistentCustomerGroupTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonExistentCustomerGroupTest.xml new file mode 100644 index 0000000000000..92dad56e81135 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonExistentCustomerGroupTest.xml @@ -0,0 +1,116 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="OnePageCheckoutAsCustomerUsingNonExistentCustomerGroupTest"> + <annotations> + <features value="OnePageCheckout"/> + <stories value="OnePageCheckout within Offline Payment Methods"/> + <title value="OnePageCheckout as a customer with non-existent customer group assigned to the quote"/> + <description value="Checkout as a customer with non-existent customer group assigned to the quote"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-36385"/> + <group value="checkout"/> + </annotations> + <before> + <!-- Create Simple Product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">560</field> + </createData> + + <!-- Create customer group --> + <createData entity="CustomCustomerGroup" stepKey="createCustomerGroup"/> + + <!-- Create customer and assign it to the customer group created on the previous step --> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"> + <field key="group_id">$$createCustomerGroup.id$$</field> + </createData> + </before> + <after> + <!-- Admin log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Customer log out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + + <!-- Delete created product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Login as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Add Simple Product to cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!-- Delete customer group --> + <deleteData createDataKey="createCustomerGroup" stepKey="deleteCustomerGroup"/> + + <!-- Go to shopping cart --> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <actionGroup ref="FillShippingZipForm" stepKey="fillShippingZipForm"> + <argument name="address" value="US_Address_CA"/> + </actionGroup> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <waitForPageLoad stepKey="waitForProceedToCheckout"/> + + <!-- Check that error does not appear and shipping methods are available to select --> + <dontSee selector="{{CheckoutCartMessageSection.errorMessage}}" userInput="No such entity with id = $$createCustomerGroup.id$$" stepKey="assertErrorMessage"/> + <dontSee selector="{{CheckoutShippingMethodsSection.noQuotesMsg}}" userInput="Sorry, no quotes are available for this order at this time" stepKey="assertNoQuotesMessage"/> + + <!-- Fill customer address data --> + <waitForElementVisible selector="{{CheckoutShippingSection.shipHereButton(UK_Not_Default_Address.street[0])}}" stepKey="waitForShipHereVisible"/> + <!-- Change address --> + <click selector="{{CheckoutShippingSection.shipHereButton(UK_Not_Default_Address.street[0])}}" stepKey="clickShipHere"/> + + <!-- Click next button to open payment section --> + <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForShipmentPageLoad"/> + + <!-- Select payment solution --> + <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution" /> + + <!-- Check order summary in checkout --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton"/> + <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Open created order in backend --> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> + <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + + <!-- Assert order total --> + <scrollTo selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="scrollToOrderTotalSection"/> + <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$565.00" stepKey="checkOrderTotalInBackend"/> + + <!-- Assert order addresses --> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" stepKey="seeBillingAddressStreet"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.city}}" stepKey="seeBillingAddressCity"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.postcode}}" stepKey="seeBillingAddressPostcode"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" stepKey="seeShippingAddressStreet"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.city}}" stepKey="seeShippingAddressCity"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.postcode}}" stepKey="seeShippingAddressPostcode"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml index 1c03808ac71cf..42d61abca845b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml @@ -79,8 +79,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Open created order in backend --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml index e678bb0d2a87b..a519aac72d1b5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml @@ -88,7 +88,9 @@ <!-- Create customer --> <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> @@ -198,8 +200,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Open created order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> @@ -213,8 +214,7 @@ </assertEquals> <!-- Assert order total --> - <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="navigateToCustomerDashboardPage"/> - <waitForPageLoad stepKey="waitForCustomerDashboardPageLoad"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="navigateToCustomerDashboardPage"/> <see selector="{{StorefrontCustomerRecentOrdersSection.orderTotal}}" userInput="$613.23" stepKey="checkOrderTotalInStorefront"/> <!-- Go to Address Book --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml index 571aa24209389..70faa3721efe9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml @@ -20,7 +20,9 @@ </annotations> <before> <!-- Flush cache --> - <magentoCLI command="cache:flush" stepKey="clearCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Create two customers --> <createData entity="Simple_US_Customer" stepKey="createFirstCustomer"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml index a5c8eb0da6530..b65cfe0eb574f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml @@ -23,7 +23,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithCustomOptions"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml index bd81a1cfab604..026f33b04f69a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml @@ -12,12 +12,15 @@ <annotations> <features value="Checkout"/> <stories value="Check customer information created by guest"/> - <title value="Check Customer Information Created By Guest"/> + <title value="Deprecated. Check Customer Information Created By Guest"/> <description value="Check customer information after placing the order as the guest who created an account"/> <severity value="MAJOR"/> <testCaseId value="MAGETWO-95932"/> <useCaseId value="MAGETWO-95820"/> <group value="checkout"/> + <skip> + <issueId value="DEPRECATED">Use StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest instead.</issueId> + </skip> </annotations> <before> @@ -25,7 +28,9 @@ <createData entity="_defaultProduct" stepKey="product"> <requiredEntity createDataKey="category"/> </createData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml index af3a2e6870cd7..20b94d0f4ec8a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -33,7 +33,9 @@ <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> <createData entity="MinimumOrderAmount90" stepKey="minimumOrderAmount90"/> - <magentoCLI command="cache:flush" stepKey="flushCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminCreateCartPriceRuleWithCouponCodeActionGroup" stepKey="createCartPriceRule"> <argument name="ruleName" value="CatPriceRule"/> <argument name="couponCode" value="CatPriceRule.coupon_code"/> @@ -50,7 +52,10 @@ <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> - <magentoCLI command="cache:flush" stepKey="flushCache2"/> + + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> <argument name="ruleName" value="{{CatPriceRule.name}}"/> </actionGroup> @@ -73,7 +78,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2"/> <waitForPageLoad stepKey="waitForShippingMethods"/> <click stepKey="chooseFreeShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext1"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext1"/> <waitForPageLoad stepKey="waitForReviewAndPayments1"/> <conditionalClick selector="{{DiscountSection.DiscountTab}}" dependentSelector="{{DiscountSection.CouponInput}}" visible="false" stepKey="clickIfDiscountTabClosed2"/> <waitForPageLoad stepKey="waitForCouponTabOpen2"/> @@ -87,7 +92,7 @@ <amOnPage stepKey="navigateToShippingPage" url="{{CheckoutShippingPage.url}}"/> <waitForPageLoad stepKey="waitForShippingPageLoad"/> <click stepKey="chooseFlatRateShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Flat Rate')}}"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext2"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext2"/> <waitForPageLoad stepKey="waitForReviewAndPayments2"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder2"/> <waitForPageLoad stepKey="waitForSuccessfullyPlacedOrder"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml index e82f3c0588835..3c090900563a5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml @@ -9,6 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontAddBundleDynamicProductToShoppingCartTest"> <annotations> + <features value="Checkout"/> <stories value="Shopping Cart"/> <title value="Add bundle dynamic product to the cart"/> <description value="Add bundle dynamic product to the cart"/> @@ -18,6 +19,7 @@ </annotations> <before> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> @@ -46,19 +48,26 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" stepKey="deleteAllRules"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="cataloginventory_stock"/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsAdmin"/> </after> <!--Open Product page in StoreFront --> <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> - <argument name="product" value="$$createBundleProduct$$"/> + <argument name="product" value="$createBundleProduct$"/> </actionGroup> <!--Assert Product Price Range --> @@ -93,8 +102,8 @@ <!--Assert Product items in cart --> <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSimpleProduct1ItemsInCheckOutCart"> - <argument name="productName" value="$$createBundleProduct.name$$"/> - <argument name="productSku" value="$$createBundleProduct.sku$$"/> + <argument name="productName" value="$createBundleProduct.name$"/> + <argument name="productSku" value="$createBundleProduct.sku$"/> <argument name="productPrice" value="$50.00"/> <argument name="subtotal" value="$100.00" /> <argument name="qty" value="2"/> @@ -107,13 +116,13 @@ </actionGroup> <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionInCart"> <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> - <argument name="userInput" value="1 x $$simpleProduct2.name$$ $50.00"/> + <argument name="userInput" value="1 x $simpleProduct2.name$ $50.00"/> </actionGroup> <!-- Assert Product in Mini Cart --> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> - <argument name="productName" value="$$createBundleProduct.name$$"/> + <argument name="productName" value="$createBundleProduct.name$"/> <argument name="productPrice" value="$50.00"/> <argument name="cartSubtotal" value="$100.00" /> <argument name="qty" value="2"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml index 5d5e2b3a91f49..edb6f8ba97b27 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml @@ -49,8 +49,12 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> @@ -111,7 +115,9 @@ <!--Enabled Mini Cart --> <magentoCLI stepKey="enableShoppingCartSidebar" command="config:set checkout/sidebar/display 1"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <reloadPage stepKey="reloadThePage"/> <!--Click on mini cart--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml index 21e785de6cab3..146ecde047016 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml @@ -110,8 +110,12 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct3"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteSimpleProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml index bbc0a29000a77..4f54363bd8dc4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml @@ -28,8 +28,12 @@ <createData entity="downloadableLink2" stepKey="addDownloadableLink2"> <requiredEntity createDataKey="createDownloadableProduct"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml index 3e2f32a4ab055..13a179fe52444 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml @@ -43,7 +43,9 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple3"/> </updateData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml index af12aecb6345a..eff18f9081b67 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml @@ -46,8 +46,12 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml index e8a72b6e88109..6cf5a390a964d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml @@ -23,7 +23,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithCustomOptions"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml index 265f9a7cbbc98..cd1c0542c5c5b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml @@ -46,8 +46,12 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml index 0b52caa7165af..4c0484f88d549 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml @@ -51,7 +51,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct10"> <field key="price">100.00</field> </createData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml index a496ff68c0cd0..b399d76e86e2f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml @@ -54,8 +54,12 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct11"> <field key="price">110.00</field> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml index 8e84deafea9f2..e0aeb2f93d30c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml @@ -49,7 +49,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct10"> <field key="price">100.00</field> </createData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml new file mode 100644 index 0000000000000..fa75a280e69f1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.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="StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest"> + <annotations> + <features value="Checkout"/> + <stories value="Check customer information created by guest"/> + <title value="Check Customer Information Created By Guest"/> + <description value="Check customer information after placing the order as the guest who created an account"/> + <severity value="MAJOR"/> + <testCaseId value="MC-28550"/> + <useCaseId value="MAGETWO-95820"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersGridFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="fillShippingSectionAsGuest"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessRegisterSection.orderNumber}}" stepKey="grabOrderNumber"/> + <actionGroup ref="StorefrontRegisterCustomerFromOrderSuccessPage" stepKey="createCustomerAfterPlaceOrder"> + <argument name="customer" value="CustomerEntityOne"/> + </actionGroup> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrdersGridById"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminShipmentOrderInformationSection.customerName}}" stepKey="seeCustomerName"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckIsCartUpdatedAfterProductDeleteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckIsCartUpdatedAfterProductDeleteTest.xml new file mode 100644 index 0000000000000..fd6a1035a326a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckIsCartUpdatedAfterProductDeleteTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckIsCartUpdatedAfterProductDeleteTest"> + <annotations> + <features value="Checkout"/> + <stories value="Delete Products from Shopping Cart"/> + <title value="Remove product added to shopping cart"/> + <description value="The product has to be deleted from shopping cart if it deleted in admin panel"/> + <testCaseId value="MC-36299"/> + <useCaseId value="MAGETWO-83169"/> + <severity value="CRITICAL"/> + <group value="checkout"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="SimpleProduct2" stepKey="createFirstProduct"> + <field key="price">10.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createSecondProduct"> + <field key="price">20.00</field> + </createData> + </before> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addFirstProductToCart"> + <argument name="product" value="$createFirstProduct$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSecondProductToCart"> + <argument name="product" value="$createSecondProduct$"/> + </actionGroup> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="selectViewAndEditCart"/> + <actionGroup ref="AssertStorefrontShoppingCartSummaryItemsActionGroup" stepKey="assertCartTotals"> + <argument name="subtotal" value="$30.00"/> + <argument name="total" value="$40.00"/> + </actionGroup> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteFirstProduct"> + <argument name="sku" value="$createFirstProduct.sku$"/> + </actionGroup> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToShoppingCartPage"/> + <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertCartAfterProductDeleted"> + <argument name="productName" value="$createSecondProduct.name$"/> + <argument name="productSku" value="$createSecondProduct.sku$"/> + <argument name="productPrice" value="$createSecondProduct.price$"/> + <argument name="subtotal" value="$createSecondProduct.price$" /> + <argument name="qty" value="1"/> + </actionGroup> + <dontSee selector="{{CheckoutCartProductSection.productName}}" userInput="$createFirstProduct.name$" stepKey="checkFirstProductIsAbsentInCart"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml index 79e46d093c2f6..ce6d465408382 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml @@ -54,7 +54,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct11"> <field key="price">110.00</field> </createData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml index 9f3eacbf5f455..c1dc0b7e62ba7 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml @@ -34,7 +34,9 @@ <createData entity="VirtualProduct" stepKey="virtualProduct4"> <field key="price">40.00</field> </createData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="virtualProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml index 27d4e4c207ae7..f16f577a4088c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml @@ -36,8 +36,12 @@ <requiredEntity createDataKey="bundleOption"/> <requiredEntity createDataKey="createSimpleProduct"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="cacheFlush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete category --> @@ -63,7 +67,7 @@ <openNewTab stepKey="openNewTab"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Find the first simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <!-- Disabled bundle product from grid --> <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid"> <argument name="product" value="$$createBundleDynamicProduct$$"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutForShowShippingMethodNoApplicableTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutForShowShippingMethodNoApplicableTest.xml new file mode 100644 index 0000000000000..22bc1260e5f33 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutForShowShippingMethodNoApplicableTest.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="StorefrontCheckoutForShowShippingMethodNoApplicableTest"> + <annotations> + <stories value="Checkout for not applicable shipping method"/> + <title value="Storefront checkout for not applicable shipping method test"/> + <description value="Checkout flow if shipping rates are not applicable"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-37420"/> + <group value="checkout"/> + </annotations> + <before> + <!-- Create simple product --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!-- Enable flat rate shipping to specific country - Afghanistan --> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{EnableFlatRateToSpecificCountriesConfigData.path}} {{EnableFlatRateToSpecificCountriesConfigData.value}}" stepKey="allowFlatRateSpecificCountries"/> + <magentoCLI command="config:set {{EnableFlatRateToAfghanistanConfigData.path}} {{EnableFlatRateToAfghanistanConfigData.value}}" stepKey="enableFlatRateToAfghanistan"/> + <!-- Enable Show Method if Not Applicable--> + <magentoCLI command="config:set {{EnableFlatRateShowMethodNoApplicableConfigData.path}} {{EnableFlatRateShowMethodNoApplicableConfigData.value}}" stepKey="enableShowMethodNoApplicable"/> + <!-- Create Customer with filled Shipping & Billing Address --> + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + </before> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFromStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set {{EnableFlatRateToAllAllowedCountriesConfigData.path}} {{EnableFlatRateToAllAllowedCountriesConfigData.value}}" stepKey="allowFlatRateToAllCountries"/> + <magentoCLI command="config:set {{EnableFlatRateShowMethodNoApplicableConfigData.path}} 0" stepKey="disableShowMethodNoApplicable"/> + <!-- Delete product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + <!-- Login with created Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Add product to cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Go to checkout page --> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="openCheckoutShippingPage"/> + <!-- Assert shipping price for US > California --> + <dontSeeElement selector="{{CheckoutShippingMethodsSection.price}}" stepKey="dontSeePrice"/> + <!-- Assert Next button is available --> + <seeElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="seeNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNextButton"/> + <!-- Assert order cannot be placed and error message will shown. --> + <waitForPageLoad stepKey="waitForError"/> + <see stepKey="seeShippingMethodError" userInput="The shipping method is missing. Select the shipping method and try again."/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndCreateCustomerAfterCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndCreateCustomerAfterCheckoutTest.xml new file mode 100644 index 0000000000000..ef1f30e2d9c36 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndCreateCustomerAfterCheckoutTest.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckoutWithDifferentShippingAndBillingAddressAndCreateCustomerAfterCheckoutTest"> + <annotations> + <stories value="Checkout"/> + <title value="Verify UK customer checkout with different billing and shipping address and register customer after checkout"/> + <description value="Checkout as UK customer with different shipping/billing address and register checkout method"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-28288"/> + <group value="mtf_migrated"/> + <group value="checkout"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct"> + <field key="price">50.00</field> + </createData> + </before> + <after> + <!-- Sign out Customer from storefront --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="UKCustomer.email"/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearCustomersGridFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <!--Open Product page in StoreFront and assert product and price range --> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> + <argument name="product" value="$simpleProduct$"/> + </actionGroup> + + <!--Add product to the cart --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$simpleProduct$"/> + </actionGroup> + + <!--Open View and edit --> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="openCartFromMiniCart"/> + + <!-- Fill the Estimate Shipping and Tax section --> + <actionGroup ref="CheckoutFillEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxFields"/> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!-- Fill the guest form --> + <actionGroup ref="FillGuestCheckoutShippingAddressFormActionGroup" stepKey="fillGuestShippingAddress"> + <argument name="customer" value="UKCustomer"/> + <argument name="customerAddress" value="updateCustomerUKAddress"/> + </actionGroup> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToBillingStep"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="waitForSameBillingAndShippingAddressCheckboxVisible"/> + <uncheckOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="uncheckSameBillingAndShippingAddress"/> + <conditionalClick selector="{{CheckoutShippingSection.editAddressButton}}" dependentSelector="{{CheckoutShippingSection.editAddressButton}}" visible="true" stepKey="clickEditBillingAddressButton"/> + + <!-- Fill Billing Address --> + <actionGroup ref="StorefrontFillBillingAddressActionGroup" stepKey="fillBillingAddressForm"/> + <click selector="{{CheckoutPaymentSection.update}}" stepKey="clickOnUpdateBillingAddressButton"/> + + <!--Place order --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> + <seeElement selector="{{StorefrontMinicartSection.emptyMiniCart}}" stepKey="assertEmptyCart" /> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> + + <!-- Register customer after checkout --> + <actionGroup ref="StorefrontRegisterCustomerAfterCheckoutActionGroup" stepKey="registerCustomer"/> + + <!-- Open Order Page in admin --> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="openOrder"> + <argument name="orderId" value="{$orderId}"/> + </actionGroup> + + <!-- Assert Grand Total --> + <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$55.00" stepKey="seeGrandTotal"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeOrderStatus"/> + + <!-- Ship the order and assert the status --> + <actionGroup ref="GoToShipmentIntoOrderActionGroup" stepKey="goToShipment"/> + <actionGroup ref="SubmitShipmentIntoOrderActionGroup" stepKey="submitShipment"/> + + <!-- Assert order buttons --> + <actionGroup ref="AdminAssertOrderAvailableButtonsActionGroup" stepKey="assertOrderButtons"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndProductWithTierPricesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndProductWithTierPricesTest.xml index 38efc9d7eca24..c724cf4986aa9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndProductWithTierPricesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndProductWithTierPricesTest.xml @@ -33,7 +33,9 @@ <argument name="price" value="Fixed"/> <argument name="amount" value="24.00"/> </actionGroup> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{DisablePaymentBankTransferConfigData.path}} {{DisablePaymentBankTransferConfigData.value}}" stepKey="enableGuestCheckout"/> @@ -71,8 +73,7 @@ <argument name="customer" value="UKCustomer"/> <argument name="customerAddress" value="updateCustomerUKAddress"/> </actionGroup> - <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickOnNextButton"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> <waitForElementVisible selector="{{CheckoutPaymentSection.bankTransfer}}" stepKey="waitForPlaceOrderButton"/> <checkOption selector="{{CheckoutPaymentSection.bankTransfer}}" stepKey="selectBankTransfer"/> <waitForElementVisible selector="{{CheckoutPaymentSection.billingAddressNotSameBankTransferCheckbox}}" stepKey="waitForElementToBeVisible"/> @@ -108,8 +109,7 @@ <see selector="{{StorefrontCustomerAddressesSection.shippingAddress}}" userInput="T: {{updateCustomerUKAddress.telephone}}" stepKey="seeTelephoneInShippingAddress"/> <!--Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrderIndexPageToLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndRegisterCustomerAfterCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndRegisterCustomerAfterCheckoutTest.xml index eda6d5f867540..118205e912b5e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndRegisterCustomerAfterCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndRegisterCustomerAfterCheckoutTest.xml @@ -7,14 +7,17 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontCheckoutWithDifferentShippingAndBillingAddressAndRegisterCustomerAfterCheckoutTest"> + <test name="StorefrontCheckoutWithDifferentShippingAndBillingAddressAndRegisterCustomerAfterCheckoutTest" deprecated="Use StorefrontCheckoutWithDifferentShippingAndBillingAddressAndCreateCustomerAfterCheckoutTest instead"> <annotations> <stories value="Checkout"/> - <title value="Verify UK customer checkout with different billing and shipping address and register customer after checkout"/> + <title value="DEPRECATED. Verify UK customer checkout with different billing and shipping address and register customer after checkout"/> <description value="Checkout as UK customer with different shipping/billing address and register checkout method"/> <severity value="CRITICAL"/> <testCaseId value="MC-14712"/> <group value="mtf_migrated"/> + <skip> + <issueId value="DEPRECATED">Use StorefrontCheckoutWithDifferentShippingAndBillingAddressAndCreateCustomerAfterCheckoutTest instead</issueId> + </skip> </annotations> <before> @@ -57,8 +60,7 @@ <argument name="customer" value="UKCustomer"/> <argument name="customerAddress" value="updateCustomerUKAddress"/> </actionGroup> - <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickOnNextButton"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> <waitForElementVisible selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="waitForElementToBeVisible"/> <uncheckOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="uncheckSameBillingAndShippingAddress"/> <conditionalClick selector="{{CheckoutShippingSection.editAddressButton}}" dependentSelector="{{CheckoutShippingSection.editAddressButton}}" visible="true" stepKey="clickEditButton"/> @@ -77,8 +79,7 @@ <actionGroup ref="StorefrontRegisterCustomerAfterCheckoutActionGroup" stepKey="registerCustomer"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml index 0042c73b13826..6abd62542e5c9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml @@ -90,8 +90,12 @@ <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct1"/> <waitForPageLoad time='60' stepKey="waitForSpecialPriceProductSaved"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSaveSuccessMessage1"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -139,8 +143,7 @@ <fillField selector="{{CheckoutShippingSection.password}}" userInput="$$createCustomer.password$$" stepKey="fillPassword"/> <click selector="{{CheckoutShippingSection.loginButton}}" stepKey="clickLoginButton"/> <waitForPageLoad stepKey="waitForLoginPageToLoad"/> - <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickOnNextButton"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> <!-- Place order and Assert success message --> <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> @@ -150,8 +153,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml index a2ff149af1a87..05d24696197d2 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml @@ -35,7 +35,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createUSCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductListing"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductListing"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> </after> @@ -70,7 +70,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Find the first simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <!-- Disabled simple product from grid --> <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml index 5fbfdb5a07678..4ae6925cc8d55 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml @@ -62,8 +62,7 @@ </actionGroup> <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout1"/> <waitForPageLoad stepKey="waitForShippingMethodSectionToLoad"/> - <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickOnNextButton"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> <!-- Verify order summary on payment page --> <actionGroup ref="VerifyCheckoutPaymentOrderSummaryActionGroup" stepKey="verifyCheckoutPaymentOrderSummary"> @@ -77,8 +76,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml index b5f573aba7561..e97f7f0d3e8e4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml @@ -23,7 +23,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> 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 4c3c1561a2445..9747980801068 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -44,21 +43,23 @@ <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> <!--TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush full_page" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> <argument name="name" value="{{SimpleTaxNY.state}}-{{SimpleTaxNY.rate}}"/> @@ -82,15 +83,14 @@ <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"/> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> <click stepKey="selectFirstShippingMethod1" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> - <waitForElement stepKey="waitForShippingMethodSelect1" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> - <click stepKey="clickNextOnShippingMethodLoad1" selector="{{CheckoutShippingMethodsSection.next}}"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNextOnShippingMethodLoad1"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <waitForElement stepKey="waitForPlaceOrderButton1" selector="{{CheckoutPaymentSection.placeOrder}}" time="30"/> @@ -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"/> @@ -113,8 +113,7 @@ <waitForElementVisible selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" time="30" stepKey="waitForShippingMethodRadioToBeVisible"/> <waitForPageLoad stepKey="waitForPageLoad23"/> <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod2"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForShippingMethodSelect2"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNextOnShippingMethodLoad2"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNextOnShippingMethodLoad2"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton2"/> 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 d042a15e3c958..d6f1408c2b66a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -28,8 +28,12 @@ <createData entity="Simple_US_Customer" stepKey="simpleuscustomer"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -48,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"/> @@ -58,8 +62,7 @@ <!-- Select address --> <click stepKey="selectAddress" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> - <waitForElement stepKey="waitNextButton" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> - <click stepKey="clickNextButton" selector="{{CheckoutShippingMethodsSection.next}}"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNextButton"/> <waitForPageLoad stepKey="waitBillingForm"/> <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> <dontSee selector="{{CheckoutPaymentSection.paymentMethodByName('Check / Money order')}}" stepKey="paymentMethodDoesNotAvailable"/> 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/StorefrontCustomerCheckoutWithNewCustomerRegistrationAndDisableGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithNewCustomerRegistrationAndDisableGuestCheckoutTest.xml index 0f82302260995..0d3bfe7b1fb84 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithNewCustomerRegistrationAndDisableGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithNewCustomerRegistrationAndDisableGuestCheckoutTest.xml @@ -87,8 +87,7 @@ <!-- Proceed to checkout --> <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout1"/> - <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickOnNextButton"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> <!-- Verify order summary on payment page --> <actionGroup ref="VerifyCheckoutPaymentOrderSummaryActionGroup" stepKey="verifyCheckoutPaymentOrderSummary"> @@ -115,8 +114,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml index 0c762519e9083..24ca488ea25e5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml @@ -49,7 +49,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckoutPage"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNextButton"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButton"/> <see selector="{{StorefrontMessagesSection.error}}" userInput='Please specify a regionId in shipping address.' stepKey="seeErrorMessages"/> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml index 95ea08e1e7d9a..d688a43e7de04 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml @@ -67,8 +67,7 @@ <!--Close Popup and click next--> <click selector="{{StorefrontCheckoutAddressPopupSection.closeAddressModalPopup}}" stepKey="closePopup"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> <!--Refresh Page and Place Order--> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml index d116d0049c9df..6e304ff9cfb50 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml @@ -40,7 +40,9 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct1"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml index eb8b047b57288..d012d44d84052 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml @@ -63,8 +63,12 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct1"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteSimpleProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml index 8a52fa7740b95..d2bcaedb74fd1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml @@ -27,8 +27,12 @@ <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="createDownloadableProduct"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> 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 0d69306a4b1ba..b591aefbdc889 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml @@ -24,8 +24,12 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> @@ -36,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 0520accdd4b84..15b550657ef60 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -24,8 +24,12 @@ </createData> <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 1"/> <magentoCLI stepKey="specificCountryValue" command="config:set payment/checkmo/specificcountry GB"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> @@ -39,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/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml index dbb695fb4fb00..5a0610f5c5b0a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -90,8 +90,12 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> @@ -115,10 +119,8 @@ </after> <!-- Create a Tax Rule --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAddNewTaxRuleButton"/> <!-- Create a tax rule with defaults --> <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> @@ -184,8 +186,7 @@ <argument name="customer" value="Simple_US_Customer"/> <argument name="customerAddress" value="US_Address_NY_Default_Shipping"/> </actionGroup> - <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickOnNextButton"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> <!-- Place order and Assert success message --> <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> @@ -195,8 +196,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithCouponAndZeroSubtotalTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithCouponAndZeroSubtotalTest.xml index e9d056417330d..f910a9d47244f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithCouponAndZeroSubtotalTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithCouponAndZeroSubtotalTest.xml @@ -73,8 +73,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml index 5df8338030efc..82324525bad24 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml @@ -58,9 +58,7 @@ <!--Select shipping method and finalize checkout--> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <waitForPageLoad stepKey="waitForShippingMethodLoad"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml index 9aea4ac79312a..e42d5e1bae956 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -23,7 +23,7 @@ <field key="price">10</field> </createData> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> </before> <after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml index 37a7bfff60eb1..f20d0b790edfa 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml @@ -53,7 +53,7 @@ <waitForPageLoad stepKey="waitForPageLoad"/> <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createProduct.name$$-new" stepKey="fillProductName"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!--Add product to cart--> <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductQuantityChangesInBackendAfterCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductQuantityChangesInBackendAfterCustomerCheckoutTest.xml index ffdbab03ca337..3978389691b55 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductQuantityChangesInBackendAfterCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductQuantityChangesInBackendAfterCustomerCheckoutTest.xml @@ -54,8 +54,7 @@ <argument name="customer" value="UKCustomer"/> <argument name="customerAddress" value="updateCustomerUKAddress"/> </actionGroup> - <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickOnNextButton"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> <waitForElementVisible selector="{{CheckoutPaymentSection.bankTransfer}}" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.bankTransfer}}" stepKey="selectBankTransfer"/> @@ -67,8 +66,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml index 792025acf1708..d037718a1ec94 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml @@ -112,8 +112,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKGuestCheckoutWithConditionProductQuantityEqualsToOrderedQuantityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKGuestCheckoutWithConditionProductQuantityEqualsToOrderedQuantityTest.xml index 76a3adfb67057..10eee5938b27d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKGuestCheckoutWithConditionProductQuantityEqualsToOrderedQuantityTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKGuestCheckoutWithConditionProductQuantityEqualsToOrderedQuantityTest.xml @@ -51,9 +51,7 @@ <argument name="customer" value="UKCustomer"/> <argument name="customerAddress" value="UK_Not_Default_Address"/> </actionGroup> - <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickOnNextButton"/> - + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> <!-- Place order and Assert success message --> <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> @@ -62,8 +60,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUSCustomerCheckoutWithCouponAndBankTransferPaymentMethodTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUSCustomerCheckoutWithCouponAndBankTransferPaymentMethodTest.xml index 8410dd15fa04e..dc92c0c5ce9ee 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUSCustomerCheckoutWithCouponAndBankTransferPaymentMethodTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUSCustomerCheckoutWithCouponAndBankTransferPaymentMethodTest.xml @@ -61,8 +61,7 @@ <!-- Fill the guest form --> <actionGroup ref="FillGuestCheckoutShippingAddressFormActionGroup" stepKey="fillGuestForm"/> - <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickOnNextButton"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> <waitForElementVisible selector="{{CheckoutPaymentSection.bankTransfer}}" stepKey="waitForPlaceOrderButton"/> <checkOption selector="{{CheckoutPaymentSection.bankTransfer}}" stepKey="selectBankTransfer"/> @@ -74,8 +73,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml index 12e2820821c87..a7a0917532dcb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -30,7 +30,9 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <actionGroup ref="SetCustomerDataLifetimeActionGroup" stepKey="setDefaultCustomerDataLifetime"/> - <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexCustomerGrid"> + <argument name="indices" value="customer_grid"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Go to product page--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml index 778967c187f65..03323b7b9c855 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml @@ -28,19 +28,22 @@ <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"/> <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="product" stepKey="deleteProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> </after> 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/Test/Unit/Controller/Sidebar/RemoveItemTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php index 09bc9e36c0abc..9fa77145ab8fa 100644 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php @@ -221,6 +221,57 @@ public function testExecuteWithException() $this->assertEquals($resultJson, $this->action->execute()); } + /** + * Test controller when DB exception is thrown. + * + * @return void + */ + public function testExecuteWithDbException(): void + { + $itemId = 1; + $dbError = 'Error'; + $message = __('An unspecified error occurred. Please contact us for assistance.'); + $responseData = [ + 'success' => false, + 'error_message' => $message, + ]; + + $this->formKeyValidatorMock + ->expects($this->once()) + ->method('validate') + ->with($this->requestMock) + ->willReturn(true); + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('item_id') + ->willReturn($itemId); + + $exception = new \Zend_Db_Exception($dbError); + + $this->sidebarMock->expects($this->once()) + ->method('checkQuoteItem') + ->with($itemId) + ->willThrowException($exception); + + $this->loggerMock->expects($this->once())->method('critical')->with($exception); + + $this->sidebarMock->expects($this->once()) + ->method('getResponseData') + ->with($message) + ->willReturn($responseData); + + $resultJson = $this->createMock(ResultJson::class); + $resultJson->expects($this->once()) + ->method('setData') + ->with($responseData) + ->willReturnSelf(); + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($resultJson); + + $this->action->execute(); + } + public function testExecuteWhenFormKeyValidationFailed() { $resultRedirect = $this->createMock(ResultRedirect::class); diff --git a/app/code/Magento/Checkout/Test/Unit/Model/TotalsInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/TotalsInformationManagementTest.php new file mode 100644 index 0000000000000..61049b4893476 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/TotalsInformationManagementTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Model; + +use Magento\Checkout\Api\Data\TotalsInformationInterface; +use Magento\Checkout\Model\TotalsInformationManagement; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\CartTotalRepositoryInterface; +use Magento\Quote\Model\Quote\Address; + +class TotalsInformationManagementTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var CartRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $cartRepositoryMock; + + /** + * @var CartTotalRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $cartTotalRepositoryMock; + + /** + * @var TotalsInformationManagement + */ + private $totalsInformationManagement; + + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + $this->cartRepositoryMock = $this->createMock( + CartRepositoryInterface::class + ); + $this->cartTotalRepositoryMock = $this->createMock( + CartTotalRepositoryInterface::class + ); + + $this->totalsInformationManagement = $this->objectManager->getObject( + TotalsInformationManagement::class, + [ + 'cartRepository' => $this->cartRepositoryMock, + 'cartTotalRepository' => $this->cartTotalRepositoryMock, + ] + ); + } + + /** + * Test for \Magento\Checkout\Model\TotalsInformationManagement::calculate. + * + * @param string|null $carrierCode + * @param string|null $carrierMethod + * @param int $methodSetCount + * @dataProvider dataProviderCalculate + */ + public function testCalculate(?string $carrierCode, ?string $carrierMethod, int $methodSetCount) + { + $cartId = 1; + $cartMock = $this->createMock( + \Magento\Quote\Model\Quote::class + ); + $cartMock->expects($this->once())->method('getItemsCount')->willReturn(1); + $cartMock->expects($this->once())->method('getIsVirtual')->willReturn(false); + $this->cartRepositoryMock->expects($this->once())->method('get')->with($cartId)->willReturn($cartMock); + $this->cartTotalRepositoryMock->expects($this->once())->method('get')->with($cartId); + + $addressInformationMock = $this->createMock( + TotalsInformationInterface::class + ); + $addressMock = $this->getMockBuilder(Address::class) + ->addMethods( + [ + 'setShippingMethod', + 'setCollectShippingRates', + ] + ) + ->disableOriginalConstructor() + ->getMock(); + + $addressInformationMock->expects($this->once())->method('getAddress')->willReturn($addressMock); + $addressInformationMock->expects($this->any())->method('getShippingCarrierCode')->willReturn($carrierCode); + $addressInformationMock->expects($this->any())->method('getShippingMethodCode')->willReturn($carrierMethod); + $cartMock->expects($this->once())->method('setShippingAddress')->with($addressMock); + $cartMock->expects($this->exactly($methodSetCount))->method('getShippingAddress')->willReturn($addressMock); + $addressMock->expects($this->exactly($methodSetCount)) + ->method('setCollectShippingRates')->with(true)->willReturn($addressMock); + $addressMock->expects($this->exactly($methodSetCount)) + ->method('setShippingMethod')->with($carrierCode . '_' . $carrierMethod); + $cartMock->expects($this->once())->method('collectTotals'); + + $this->totalsInformationManagement->calculate($cartId, $addressInformationMock); + } + + /** + * Data provider for testCalculate. + * + * @return array + */ + public function dataProviderCalculate(): array + { + return [ + [ + null, + null, + 0 + ], + [ + null, + 'carrier_method', + 0 + ], + [ + 'carrier_code', + null, + 0 + ], + [ + 'carrier_code', + 'carrier_method', + 1 + ] + ]; + } +} diff --git a/app/code/Magento/Checkout/ViewModel/Cart.php b/app/code/Magento/Checkout/ViewModel/Cart.php new file mode 100644 index 0000000000000..f5415079d396e --- /dev/null +++ b/app/code/Magento/Checkout/ViewModel/Cart.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Checkout\ViewModel; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\View\Element\Context; +use Magento\Store\Model\ScopeInterface; + +/** + * Cart form view model. + */ +class Cart implements ArgumentInterface +{ + /** + * Config settings path to enable clear shopping cart button + */ + private const XPATH_CONFIG_ENABLE_CLEAR_SHOPPING_CART = 'checkout/cart/enable_clear_shopping_cart'; + + /** + * @var ScopeConfigInterface + */ + private $_scopeConfig; + + /** + * Constructor + * + * @param Context $context + */ + public function __construct( + Context $context + ) { + $this->_scopeConfig = $context->getScopeConfig(); + } + + /** + * Check if clear shopping cart button is enabled + * + * @return bool + */ + public function isClearShoppingCartEnabled() + { + return (bool)$this->_scopeConfig->getValue( + self::XPATH_CONFIG_ENABLE_CLEAR_SHOPPING_CART, + ScopeInterface::SCOPE_WEBSITE + ); + } +} diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml index 7454c2b6524f3..b56566a043c3e 100644 --- a/app/code/Magento/Checkout/etc/adminhtml/system.xml +++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml @@ -48,6 +48,10 @@ <label>Show Cross-sell Items in the Shopping Cart</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> + <field id="enable_clear_shopping_cart" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>Enable Clear Shopping Cart</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="cart_link" translate="label" sortOrder="3" showInDefault="1" showInWebsite="1"> <label>My Cart Link</label> diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml index c8408f6d902fa..4db5f5bdc01c9 100644 --- a/app/code/Magento/Checkout/etc/config.xml +++ b/app/code/Magento/Checkout/etc/config.xml @@ -19,6 +19,7 @@ <redirect_to_cart>0</redirect_to_cart> <number_items_to_display_pager>20</number_items_to_display_pager> <crosssell_enabled>1</crosssell_enabled> + <enable_clear_shopping_cart>0</enable_clear_shopping_cart> </cart> <cart_link> <use_qty>1</use_qty> diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index 251985faf6cc4..ca118f21f2441 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -153,6 +153,8 @@ Shipping,Shipping "Maximum Number of Items to Display in Order Summary","Maximum Number of Items to Display in Order Summary" "Quote Lifetime (days)","Quote Lifetime (days)" "After Adding a Product Redirect to Shopping Cart","After Adding a Product Redirect to Shopping Cart" +"Enable Clear Shopping Cart","Enable Clear Shopping Cart" +"Are you sure you want to remove all items from your shopping cart?","Are you sure you want to remove all items from your shopping cart?" "Number of Items to Display Pager","Number of Items to Display Pager" "My Cart Link","My Cart Link" "Display Cart Summary","Display Cart Summary" @@ -174,7 +176,7 @@ Summary,Summary "We'll send your order confirmation here.","We'll send your order confirmation here." Payment,Payment "Not yet calculated","Not yet calculated" -"We received your order!","We received your order!" +"The order was not successful!","The order was not successful!" "Thank you for your purchase!","Thank you for your purchase!" "Password", "Password" "Something went wrong while saving the page. Please refresh the page and try again.","Something went wrong while saving the page. Please refresh the page and try again." @@ -182,4 +184,5 @@ Payment,Payment "Items in Cart","Items in Cart" "Close","Close" "Show Cross-sell Items in the Shopping Cart","Show Cross-sell Items in the Shopping Cart" -"You added %1 to your <a href=""%2"">shopping cart</a>.","You added %1 to your <a href=""%2"">shopping cart</a>." \ No newline at end of file +"You added %1 to your <a href=""%2"">shopping cart</a>.","You added %1 to your <a href=""%2"">shopping cart</a>." +"The shipping method is missing. Select the shipping method and try again.","The shipping method is missing. Select the shipping method and try again." diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml index 81ee1a5e6db4c..b465c68078641 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml @@ -181,6 +181,9 @@ </block> </container> <block class="Magento\Checkout\Block\Cart\Grid" name="checkout.cart.form" as="cart-items" template="Magento_Checkout::cart/form.phtml" after="cart.summary"> + <arguments> + <argument name="view_model" xsi:type="object">Magento\Checkout\ViewModel\Cart</argument> + </arguments> <block class="Magento\Framework\View\Element\RendererList" name="checkout.cart.item.renderers" as="renderer.list"/> <block class="Magento\Framework\View\Element\Text\ListText" name="checkout.cart.order.actions"/> </block> diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index c33b784fcd20c..192f20653f8c3 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml @@ -105,7 +105,7 @@ <item name="trigger" xsi:type="string">opc-new-shipping-address</item> <item name="buttons" xsi:type="array"> <item name="save" xsi:type="array"> - <item name="text" xsi:type="string" translate="true">Ship here</item> + <item name="text" xsi:type="string" translate="true">Ship Here</item> <item name="class" xsi:type="string">action primary action-save-address</item> </item> <item name="cancel" xsi:type="array"> diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_onepage_failure.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_onepage_failure.xml index 3ab37c2ab6b9f..b815bf74c155a 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_onepage_failure.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_onepage_failure.xml @@ -9,7 +9,7 @@ <body> <referenceBlock name="page.main.title"> <action method="setPageTitle"> - <argument translate="true" name="title" xsi:type="string">We received your order!</argument> + <argument translate="true" name="title" xsi:type="string">The order was not successful!</argument> </action> </referenceBlock> <referenceContainer name="content"> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 370d70c44d886..59e33a7c855ce 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -20,7 +20,7 @@ class="form form-cart"> <?= $block->getBlockHtml('formkey') ?> <div class="cart table-wrapper<?= $mergedCells == 2 ? ' detailed' : '' ?>"> - <?php if ($block->getPagerHtml()) :?> + <?php if ($block->getPagerHtml()): ?> <div class="cart-products-toolbar cart-products-toolbar-top toolbar" data-attribute="cart-products-toolbar-top"><?= $block->getPagerHtml() ?> </div> @@ -38,32 +38,34 @@ <th class="col subtotal" scope="col"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> </tr> </thead> - <?php foreach ($block->getItems() as $_item) :?> + <?php foreach ($block->getItems() as $_item): ?> <?= $block->getItemHtml($_item) ?> <?php endforeach ?> </table> - <?php if ($block->getPagerHtml()) :?> + <?php if ($block->getPagerHtml()): ?> <div class="cart-products-toolbar cart-products-toolbar-bottom toolbar" data-attribute="cart-products-toolbar-bottom"><?= $block->getPagerHtml() ?> </div> <?php endif ?> </div> <div class="cart main actions"> - <?php if ($block->getContinueShoppingUrl()) :?> + <?php if ($block->getContinueShoppingUrl()): ?> <a class="action continue" href="<?= $block->escapeUrl($block->getContinueShoppingUrl()) ?>" title="<?= $block->escapeHtml(__('Continue Shopping')) ?>"> <span><?= $block->escapeHtml(__('Continue Shopping')) ?></span> </a> <?php endif; ?> - <button type="button" - name="update_cart_action" - data-cart-empty="" - value="empty_cart" - title="<?= $block->escapeHtml(__('Clear Shopping Cart')) ?>" - class="action clear" id="empty_cart_button"> - <span><?= $block->escapeHtml(__('Clear Shopping Cart')) ?></span> - </button> + <?php if ($block->getViewModel()->isClearShoppingCartEnabled()): ?> + <button type="button" + name="update_cart_action" + data-cart-empty="" + value="empty_cart" + title="<?= $block->escapeHtml(__('Clear Shopping Cart')) ?>" + class="action clear" id="empty_cart_button"> + <span><?= $block->escapeHtml(__('Clear Shopping Cart')) ?></span> + </button> + <?php endif ?> <button type="submit" name="update_cart_action" data-cart-item-update="" @@ -77,4 +79,3 @@ </form> <?= $block->getChildHtml('checkout.cart.order.actions') ?> <?= $block->getChildHtml('shopping.cart.table.after') ?> - diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml index 28275e0223936..56cd8cd7a1fab 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Checkout\Block\Cart\Sidebar */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-block="minicart" class="minicart-wrapper"> @@ -12,7 +13,8 @@ data-bind="scope: 'minicart_content'"> <span class="text"><?= $block->escapeHtml(__('My Cart')) ?></span> <span class="counter qty empty" - data-bind="css: { empty: !!getCartParam('summary_count') == false && !isLoading() }, blockLoader: isLoading"> + data-bind="css: { empty: !!getCartParam('summary_count') == false && !isLoading() }, + blockLoader: isLoading"> <span class="counter-number"><!-- ko text: getCartParam('summary_count') --><!-- /ko --></span> <span class="counter-label"> <!-- ko if: getCartParam('summary_count') --> @@ -22,7 +24,7 @@ </span> </span> </a> - <?php if ($block->getIsNeedToDisplaySideBar()) :?> + <?php if ($block->getIsNeedToDisplaySideBar()):?> <div class="block block-minicart" data-role="dropdownDialog" data-mage-init='{"dropdownDialog":{ @@ -39,18 +41,19 @@ </div> <?= $block->getChildHtml('minicart.addons') ?> </div> - <?php else :?> - <script> + <?php else: ?> + <?php $scriptString = <<<script require(['jquery'], function ($) { $('a.action.showcart').click(function() { $(document.body).trigger('processStart'); }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <?php endif ?> - <script> - window.checkout = <?= /* @noEscape */ $block->getSerializedConfig() ?>; - </script> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], 'window.checkout = ' . + /* @noEscape */ $block->getSerializedConfig(), false); ?> <script type="text/x-magento-init"> { "[data-block='minicart']": { @@ -64,5 +67,3 @@ } </script> </div> - - diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml index a44d37dccfdc5..78625521403a4 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml @@ -6,7 +6,7 @@ ?> <?php /** @var $block \Magento\Checkout\Block\Cart\Shipping */ ?> - +<?php /** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="block-shipping" class="block shipping" data-mage-init='{"collapsible":{"openedState": "active", "saveState": true}}' @@ -33,8 +33,11 @@ } } </script> - <script> - window.checkoutConfig = <?= /* @noEscape */ $block->getSerializedCheckoutConfig() ?>; +<?php $serializedCheckoutConfig = /* @noEscape */ $block->getSerializedCheckoutConfig(); + +$scriptString = <<<script + + window.checkoutConfig = {$serializedCheckoutConfig}; window.customerData = window.checkoutConfig.customerData; window.isCustomerLoggedIn = window.checkoutConfig.isCustomerLoggedIn; require([ @@ -42,10 +45,12 @@ 'Magento_Ui/js/block-loader' ], function(url, blockLoader) { blockLoader( - "<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif'))) ?>" + "{$block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')))}" ); - return url.setBaseUrl('<?= $block->escapeJs($block->escapeUrl($block->getBaseUrl())) ?>'); + return url.setBaseUrl('{$block->escapeJs($block->escapeUrl($block->getBaseUrl()))}'); }) - </script> +script; +?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml index 55f7039f33344..f4cc667e4ce8a 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml @@ -5,15 +5,17 @@ */ /** @var $block \Magento\Checkout\Block\Onepage */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> + <div id="checkout" data-bind="scope:'checkout'" class="checkout-container"> <div id="checkout-loader" data-role="checkout-loader" class="loading-mask" data-mage-init='{"checkoutLoader": {}}'> <div class="loader"> <img src="<?= $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')) ?>" - alt="<?= $block->escapeHtmlAttr(__('Loading...')) ?>" - style="position: absolute;"> + alt="<?= $block->escapeHtmlAttr(__('Loading...')) ?>"> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("position: absolute;", "#checkout-loader img") ?> <!-- ko template: getTemplate() --><!-- /ko --> <script type="text/x-magento-init"> { @@ -22,19 +24,24 @@ } } </script> - <script> - window.checkoutConfig = <?= /* @noEscape */ $block->getSerializedCheckoutConfig() ?>; + <?php $serializedCheckoutConfig = /* @noEscape */ $block->getSerializedCheckoutConfig(); + $scriptString = <<<script + window.checkoutConfig = {$serializedCheckoutConfig}; // Create aliases for customer.js model from customer module window.isCustomerLoggedIn = window.checkoutConfig.isCustomerLoggedIn; window.customerData = window.checkoutConfig.customerData; - </script> - <script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <?php $scriptString = <<<script require([ 'mage/url', 'Magento_Ui/js/block-loader' ], function(url, blockLoader) { - blockLoader("<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif'))) ?>"); - return url.setBaseUrl('<?= $block->escapeJs($block->escapeUrl($block->getBaseUrl())) ?>'); + blockLoader("{$block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')))}"); + return url.setBaseUrl('{$block->escapeJs($block->escapeUrl($block->getBaseUrl()))}'); }) - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml index 0d9da171c11a8..dbe8a2142e3f1 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml @@ -4,40 +4,42 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** @var $block \Magento\Checkout\Block\Total\DefaultTotal */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> + +<?php +/** @var \Magento\Checkout\Helper\Data $checkoutHelper */ +$checkoutHelper = $block->getData('checkoutHelper'); ?> <tr class="totals"> - <th - colspan="<?= $block->escapeHtmlAttr($block->getColspan()) ?>" - style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" - class="mark" scope="row" - > - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + <th colspan="<?= $block->escapeHtmlAttr($block->getColspan()) ?>" + class="mark" scope="row"> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()):?> <strong> <?php endif; ?> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()):?> </strong> <?php endif; ?> </th> - <td - style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" - class="amount" - data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>" - > - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + <td class="amount" + data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()):?> <strong> <?php endif; ?> <span> <?= $block->escapeHtml( - $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValue()), + $checkoutHelper->formatPrice($block->getTotal()->getValue()), ['span'] ) ?> </span> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()):?> </strong> <?php endif; ?> </td> </tr> +<?php if ($block->getTotal()->getStyle()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($block->getTotal()->getStyle(), 'tr.totals th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($block->getTotal()->getStyle(), 'tr.totals td.amount') ?> +<?php endif; ?> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js index 4b30ad8075274..2e9bdf1f31086 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js @@ -3,14 +3,16 @@ * See COPYING.txt for license details. */ -define([ - 'Magento_Customer/js/customer-data' -], function (customerData) { +define(['Magento_Customer/js/customer-data'], function (customerData) { 'use strict'; - var cartData = customerData.get('cart'); + return function () { + var cartData = customerData.get('cart'); - if (cartData().items && cartData().items.length !== 0) { - customerData.reload(['cart'], false); - } + customerData.getInitCustomerData().done(function () { + if (cartData().items && cartData().items.length !== 0) { + customerData.reload(['cart'], false); + } + }); + }; }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js index 6e1b031ab48ce..a59ea7101f16c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js @@ -84,7 +84,8 @@ define([ quoteAddressToFormAddressData: function (addrs) { var self = this, output = {}, - streetObject; + streetObject, + customAttributesObject; $.each(addrs, function (key) { if (addrs.hasOwnProperty(key) && !$.isFunction(addrs[key])) { @@ -100,6 +101,16 @@ define([ output.street = streetObject; } + //jscs:disable requireCamelCaseOrUpperCaseIdentifiers + if ($.isArray(addrs.customAttributes)) { + customAttributesObject = {}; + addrs.customAttributes.forEach(function (value) { + customAttributesObject[value.attribute_code] = value.value; + }); + output.custom_attributes = customAttributesObject; + } + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + return output; }, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js index 6d54f607484b4..68f6b1b2753c0 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js @@ -56,6 +56,9 @@ define([ if (this.options.isMultipleCountriesAllowed) { this.element.parents('div.field').show(); this.element.on('change', $.proxy(function (e) { + // clear region inputs on country change + $(this.options.regionListId).val(''); + $(this.options.regionInputId).val(''); this._updateRegion($(e.target).val()); }, this)); @@ -157,19 +160,25 @@ define([ regionInput = $(this.options.regionInputId), postcode = $(this.options.postcodeId), label = regionList.parent().siblings('label'), - container = regionList.parents('div.field'); + container = regionList.parents('div.field'), + regionsEntries, + regionId, + regionData; this._clearError(); this._checkRegionRequired(country); - $(regionList).find('option:selected').removeAttr('selected'); - regionInput.val(''); - // Populate state/province dropdown list if available or use input box if (this.options.regionJson[country]) { this._removeSelectOptions(regionList); - $.each(this.options.regionJson[country], $.proxy(function (key, value) { - this._renderSelectOption(regionList, key, value); + regionsEntries = _.pairs(this.options.regionJson[country]); + regionsEntries.sort(function (a, b) { + return a[1].name > b[1].name ? 1 : -1; + }); + $.each(regionsEntries, $.proxy(function (key, value) { + regionId = value[0]; + regionData = value[1]; + this._renderSelectOption(regionList, regionId, regionData); }, this)); if (this.currentRegionOption) { @@ -193,7 +202,7 @@ define([ regionList.hide(); container.hide(); } else { - regionList.show(); + regionList.removeAttr('disabled').show(); } } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js index b15599673095f..97dff2f6fd47a 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -5,8 +5,10 @@ define([ 'jquery', - 'jquery-ui-modules/widget' -], function ($) { + 'Magento_Ui/js/modal/confirm', + 'jquery-ui-modules/widget', + 'mage/translate' +], function ($, confirm) { 'use strict'; $.widget('mage.shoppingCart', { @@ -15,13 +17,7 @@ define([ var items, i, reload; $(this.options.emptyCartButton).on('click', $.proxy(function () { - $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); - $(this.options.updateCartActionContainer) - .attr('name', 'update_cart_action').attr('value', 'empty_cart'); - - if ($(this.options.emptyCartButton).parents('form').length > 0) { - $(this.options.emptyCartButton).parents('form').submit(); - } + this._confirmClearCart(); }, this)); items = $.find('[data-role="cart-item-qty"]'); @@ -61,6 +57,40 @@ define([ $('div.block.block-minicart').off('dropdowndialogclose'); })); }, this)); + }, + + /** + * Display confirmation modal for clearing the cart + * @private + */ + _confirmClearCart: function () { + var self = this; + + confirm({ + content: $.mage.__('Are you sure you want to remove all items from your shopping cart?'), + actions: { + /** + * Confirmation modal handler to execute clear cart action + */ + confirm: function () { + self.clearCart(); + } + } + }); + }, + + /** + * Prepares the form and submit to clear the cart + * @public + */ + clearCart: function () { + $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); + $(this.options.updateCartActionContainer) + .attr('name', 'update_cart_action').attr('value', 'empty_cart'); + + if ($(this.options.emptyCartButton).parents('form').length > 0) { + $(this.options.emptyCartButton).parents('form').submit(); + } } }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js index a7ccb217fa102..2e501c0c42b77 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js @@ -42,7 +42,6 @@ define([ update: function () { $(this.options.targetElement).trigger('contentUpdated'); this._calcHeight(); - this._isOverflowed(); }, /** @@ -135,23 +134,6 @@ define([ this._on(this.element, events); this._calcHeight(); - this._isOverflowed(); - }, - - /** - * Add 'overflowed' class to minicart items wrapper element - * - * @private - */ - _isOverflowed: function () { - var list = $(this.options.minicart.list), - cssOverflowClass = 'overflowed'; - - if (this.scrollHeight > list.innerHeight()) { - list.parent().addClass(cssOverflowClass); - } else { - list.parent().removeClass(cssOverflowClass); - } }, /** 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/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index c77e72e38107a..d3890556f3ccd 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -123,20 +123,6 @@ define([ $('[data-block="minicart"]').find('[data-role="dropdownDialog"]').dropdownDialog('close'); }, - /** - * @return {Boolean} - */ - closeSidebar: function () { - var minicart = $('[data-block="minicart"]'); - - minicart.on('click', '[data-action="close"]', function (event) { - event.stopPropagation(); - minicart.find('[data-role="dropdownDialog"]').dropdownDialog('close'); - }); - - return true; - }, - /** * @param {String} productType * @return {*|String} diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index fe8d7782e5eae..646e6156ec646 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -299,6 +299,12 @@ define([ this.source.set('params.invalid', false); this.triggerShippingDataValidateEvent(); + if (!quote.shippingMethod()['method_code']) { + this.errorValidationMessage( + $t('The shipping method is missing. Select the shipping method and try again.') + ); + } + if (emailValidationResult && this.source.get('params.invalid') || !quote.shippingMethod()['method_code'] || diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html index c5fd6d545702b..a47f11e5787c3 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html @@ -21,7 +21,12 @@ id="btn-minicart-close" class="action close" data-action="close" - data-bind="attr: { title: $t('Close') }"> + data-bind=" + attr: { + title: $t('Close') + }, + click: closeMinicart() + "> <span translate="'Close'"/> </button> @@ -74,7 +79,6 @@ <ifnot args="getCartParam('summary_count')"> <strong class="subtitle empty" - data-bind="visible: closeSidebar()" translate="'You have no items in your shopping cart.'" /> <if args="getCartParam('cart_empty_message')"> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html index 5489089452d85..053b15b4ad343 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html @@ -44,11 +44,11 @@ <!-- ko if: Array.isArray(option.value) --> <span data-bind="html: option.value.join('<br>')"></span> <!-- /ko --> - <!-- ko if: (!Array.isArray(option.value) && option.option_type == 'file') --> + <!-- ko if: (!Array.isArray(option.value) && ['file', 'html'].includes(option.option_type)) --> <span data-bind="html: option.value"></span> <!-- /ko --> - <!-- ko if: (!Array.isArray(option.value) && option.option_type != 'file') --> - <span data-bind="text: option.value"></span> + <!-- ko if: (!Array.isArray(option.value) && !['file', 'html'].includes(option.option_type)) --> + <span data-bind="text: option.value"></span> <!-- /ko --> </dd> <!-- /ko --> @@ -112,7 +112,7 @@ </div> </div> </div> - <div class="message notice" if="message"> - <div data-bind="text: message"></div> + <div class="message notice" if="$data.message"> + <div data-bind="text: $data.message"></div> </div> </li> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/shipping-method-item.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/shipping-method-item.html index 11e419054582f..fd6be02657e3e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/shipping-method-item.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/shipping-method-item.html @@ -15,9 +15,11 @@ attr="'aria-labelledby': 'label_method_' + method.method_code + '_' + method.carrier_code + ' ' + 'label_carrier_' + method.method_code + '_' + method.carrier_code, 'checked': element.rates().length == 1 || element.isSelected" /> </td> + <!-- ko ifnot: (method.error_message) --> <td class="col col-price"> <each args="element.getRegion('price')" render="" /> </td> + <!-- /ko --> <td class="col col-method" attr="'id': 'label_method_' + method.method_code + '_' + method.carrier_code" text="method.method_title" /> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html index 4e49d4502d8a8..8fc514990d567 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html @@ -15,7 +15,7 @@ </strong> </div> <div class="content minicart-items" data-role="content"> - <div class="minicart-items-wrapper overflowed"> + <div class="minicart-items-wrapper"> <ol class="minicart-items"> <each args="items()"> <li class="product-item"> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html index eb218bbee9941..fa32ea1b212ae 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html @@ -5,7 +5,7 @@ */ --> <span class="product-image-container" - data-bind="attr: {'style': 'height: ' + getHeight($parents[1]) + 'px; width: ' + getWidth($parents[1]) + 'px;' }"> + data-bind="attr: {'style': 'height: ' + getHeight($parents[1])/2 + 'px; width: ' + getWidth($parents[1])/2 + 'px;' }"> <span class="product-image-wrapper"> <img data-bind="attr: {'src': getSrc($parents[1]), 'width': getWidth($parents[1]), 'height': getHeight($parents[1]), 'alt': getAlt($parents[1]), 'title': getAlt($parents[1]) }"/> diff --git a/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsListInterface.php b/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsListInterface.php index b91701acef04d..a15191244a030 100644 --- a/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsListInterface.php +++ b/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsListInterface.php @@ -13,6 +13,7 @@ * search filters without predefined limitations. * * @api + * @since 100.3.0 */ interface CheckoutAgreementsListInterface { @@ -21,6 +22,7 @@ interface CheckoutAgreementsListInterface * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \Magento\CheckoutAgreements\Api\Data\AgreementInterface[] + * @since 100.3.0 */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) : array; } diff --git a/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryInterface.php b/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryInterface.php index 5822b8f082fef..7dc757395a478 100644 --- a/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryInterface.php +++ b/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryInterface.php @@ -25,7 +25,7 @@ public function get($id, $storeId = null); * Lists active checkout agreements. * * @return \Magento\CheckoutAgreements\Api\Data\AgreementInterface[] - * @deprecated + * @deprecated 100.3.0 * @see \Magento\CheckoutAgreements\Api\CheckoutAgreementsListInterface::getList */ public function getList(); diff --git a/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php b/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php index 4a35a58a41ff9..21b318cd00f09 100644 --- a/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php +++ b/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php @@ -12,7 +12,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { /** * @var \Magento\CheckoutAgreements\Model\ResourceModel\Agreement\CollectionFactory - * @deprecated + * @deprecated 100.2.2 */ protected $_collectionFactory; diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml index 5cb256090c196..d7b1565c95400 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml @@ -4,30 +4,37 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Files.LineLength - +use Magento\CheckoutAgreements\Model\AgreementModeOptions; ?> <?php /** * @var $block \Magento\CheckoutAgreements\Block\Agreements */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php if (!$block->getAgreements()) { return; } ?> <ol id="checkout-agreements" class="agreements checkout items"> <?php /** @var \Magento\CheckoutAgreements\Api\Data\AgreementInterface $agreement */ ?> - <?php foreach ($block->getAgreements() as $agreement) :?> + <?php foreach ($block->getAgreements() as $agreement):?> <li class="item"> - <div class="checkout-agreement-item-content"<?= $block->escapeHtmlAttr($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> - <?php if ($agreement->getIsHtml()) :?> + <div class="checkout-agreement-item-content" id="<?= /* @noEscape */ $agreement->getAgreementId() ?>" + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getContent() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml(nl2br($agreement->getContent())) ?> <?php endif; ?> </div> - <form id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree required"> - <?php if ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL) :?> + <?php if ($agreement->getContentHeight()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'height:' . $agreement->getContentHeight(), + '#' . $agreement->getAgreementId() + ) ?> + <?php endif; ?> + <form id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" + class="field choice agree required"> + <?php if ($agreement->getMode() == AgreementModeOptions::MODE_MANUAL):?> <input type="checkbox" id="agreement-<?= (int) $agreement->getAgreementId() ?>" name="agreement[<?= (int) $agreement->getAgreementId() ?>]" @@ -37,19 +44,19 @@ data-validate="{required:true}"/> <label class="label" for="agreement-<?= (int) $agreement->getAgreementId() ?>"> <span> - <?php if ($agreement->getIsHtml()) :?> + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getCheckboxText() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml($agreement->getCheckboxText()) ?> <?php endif; ?> </span> </label> - <?php elseif ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO) :?> + <?php elseif ($agreement->getMode() == AgreementModeOptions::MODE_AUTO):?> <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree"> <span> - <?php if ($agreement->getIsHtml()) :?> + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getCheckboxText() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml($agreement->getCheckboxText()) ?> <?php endif; ?> </span> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml index fb2d5168d21de..bc714f21e4dfa 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml @@ -4,13 +4,12 @@ * See COPYING.txt for license details. */ -// @deprecated -// phpcs:disable Magento2.Files.LineLength - +use Magento\CheckoutAgreements\Model\AgreementModeOptions; ?> <?php /** * @var $block \Magento\CheckoutAgreements\Block\Agreements + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php if (!$block->getAgreements()) { @@ -18,17 +17,24 @@ } ?> <ol id="checkout-agreements" class="agreements checkout items"> <?php /** @var \Magento\CheckoutAgreements\Api\Data\AgreementInterface $agreement */ ?> - <?php foreach ($block->getAgreements() as $agreement) :?> + <?php foreach ($block->getAgreements() as $agreement):?> <li class="item"> - <div class="checkout-agreement-item-content"<?= $block->escapeHtmlAttr($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> - <?php if ($agreement->getIsHtml()) :?> + <div class="checkout-agreement-item-content" id="<?= /* @noEscape */ $agreement->getAgreementId() ?>" + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getContent() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml(nl2br($agreement->getContent())) ?> <?php endif; ?> </div> - <?php if ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL) :?> - <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree required"> + <?php if ($agreement->getContentHeight()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'height:' . $agreement->getContentHeight(), + '#' . $agreement->getAgreementId() + ) ?> + <?php endif; ?> + <?php if ($agreement->getMode() == AgreementModeOptions::MODE_MANUAL):?> + <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" + class="field choice agree required"> <input type="checkbox" id="agreement-<?= (int) $agreement->getAgreementId() ?>" name="agreement[<?= (int) $agreement->getAgreementId() ?>]" @@ -38,20 +44,20 @@ data-validate="{required:true}"/> <label class="label" for="agreement-<?= (int) $agreement->getAgreementId() ?>"> <span> - <?php if ($agreement->getIsHtml()) :?> + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getCheckboxText() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml($agreement->getCheckboxText()) ?> <?php endif; ?> </span> </label> </div> - <?php elseif ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO) :?> + <?php elseif ($agreement->getMode() == AgreementModeOptions::MODE_AUTO):?> <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree"> <span> - <?php if ($agreement->getIsHtml()) :?> + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getCheckboxText() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml($agreement->getCheckboxText()) ?> <?php endif; ?> </span> diff --git a/app/code/Magento/Cms/Api/Data/PageInterface.php b/app/code/Magento/Cms/Api/Data/PageInterface.php index 7a31ab1b9a94f..402c2ccd289e0 100644 --- a/app/code/Magento/Cms/Api/Data/PageInterface.php +++ b/app/code/Magento/Cms/Api/Data/PageInterface.php @@ -125,7 +125,7 @@ public function getSortOrder(); * Get layout update xml * * @return string|null - * @deprecated Existing updates are applied, new are not accepted. + * @deprecated 103.0.4 Existing updates are applied, new are not accepted. */ public function getLayoutUpdateXml(); @@ -146,7 +146,7 @@ public function getCustomRootTemplate(); /** * Get custom layout update xml * - * @deprecated Existing updates are applied, new are not accepted. + * @deprecated 103.0.4 Existing updates are applied, new are not accepted. * @see \Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface * @return string|null */ @@ -275,7 +275,7 @@ public function setSortOrder($sortOrder); * * @param string $layoutUpdateXml * @return \Magento\Cms\Api\Data\PageInterface - * @deprecated Existing updates are applied, new are not accepted. + * @deprecated 103.0.4 Existing updates are applied, new are not accepted. */ public function setLayoutUpdateXml($layoutUpdateXml); @@ -300,7 +300,7 @@ public function setCustomRootTemplate($customRootTemplate); * * @param string $customLayoutUpdateXml * @return \Magento\Cms\Api\Data\PageInterface - * @deprecated Existing updates are applied, new are not accepted. + * @deprecated 103.0.4 Existing updates are applied, new are not accepted. * @see \Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface */ public function setCustomLayoutUpdateXml($customLayoutUpdateXml); diff --git a/app/code/Magento/Cms/Api/GetBlockByIdentifierInterface.php b/app/code/Magento/Cms/Api/GetBlockByIdentifierInterface.php index 7e8bbdc3e8d2f..70fadae42f327 100644 --- a/app/code/Magento/Cms/Api/GetBlockByIdentifierInterface.php +++ b/app/code/Magento/Cms/Api/GetBlockByIdentifierInterface.php @@ -8,6 +8,7 @@ /** * Command to load the block data by specified identifier * @api + * @since 103.0.0 */ interface GetBlockByIdentifierInterface { @@ -18,6 +19,7 @@ interface GetBlockByIdentifierInterface * @param int $storeId * @throws \Magento\Framework\Exception\NoSuchEntityException * @return \Magento\Cms\Api\Data\BlockInterface + * @since 103.0.0 */ public function execute(string $identifier, int $storeId) : \Magento\Cms\Api\Data\BlockInterface; } diff --git a/app/code/Magento/Cms/Api/GetPageByIdentifierInterface.php b/app/code/Magento/Cms/Api/GetPageByIdentifierInterface.php index f432f678d3a12..8f47de5266321 100644 --- a/app/code/Magento/Cms/Api/GetPageByIdentifierInterface.php +++ b/app/code/Magento/Cms/Api/GetPageByIdentifierInterface.php @@ -8,6 +8,7 @@ /** * Command to load the page data by specified identifier * @api + * @since 103.0.0 */ interface GetPageByIdentifierInterface { @@ -18,6 +19,7 @@ interface GetPageByIdentifierInterface * @param int $storeId * @throws \Magento\Framework\Exception\NoSuchEntityException * @return \Magento\Cms\Api\Data\PageInterface + * @since 103.0.0 */ public function execute(string $identifier, int $storeId) : \Magento\Cms\Api\Data\PageInterface; } diff --git a/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php b/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php index c6bf4c8404701..07c5f5c8a9e07 100644 --- a/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php +++ b/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php @@ -9,12 +9,14 @@ * Utility Cms Pages * * @api + * @since 102.0.4 */ interface GetUtilityPageIdentifiersInterface { /** * Get List Page Identifiers * @return array + * @since 102.0.4 */ public function execute(); } 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/Page/PostDataProcessor.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php index f1862026f0e35..71b620a1632ca 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php @@ -79,7 +79,7 @@ public function filter($data) * * @param array $data * @return bool Return FALSE if some item is invalid - * @deprecated + * @deprecated 103.0.2 */ public function validate($data) { 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 5344472a79a9d..1f991bb47c6fd 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php @@ -4,10 +4,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Action\HttpPostActionInterface; -use Magento\Framework\App\Filesystem\DirectoryList; /** * Delete image folder. @@ -60,13 +62,8 @@ public function execute() { try { $path = $this->getStorage()->getCmsWysiwygImages()->getCurrentPath(); - if (!$this->directoryResolver->validatePath($path, DirectoryList::MEDIA)) { - throw new \Magento\Framework\Exception\LocalizedException( - __('Directory %1 is not under storage root path.', $path) - ); - } $this->getStorage()->deleteDirectory($path); - + return $this->resultRawFactory->create(); } catch (\Exception $e) { $result = ['error' => true, 'message' => $e->getMessage()]; 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/BlockRepository.php b/app/code/Magento/Cms/Model/BlockRepository.php index fa29cc9ff7631..d0df0d2b31caa 100644 --- a/app/code/Magento/Cms/Model/BlockRepository.php +++ b/app/code/Magento/Cms/Model/BlockRepository.php @@ -196,7 +196,7 @@ public function deleteById($blockId) /** * Retrieve collection processor * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php index 23a452c0fe58c..3413aa7b0dd6c 100644 --- a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php +++ b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php @@ -20,7 +20,7 @@ class PageLayout implements OptionSourceInterface /** * @var array - * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + * @deprecated 103.0.1 since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles */ protected $options; diff --git a/app/code/Magento/Cms/Model/PageRepository.php b/app/code/Magento/Cms/Model/PageRepository.php index 2de44b6691274..0439fbcd2f799 100644 --- a/app/code/Magento/Cms/Model/PageRepository.php +++ b/app/code/Magento/Cms/Model/PageRepository.php @@ -256,7 +256,7 @@ public function deleteById($pageId) /** * Retrieve collection processor * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Cms/Model/Template/Filter.php b/app/code/Magento/Cms/Model/Template/Filter.php index 7e71a06de1f31..66bca1052b797 100644 --- a/app/code/Magento/Cms/Model/Template/Filter.php +++ b/app/code/Magento/Cms/Model/Template/Filter.php @@ -5,32 +5,11 @@ */ namespace Magento\Cms\Model\Template; -use Magento\Framework\Exception\LocalizedException; - /** * Cms Template Filter Model */ class Filter extends \Magento\Email\Model\Template\Filter { - /** - * Whether to allow SID in store directive: AUTO - * - * @var bool - */ - protected $_useSessionInUrl; - - /** - * Setter whether SID is allowed in store directive - * - * @param bool $flag - * @return $this - */ - public function setUseSessionInUrl($flag) - { - $this->_useSessionInUrl = (bool)$flag; - return $this; - } - /** * Retrieve media file URL directive * @@ -41,8 +20,8 @@ public function mediaDirective($construction) { // phpcs:ignore Magento2.Functions.DiscouragedFunction $params = $this->getParameters(html_entity_decode($construction[2], ENT_QUOTES)); - if (preg_match('/\.\.(\\\|\/)/', $params['url'])) { - throw new \InvalidArgumentException('Image path must be absolute'); + if (preg_match('/(^.*:\/\/.*|\.\.\/.*)/', $params['url'])) { + throw new \InvalidArgumentException('Image path must be absolute and not include URLs'); } return $this->_storeManager->getStore()->getBaseMediaDir() . '/' . $params['url']; diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Config.php b/app/code/Magento/Cms/Model/Wysiwyg/Config.php index 1da7b99c6d886..95f5971251f1c 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Config.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Config.php @@ -61,14 +61,14 @@ class Config extends \Magento\Framework\DataObject implements ConfigInterface /** * @var \Magento\Variable\Model\Variable\Config - * @deprecated + * @deprecated 103.0.0 * @see \Magento\Cms\Model\ConfigProvider::processVariableConfig */ protected $_variableConfig; /** * @var \Magento\Widget\Model\Widget\Config - * @deprecated + * @deprecated 103.0.0 * @see \Magento\Cms\Model\ConfigProvider::processWidgetConfig */ protected $_widgetConfig; 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/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index f0a232bdccccc..0cc108e5bed8b 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -22,6 +22,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * * @api * @since 100.0.2 @@ -152,6 +153,11 @@ class Storage extends \Magento\Framework\DataObject */ private $ioFile; + /** + * @var \Magento\Framework\File\Mime|null + */ + private $mime; + /** * Construct * @@ -174,6 +180,7 @@ class Storage extends \Magento\Framework\DataObject * @param \Magento\Framework\Filesystem\DriverInterface $file * @param \Magento\Framework\Filesystem\Io\File|null $ioFile * @param \Psr\Log\LoggerInterface|null $logger + * @param \Magento\Framework\File\Mime $mime * * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -197,7 +204,8 @@ public function __construct( array $data = [], \Magento\Framework\Filesystem\DriverInterface $file = null, \Magento\Framework\Filesystem\Io\File $ioFile = null, - \Psr\Log\LoggerInterface $logger = null + \Psr\Log\LoggerInterface $logger = null, + \Magento\Framework\File\Mime $mime = null ) { $this->_session = $session; $this->_backendUrl = $backendUrl; @@ -217,6 +225,7 @@ public function __construct( $this->_dirs = $dirs; $this->file = $file ?: ObjectManager::getInstance()->get(\Magento\Framework\Filesystem\Driver\File::class); $this->ioFile = $ioFile ?: ObjectManager::getInstance()->get(\Magento\Framework\Filesystem\Io\File::class); + $this->mime = $mime ?: ObjectManager::getInstance()->get(\Magento\Framework\File\Mime::class); parent::__construct($data); } @@ -225,6 +234,8 @@ public function __construct( * * @param string $path * @return void + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ protected function createSubDirectories($path) { @@ -295,6 +306,7 @@ protected function removeItemFromCollection($collection, $conditions) * * @param string $path Parent directory path * @return \Magento\Framework\Data\Collection\Filesystem + * @throws \Exception */ public function getDirsCollection($path) { @@ -359,7 +371,7 @@ public function getFilesCollection($path, $type = null) $item->setUrl($this->_cmsWysiwygImages->getCurrentUrl() . $item->getBasename()); $itemStats = $this->file->stat($item->getFilename()); $item->setSize($itemStats['size']); - $item->setMimeType(\mime_content_type($item->getFilename())); + $item->setMimeType($this->mime->getMimeType($item->getFilename())); if ($this->isImage($item->getBasename())) { $thumbUrl = $this->getThumbnailUrl($item->getFilename(), true); @@ -393,6 +405,7 @@ public function getFilesCollection($path, $type = null) * * @param string $path Path to the directory * @return \Magento\Cms\Model\Wysiwyg\Images\Storage\Collection + * @throws \Exception */ public function getCollection($path = null) { @@ -485,6 +498,9 @@ public function deleteDirectory($path) * * @param string $path * @return void + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\ValidatorException */ protected function _deleteByPath($path) { @@ -500,6 +516,8 @@ protected function _deleteByPath($path) * * @param string $target File path to be deleted * @return $this + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ public function deleteFile($target) { @@ -561,9 +579,11 @@ public function uploadFile($targetPath, $type = null) /** * Thumbnail path getter * - * @param string $filePath original file path - * @param bool $checkFile OPTIONAL is it necessary to check file availability + * @param string $filePath original file path + * @param bool $checkFile OPTIONAL is it necessary to check file availability * @return string|false + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ public function getThumbnailPath($filePath, $checkFile = false) { @@ -587,9 +607,11 @@ public function getThumbnailPath($filePath, $checkFile = false) /** * Thumbnail URL getter * - * @param string $filePath original file path - * @param bool $checkFile OPTIONAL is it necessary to check file availability + * @param string $filePath original file path + * @param bool $checkFile OPTIONAL is it necessary to check file availability * @return string|false + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ public function getThumbnailUrl($filePath, $checkFile = false) { @@ -610,6 +632,8 @@ public function getThumbnailUrl($filePath, $checkFile = false) * @param string $source Image path to be resized * @param bool $keepRatio Keep aspect ratio or not * @return bool|string Resized filepath or false if errors were occurred + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ public function resizeFile($source, $keepRatio = true) { @@ -628,8 +652,12 @@ public function resizeFile($source, $keepRatio = true) } $image = $this->_imageFactory->create(); $image->open($source); + $image->keepAspectRatio($keepRatio); - $image->resize($this->_resizeParameters['width'], $this->_resizeParameters['height']); + + list($imageWidth, $imageHeight) = $this->getResizedParams($source); + + $image->resize($imageWidth, $imageHeight); $dest = $targetDir . '/' . $this->ioFile->getPathInfo($source)['basename']; $image->save($dest); if ($this->_directory->isFile($this->_directory->getRelativePath($dest))) { @@ -638,11 +666,37 @@ public function resizeFile($source, $keepRatio = true) return false; } + /** + * Return width height for the image resizing. + * + * @param string $source + * @return array + */ + private function getResizedParams(string $source): array + { + $configWidth = $this->_resizeParameters['width']; + $configHeight = $this->_resizeParameters['height']; + + //phpcs:ignore Generic.PHP.NoSilencedErrors + list($imageWidth, $imageHeight) = @getimagesize($source); + + if ($imageWidth && $imageHeight) { + $imageWidth = $configWidth > $imageWidth ? $imageWidth : $configWidth; + $imageHeight = $configHeight > $imageHeight ? $imageHeight : $configHeight; + + return [$imageWidth, $imageHeight]; + } + return [$configWidth, $configHeight]; + } + /** * Resize images on the fly in controller action * * @param string $filename File basename * @return bool|string Thumbnail path or false for errors + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\ValidatorException */ public function resizeOnTheFly($filename) { @@ -658,6 +712,8 @@ public function resizeOnTheFly($filename) * * @param bool|string $filePath Path to the file * @return string + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ public function getThumbsPath($filePath = false) { @@ -782,10 +838,20 @@ protected function _validatePath($path) * * @param string $path * @return string + * @throws \Magento\Framework\Exception\ValidatorException */ protected function _sanitizePath($path) { - return rtrim(preg_replace('~[/\\\]+~', '/', $this->_directory->getDriver()->getRealPathSafety($path)), '/'); + return rtrim( + preg_replace( + '~[/\\\]+~', + '/', + $this->_directory->getDriver()->getRealPathSafety( + $this->_directory->getAbsolutePath($path) + ) + ), + '/' + ); } /** @@ -793,6 +859,7 @@ protected function _sanitizePath($path) * * @param string $path * @return string|bool + * @throws \Magento\Framework\Exception\ValidatorException */ protected function _getRelativePathToRoot($path) { diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSPageByUrlKeyActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSPageByUrlKeyActionGroup.xml new file mode 100644 index 0000000000000..01e430807d7bd --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSPageByUrlKeyActionGroup.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteCMSPageByUrlKeyActionGroup"> + <annotations> + <description>Goes to the Admin CMS Pages page. Filters the grid based on the provided Page url key. Deletes the Page via the grid.</description> + </annotations> + <arguments> + <argument name="pageUrlKey" type="string" defaultValue="cms_page"/> + </arguments> + + <amOnPage url="{{CmsPagesPage.url}}" stepKey="navigateToCMSPagesGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{CmsPagesPageActionsSection.clearAllButton}}" dependentSelector="{{CmsPagesPageActionsSection.activeFilters}}" visible="true" stepKey="clickToResetFilter"/> + <waitForPageLoad stepKey="waitForPageLoadAfterClearFilters"/> + <click selector="{{CmsPagesPageActionsSection.filterButton}}" stepKey="clickFilterButton"/> + <fillField selector="{{CmsPagesPageActionsSection.URLKey}}" userInput="{{pageUrlKey}}" stepKey="fillPageUrlKeyFilter"/> + <click selector="{{CmsPagesPageActionsSection.ApplyFiltersBtn}}" stepKey="applyFilter"/> + <waitForElementVisible selector="{{CmsPagesPageActionsSection.select(pageUrlKey)}}" stepKey="waitItemAppears"/> + <click selector="{{CmsPagesPageActionsSection.select(pageUrlKey)}}" stepKey="clickSelect"/> + <click selector="{{CmsPagesPageActionsSection.delete(pageUrlKey)}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{CmsPagesPageActionsSection.deleteConfirm}}" stepKey="waitForOkButtonToBeVisible"/> + <click selector="{{CmsPagesPageActionsSection.deleteConfirm}}" stepKey="clickOkButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessageAppeared"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The page has been deleted." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminNavigateToPageGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminNavigateToPageGridActionGroup.xml index 7dc68e7a5a891..6128db33a2afe 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminNavigateToPageGridActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminNavigateToPageGridActionGroup.xml @@ -8,12 +8,12 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminNavigateToPageGridActionGroup"> + <actionGroup name="AdminNavigateToPageGridActionGroup" deprecated="Use AdminOpenCMSPagesGridActionGroup instead."> <annotations> <description>Navigates to CMS page grid.</description> </annotations> <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnPagePagesGrid"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> </actionGroups> 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/Data/WysiwygConfigData.xml b/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml index 46a968959407f..ea5e90383511c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml @@ -18,7 +18,7 @@ <data key="scope_id">0</data> <data key="value">hidden</data> </entity> - <entity name="WysiwygTinyMCE3Enable"> + <entity name="WysiwygTinyMCE3Enable" deprecated="Use WysiwygTinyMCE4Enable instead"> <data key="path">cms/wysiwyg/editor</data> <data key="scope_id">0</data> <data key="value">Magento_Tinymce3/tinymce3Adapter</data> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml index d487517269c01..ac9c66fe82c74 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml @@ -15,8 +15,10 @@ <element name="idColumn" type="button" selector="//div[contains(@data-role, 'grid-wrapper')]/table/thead/tr/th/span[contains(text(), 'ID')]"/> <element name="clearAll" type="button" selector="//div[@class='admin__data-grid-header']//button[contains(text(), 'Clear all')]"/> <element name="activeFilters" type="button" selector="//div[@class='admin__data-grid-header']//span[contains(text(), 'Active filters:')]" /> + <element name="activeFilterDiv" type="button" selector="(//div[contains(@class, 'admin__data-grid-filters-current') and contains(@class, '_show')])[1]"/> <element name="FilterBtn" type="input" selector="//button[text()='Filters']"/> <element name="URLKey" type="input" selector="//div[@class='admin__form-field-control']/input[@name='identifier']"/> <element name="ApplyFiltersBtn" type="button" selector="//span[text()='Apply Filters']"/> + <element name="blockGridRowByTitle" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml index 6f16fa54a6ebf..a287685dbdefb 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml @@ -11,14 +11,15 @@ <section name="CmsPagesPageActionsSection"> <element name="filterButton" type="input" selector="//button[text()='Filters']"/> <element name="URLKey" type="input" selector="//div[@class='admin__form-field-control']/input[@name='identifier']"/> - <element name="ApplyFiltersBtn" type="button" selector="//span[text()='Apply Filters']"/> - <element name="searchInput" type="input" selector="//*[@id='fulltext']"/> + <element name="ApplyFiltersBtn" type="button" selector="//span[text()='Apply Filters']" timeout="60"/> + <element name="searchInput" type="input" selector="#fulltext"/> <element name="searchButton" type="button" selector="//*[@id='fulltext']/parent::*/button"/> <element name="addNewPageButton" type="button" selector="#add" timeout="30"/> <element name="select" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//button[text()='Select']" parameterized="true"/> <element name="edit" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Edit']" parameterized="true"/> <element name="preview" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='View']" parameterized="true"/> - <element name="clearAllButton" type="button" selector="//div[@class='admin__data-grid-header']//button[contains(text(), 'Clear all')]"/> + <element name="clearAllButton" type="button" selector="//div[@class='admin__data-grid-header']//button[contains(text(), 'Clear all')]" timeout="60"/> + <element name="clearFilters" type="button" selector=".admin__data-grid-header button[data-action='grid-filter-reset']" timeout="30"/> <element name="activeFilters" type="button" selector="//div[@class='admin__data-grid-header']//span[contains(text(), 'Active filters:')]" /> <element name="spinner" type="input" selector='//div[@data-component="cms_page_listing.cms_page_listing.cms_page_columns"]'/> <element name="firstItemSelectButton" type="button" selector=".data-grid .action-select-wrap button.action-select"/> @@ -31,5 +32,6 @@ <element name="massActionsButton" type="button" selector="//div[@class='admin__data-grid-header'][(not(ancestor::*[@class='sticky-header']) and not(contains(@style,'visibility: hidden'))) or (ancestor::*[@class='sticky-header' and not(contains(@style,'display: none'))])]//button[contains(@class, 'action-select')]" /> <element name="massActionsOption" type="button" selector="//div[@class='admin__data-grid-header'][(not(ancestor::*[@class='sticky-header']) and not(contains(@style,'visibility: hidden'))) or (ancestor::*[@class='sticky-header' and not(contains(@style,'display: none'))])]//span[contains(@class, 'action-menu-item') and .= '{{action}}']" parameterized="true"/> <element name="gridDataRow" type="input" selector=".data-row .data-grid-cell-content"/> + <element name="pagesGridRowByTitle" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml index 112335e726270..725d050554f2d 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="MediaGallerySection"> <element name="Browse" type="button" selector=".mce-i-browse"/> - <element name="browseForImage" type="button" selector="//*[@id='srcbrowser']"/> + <element name="browseForImage" type="button" selector="#srcbrowser"/> <element name="BrowseUploadImage" type="file" selector=".fileupload"/> <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> <element name="imageOrImageCopy" type="text" selector="//div[contains(@class,'media-gallery-modal')]//img[contains(@alt, '{{arg1}}.{{arg2}}')]|//img[contains(@alt,'{{arg1}}_') and contains(@alt,'.{{arg2}}')]" parameterized="true"/> @@ -17,7 +17,8 @@ <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open"/> <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last"/> - <element name="ImageDescriptionTinyMCE3" type="input" selector="#alt"/> + <element name="ImageDescriptionTinyMCE3" type="input" selector="#alt" deprecated="Deprecated New element was introduced. Please use 'ImageDescriptionTinyMCE4'"/> + <element name="ImageDescriptionTinyMCE4" type="input" selector="#alt" /> <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first"/> <element name="UploadImage" type="file" selector=".fileupload"/> <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml index 1869a6544c3d3..5be91f61e1e1e 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml @@ -38,6 +38,8 @@ <element name="PageSize" type="input" selector="input[name='parameters[page_size]']"/> <element name="ProductAttribute" type="multiselect" selector="select[name='parameters[show_attributes][]']"/> <element name="ButtonToShow" type="multiselect" selector="select[name='parameters[show_buttons][]']"/> + <element name="InputAnchorCustomText" type="input" selector="input[name='parameters[anchor_text]']"/> + <element name="InputAnchorCustomTitle" type="input" selector="input[name='parameters[title]']"/> <!--Compare on Storefront--> <element name="ProductName" type="text" selector=".product.name.product-item-name"/> <element name="CompareBtn" type="button" selector=".action.tocompare"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml index 7c2aedceb9b7e..35d8e692cd460 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml @@ -8,7 +8,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminAddImageToCMSPageTinyMCE3Test"> + <test name="AdminAddImageToCMSPageTinyMCE3Test" deprecated="TinyMCE3 is no longer supported"> <annotations> <features value="Cms"/> <stories value="Admin should be able to upload images with TinyMCE3 WYSIWYG"/> @@ -17,6 +17,9 @@ <description value="Verify that admin is able to upload image to CMS Page with TinyMCE3 enabled"/> <severity value="BLOCKER"/> <testCaseId value="MAGETWO-95725"/> + <skip> + <issueId value="DEPRECATED">TinyMCE3 is no longer supported</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -25,20 +28,23 @@ <magentoCLI command="config:set cms/wysiwyg/editor Magento_Tinymce3/tinymce3Adapter" stepKey="enableTinyMCE3"/> </before> <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> <!-- Switch WYSIWYG editor to TinyMCE4--> <comment userInput="Reset editor as TinyMCE4" stepKey="chooseTinyMCE4AsEditor"/> <magentoCLI command="config:set cms/wysiwyg/editor mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter" stepKey="enableTinyMCE4"/> <actionGroup ref="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" /> - <waitForElementVisible selector="{{TinyMCESection.TinyMCE3}}" stepKey="waitForTinyMCE3"/> - <seeElement selector="{{TinyMCESection.TinyMCE3}}" stepKey="seeTinyMCE3" /> + <comment userInput="removing deprecated element" stepKey="waitForTinyMCE3"/> + <comment userInput="removing deprecated element" stepKey="seeTinyMCE3" /> <wait time="3" stepKey="waiting"/> <comment userInput="Click Insert image button" stepKey="clickImageButton"/> - <click selector="{{TinyMCESection.InsertImageBtnTinyMCE3}}" stepKey="clickInsertImage" /> + <comment userInput="removing deprecated element" stepKey="clickInsertImage" /> <waitForPageLoad stepKey="waitForiFrameToLoad" /> <!-- Switch to the Edit/Insert Image iFrame --> <comment userInput="Switching to iFrame" stepKey="insertImageiFrame"/> @@ -47,6 +53,9 @@ <click selector="{{MediaGallerySection.browseForImage}}" stepKey="clickBrowse"/> <switchToIFrame stepKey="switchOutOfIFrame"/> <waitForPageLoad stepKey="waitForPageToLoad" /> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> @@ -59,7 +68,7 @@ <executeJS function="document.querySelector('.clearlooks2 iframe').setAttribute('name', 'insert-image');" stepKey="makeIFrameInteractable2"/> <switchToIFrame selector="insert-image" stepKey="switchToIFrame2"/> <waitForElementVisible selector="{{MediaGallerySection.insertBtn}}" stepKey="waitForInsertBtnOnIFrame" /> - <fillField selector="{{MediaGallerySection.ImageDescriptionTinyMCE3}}" userInput="{{ImageUpload.content}}" stepKey="fillImageDescription" /> + <comment userInput="removing deprecated element" stepKey="fillImageDescription" /> <click selector="{{MediaGallerySection.insertBtn}}" stepKey="clickInsertBtn" /> <waitForPageLoad stepKey="wait3"/> <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml index 162c9a60fd6b1..c0424e09f8f76 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml @@ -37,6 +37,9 @@ <waitForPageLoad stepKey="waitForPageLoad2" /> <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> @@ -56,6 +59,10 @@ <seeElement selector="{{StorefrontBlockSection.mediaDescription}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontBlockSection.imageSource(ImageUpload.fileName)}}" stepKey="assertMediaSource"/> <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnEditPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> <conditionalClick selector="{{CmsPagesPageActionsSection.clearAllButton}}" dependentSelector="{{CmsPagesPageActionsSection.activeFilters}}" stepKey="clickToResetFilter" visible="true"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml index 0476ecf99ad36..4f67b81446ae7 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml @@ -23,6 +23,17 @@ <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> + <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYGFirst"/> + <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> <argument name="CMSPage" value="$$createCMSPage$$"/> </actionGroup> @@ -32,6 +43,9 @@ <waitForPageLoad stepKey="waitForPageLoad" /> <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> @@ -54,11 +68,5 @@ <waitForPageLoad stepKey="wait4"/> <seeElement selector="{{StorefrontCMSPageSection.mediaDescription}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontCMSPageSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> - <after> - <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYGFirst"/> - <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> - <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - </after> </test> </tests> 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/AdminCmsBlockGridUrlFilterApplierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml new file mode 100644 index 0000000000000..e2cf9c20627f8 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.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="AdminCmsBlockGridUrlFilterApplierTest"> + <annotations> + <features value="Cms"/> + <stories value="Filter CMS block using GET URL parameter"/> + <title value="Verify that filter is applied on block grid when filters parameter is set on url"/> + <description value="Accessing block grid url with filters parameter"/> + <severity value="MAJOR"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> + <group value="Cms"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="Sales25offBlock" stepKey="createBlock"/> + </before> + <after> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> + <deleteData createDataKey="createBlock" stepKey="deletePage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + <amOnPage url="{{CmsBlocksPage.url}}?filters[title]=$$createBlock.title$$" stepKey="navigateToBlockGridWithFilters"/> + <waitForPageLoad stepKey="waitForBlockGrid"/> + <see selector="{{BlockPageActionsSection.blockGridRowByTitle($$createBlock.title$$)}}" userInput="$$createBlock.title$$" stepKey="seeBlock"/> + <seeElement selector="{{BlockPageActionsSection.activeFilterDiv}}" stepKey="seeEnabledFilters"/> + <see selector="{{BlockPageActionsSection.activeFilterDiv}}" userInput="Title: $$createBlock.title$$" stepKey="seeBlockTitleFilter"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml new file mode 100644 index 0000000000000..1f1f1c98d507b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.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="AdminCmsPageGridUrlFilterApplierTest"> + <annotations> + <features value="CmsPage"/> + <stories value="Filter CMS page using GET URL parameter"/> + <title value="Verify that filter is applied on page grid when filters parameter is set on url"/> + <description value="Accessing page grid url with filters parameter"/> + <severity value="MAJOR"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> + <group value="Cms"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCmsPage" stepKey="createPage"/> + </before> + <after> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> + <deleteData createDataKey="createPage" stepKey="deletePage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + <amOnPage url="{{CmsPagesPage.url}}?filters[title]=$$createPage.title$$" stepKey="navigateToPageGridWithFilters"/> + <waitForPageLoad stepKey="waitForPageGrid"/> + <see selector="{{CmsPagesPageActionsSection.pagesGridRowByTitle($$createPage.title$$)}}" userInput="$$createPage.title$$" stepKey="seePage"/> + <seeElement selector="{{CmsPagesPageActionsSection.activeFilter}}" stepKey="seeEnabledFilters"/> + <see selector="{{CmsPagesPageActionsSection.activeFilter}}" userInput="Title: $$createPage.title$$" stepKey="seePageTitleFilter"/> + </test> +</tests> 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/AdminCreateCmsPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml index 90da152e7a7b1..f82ae3eed5de6 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml @@ -25,7 +25,7 @@ <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> - <actionGroup ref="AdminNavigateToPageGridActionGroup" stepKey="navigateToCmsPageGrid" /> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCmsPageGrid" /> <actionGroup ref="CreateNewPageWithBasicValues" stepKey="createNewPageWithBasicValues" /> <actionGroup ref="SaveCmsPageActionGroup" stepKey="clickSaveCmsPageButton" /> <actionGroup ref="VerifyCreatedCmsPage" stepKey="verifyCmsPage" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml index c46410dce919e..e80f6010b6c69 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml @@ -14,13 +14,14 @@ <stories value="Delete a CMS Page via the Admin"/> <title value="Admin should be able to delete CMS Pages"/> <description value="Admin should be able to delete CMS Pages"/> + <severity value="CRITICAL"/> <group value="Cms"/> <group value="WYSIWYGDisabled"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminNavigateToPageGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCmsPageGrid"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridSearchFilters"/> <actionGroup ref="CreateNewPageWithBasicValues" stepKey="createNewPageWithBasicValues"/> <actionGroup ref="SaveCmsPageActionGroup" stepKey="clickSaveCmsPageButton"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml index fe3e69880fc5c..eba7812e29a0c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml @@ -15,27 +15,37 @@ <title value="Check static blocks: ID should be unique per Store View"/> <description value="Check static blocks: ID should be unique per Store View"/> <severity value="BLOCKER"/> - <testCaseId value="MAGETWO-94229"/> + <testCaseId value="MC-25828"/> <group value="Cms"/> + <group value="WYSIWYGDisabled"/> </annotations> + <before> - <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> - <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> - <argument name="newWebsiteName" value="secondWebsite"/> - <argument name="websiteCode" value="second_website"/> + + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> - <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> - <argument name="website" value="secondWebsite"/> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> </actionGroup> - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="DeleteCMSBlockActionGroup"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + </after> + <!--Go to Cms blocks page--> <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSPagesGrid"/> <waitForPageLoad stepKey="waitForPageLoad1"/> @@ -73,13 +83,5 @@ <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock2"/> <waitForPageLoad stepKey="waitForPageLoad9"/> <see userInput="You saved the block." stepKey="VerifyBlockIsSaved2"/> - - <after> - <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> - <argument name="websiteName" value="secondWebsite"/> - </actionGroup> - <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="DeleteCMSBlockActionGroup"/> - <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> - </after> </test> </tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/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/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php index 9b02050156cc7..f942e62588f0e 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php @@ -43,7 +43,7 @@ public function testConstructorValidation($validators) new ValidationComposite($this->subject, $validators); } - public function testSaveInvokesValidatorsWithSucess() + public function testSaveInvokesValidatorsWithSuccess() { $validator1 = $this->getMockForAbstractClass(ValidatorInterface::class); $validator2 = $this->getMockForAbstractClass(ValidatorInterface::class); diff --git a/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php index e92554094224e..502b7aa63a1a2 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php @@ -114,4 +114,25 @@ public function testMediaDirectiveRelativePath() ->willReturn($baseMediaDir); $this->filter->mediaDirective($construction); } + + /** + * Test using media directive with a URL path including schema. + * + * @covers \Magento\Cms\Model\Template\Filter::mediaDirective + */ + public function testMediaDirectiveURL() + { + $this->expectException(\InvalidArgumentException::class); + + $baseMediaDir = 'pub/media'; + $construction = [ + '{{media url="http://wysiwyg/images/image.jpg"}}', + 'media', + ' url="http://wysiwyg/images/../image.jpg"' + ]; + $this->storeMock->expects($this->any()) + ->method('getBaseMediaDir') + ->willReturn($baseMediaDir); + $this->filter->mediaDirective($construction); + } } diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index 12f0791290b49..c2c748dcc7633 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -162,7 +162,7 @@ protected function setUp(): void $this->directoryMock = $this->createPartialMock( Write::class, - ['delete', 'getDriver', 'create', 'getRelativePath', 'isExist', 'isFile'] + ['delete', 'getDriver', 'create', 'getRelativePath', 'getAbsolutePath', 'isExist', 'isFile'] ); $this->directoryMock->expects( $this->any() @@ -304,6 +304,7 @@ public function testDeleteDirectoryOverRoot() $this->expectException('Magento\Framework\Exception\LocalizedException'); $this->expectExceptionMessage('Directory /storage/some/another/dir is not under storage root path.'); $this->driverMock->expects($this->atLeastOnce())->method('getRealPathSafety')->willReturnArgument(0); + $this->directoryMock->expects($this->atLeastOnce())->method('getAbsolutePath')->willReturnArgument(0); $this->imagesStorage->deleteDirectory(self::INVALID_DIRECTORY_OVER_ROOT); } @@ -315,6 +316,7 @@ public function testDeleteRootDirectory() $this->expectException('Magento\Framework\Exception\LocalizedException'); $this->expectExceptionMessage('We can\'t delete root directory /storage/root/dir right now.'); $this->driverMock->expects($this->atLeastOnce())->method('getRealPathSafety')->willReturnArgument(0); + $this->directoryMock->expects($this->atLeastOnce())->method('getAbsolutePath')->willReturnArgument(0); $this->imagesStorage->deleteDirectory(self::STORAGE_ROOT_DIR); } diff --git a/app/code/Magento/Cms/view/adminhtml/layout/cms_block_index.xml b/app/code/Magento/Cms/view/adminhtml/layout/cms_block_index.xml index d22eaf504e703..4a8002d89726d 100644 --- a/app/code/Magento/Cms/view/adminhtml/layout/cms_block_index.xml +++ b/app/code/Magento/Cms/view/adminhtml/layout/cms_block_index.xml @@ -9,6 +9,11 @@ <body> <referenceContainer name="content"> <uiComponent name="cms_block_listing"/> + <block class="Magento\Backend\Block\Template" template="Magento_Cms::url_filter_applier.phtml" name="block_list_url_filter_applier"> + <arguments> + <argument name="listing_namespace" xsi:type="string">cms_block_listing</argument> + </arguments> + </block> </referenceContainer> <referenceContainer name="admin.scope.col.wrap" htmlClass="admin__old" /> <!-- ToDo UI: remove this wrapper with old styles removal. The class name "admin__old" is for tests only, we shouldn't use it in any way --> </body> diff --git a/app/code/Magento/Cms/view/adminhtml/layout/cms_page_index.xml b/app/code/Magento/Cms/view/adminhtml/layout/cms_page_index.xml index bf78b1cd49448..1256751e65742 100644 --- a/app/code/Magento/Cms/view/adminhtml/layout/cms_page_index.xml +++ b/app/code/Magento/Cms/view/adminhtml/layout/cms_page_index.xml @@ -10,6 +10,11 @@ <body> <referenceContainer name="content"> <uiComponent name="cms_page_listing"/> + <block class="Magento\Backend\Block\Template" template="Magento_Cms::url_filter_applier.phtml" name="page_list_url_filter_applier"> + <arguments> + <argument name="listing_namespace" xsi:type="string">cms_page_listing</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml index 099efa0abdb88..d1c204c01ad1c 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml @@ -5,17 +5,21 @@ */ /** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $filters = $block->getConfig()->getFilters() ?? []; $allowedExtensions = []; $blockHtmlId = $block->getHtmlId(); +$listExtensions = [[]]; foreach ($filters as $media_type) { - $allowedExtensions = array_merge($allowedExtensions, array_map(function ($fileExt) { + $listExtensions[] = array_map(function ($fileExt) { return ltrim($fileExt, '.*'); - }, $media_type['files'])); + }, $media_type['files']); } +$allowedExtensions = array_merge(...$listExtensions); + $resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() ? "{action: 'resize', maxWidth: " . $block->escapeHtml($block->getImageUploadMaxWidth()) @@ -28,7 +32,9 @@ $resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() <div id="<?= /* @noEscape */ $blockHtmlId ?>" class="uploader"> <span class="fileinput-button form-buttons"> <span><?= $block->escapeHtml(__('Upload Images')) ?></span> - <input class="fileupload" type="file" name="<?= $block->escapeHtmlAttr($block->getConfig()->getFileField()) ?>" data-url="<?= $block->escapeUrl($block->getConfig()->getUrl()) ?>" multiple> + <input class="fileupload" type="file" + name="<?= $block->escapeHtmlAttr($block->getConfig()->getFileField()) ?>" + data-url="<?= $block->escapeUrl($block->getConfig()->getUrl()) ?>" multiple> </span> <div class="clear"></div> <script type="text/x-magento-template" id="<?= /* @noEscape */ $blockHtmlId ?>-template"> @@ -40,7 +46,11 @@ $resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() <div class="clear"></div> </div> </script> - <script> + <?php $intMaxSize = $block->getFileSizeService()->getMaxFileSize(); + $resizeConfig = /* @noEscape */ $resizeConfig; + $blockHtmlId = /* @noEscape */ $blockHtmlId; + $scriptString = <<<script + require([ 'jquery', 'mage/template', @@ -50,10 +60,10 @@ require([ 'domReady!', 'mage/translate' ], function ($, mageTemplate, validator, uiAlert) { - var maxFileSize = <?= $block->escapeJs($block->getFileSizeService()->getMaxFileSize()) ?>, - allowedExtensions = '<?= $block->escapeHtml(implode(' ', $allowedExtensions)) ?>'; + var maxFileSize = {$block->escapeJs($block->getFileSizeService()->getMaxFileSize())}, + allowedExtensions = '{$block->escapeJs(implode(' ', $allowedExtensions))}'; - $('#<?= /* @noEscape */ $blockHtmlId ?> .fileupload').fileupload({ + $('#{$blockHtmlId} .fileupload').fileupload({ dataType: 'json', formData: { isAjax: 'true', @@ -63,9 +73,9 @@ require([ acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, allowedExtensions: allowedExtensions, maxFileSize: maxFileSize, - dropZone: $('#<?= /* @noEscape */ $blockHtmlId ?>').closest('[role="dialog"]'), + dropZone: $('#{$blockHtmlId}').closest('[role="dialog"]'), add: function (e, data) { - var progressTmpl = mageTemplate('#<?= /* @noEscape */ $blockHtmlId ?>-template'), + var progressTmpl = mageTemplate('#{$blockHtmlId}-template'), fileSize, tmpl, validationResult; @@ -109,7 +119,7 @@ require([ } }); - $(tmpl).data('image', data).appendTo('#<?= /* @noEscape */ $blockHtmlId ?>'); + $(tmpl).data('image', data).appendTo('#{$blockHtmlId}'); return true; }); @@ -146,17 +156,20 @@ require([ } }); - $('#<?= /* @noEscape */ $blockHtmlId ?> .fileupload').fileupload('option', { + $('#{$blockHtmlId} .fileupload').fileupload('option', { process: [{ action: 'load', fileTypes: /^image\/(gif|jpeg|png)$/, - maxFileSize: <?= (int) $block->getFileSizeService()->getMaxFileSize() ?> * 10 + maxFileSize: {$intMaxSize} * 10 }, - <?= /* @noEscape */ $resizeConfig ?>, + {$resizeConfig}, { action: 'save' }] }); }); -</script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml index 9603bb4f1a412..4c8d1ca7157e9 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml @@ -4,16 +4,34 @@ * See COPYING.txt for license details. */ - /** @var \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Tree $block */ +/** @var \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Tree $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> + +<?php +/** Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <div class="tree-panel" > <div class="categories-side-col"> <div class="tree-actions"> - <a onclick="jQuery('[data-role=tree]').jstree('close_all');"><?= $block->escapeHtml(__('Collapse All')) ?></a> + <a id="collapseAll"><?= $block->escapeHtml(__('Collapse All')) ?></a> <span class="separator">|</span> - <a onclick="jQuery('[data-role=tree]').jstree('open_all');"><?= $block->escapeHtml(__('Expand All')) ?></a> + <a id="expandAll"><?= $block->escapeHtml(__('Expand All')) ?></a> </div> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "jQuery('[data-role=tree]').jstree('close_all');", + '#div.tree-actions a#collapseAll' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "jQuery('[data-role=tree]').jstree('open_all');", + '#div.tree-actions a#expandAll' + ) ?> </div> - <div data-role="tree" data-mage-init='<?= $block->escapeHtml($this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getTreeWidgetOptions())) ?>'> + <div data-role="tree" data-mage-init='<?= $block->escapeHtml( + $jsonHelper->jsonEncode($block->getTreeWidgetOptions()) + ) ?>'> </div> </div> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/url_filter_applier.phtml b/app/code/Magento/Cms/view/adminhtml/templates/url_filter_applier.phtml new file mode 100644 index 0000000000000..a4918e86715a8 --- /dev/null +++ b/app/code/Magento/Cms/view/adminhtml/templates/url_filter_applier.phtml @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var $block \Magento\Backend\Block\Template */ +/** @var \Magento\Framework\Escaper $escaper */ +?> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Ui/js/grid/url-filter-applier": { + "listingNamespace": "<?= $escaper->escapeJs($block->getListingNamespace()) ?>" + } + } + } +</script> diff --git a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php index e56225cbe2548..15e62d00cd9f9 100644 --- a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php +++ b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php @@ -7,6 +7,7 @@ namespace Magento\CmsUrlRewrite\Plugin\Cms\Model\Store; +use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\PageRepositoryInterface; use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -21,6 +22,8 @@ */ class View { + private const ALL_STORE_VIEWS = '0'; + /** * @var UrlPersistInterface */ @@ -67,16 +70,18 @@ public function __construct( * @param ResourceStore $object * @param ResourceStore $result * @param ResourceStore $store - * @return void + * @return ResourceStore * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function afterSave(ResourceStore $object, ResourceStore $result, AbstractModel $store): void + public function afterSave(ResourceStore $object, ResourceStore $result, AbstractModel $store): ResourceStore { - if ($store->isObjectNew() || $store->dataHasChangedFor('group_id')) { + if ($store->isObjectNew()) { $this->urlPersist->replace( $this->generateCmsPagesUrls((int)$store->getId()) ); } + + return $result; } /** @@ -89,9 +94,8 @@ private function generateCmsPagesUrls(int $storeId): array { $rewrites = []; $urls = []; - $searchCriteria = $this->searchCriteriaBuilder->create(); - $cmsPagesCollection = $this->pageRepository->getList($searchCriteria)->getItems(); - foreach ($cmsPagesCollection as $page) { + + foreach ($this->getCmsPageItems() as $page) { $page->setStoreId($storeId); $rewrites[] = $this->cmsPageUrlRewriteGenerator->generate($page); } @@ -99,4 +103,18 @@ private function generateCmsPagesUrls(int $storeId): array return $urls; } + + /** + * Return cms page items for all store view + * + * @return PageInterface[] + */ + private function getCmsPageItems(): array + { + $searchCriteria = $this->searchCriteriaBuilder->addFilter('store_id', self::ALL_STORE_VIEWS) + ->create(); + $list = $this->pageRepository->getList($searchCriteria); + + return $list->getItems(); + } } diff --git a/app/code/Magento/CmsUrlRewrite/Test/Unit/Plugin/Cms/Model/Store/ViewTest.php b/app/code/Magento/CmsUrlRewrite/Test/Unit/Plugin/Cms/Model/Store/ViewTest.php new file mode 100644 index 0000000000000..7a0e17015dc3c --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/Test/Unit/Plugin/Cms/Model/Store/ViewTest.php @@ -0,0 +1,179 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CmsUrlRewrite\Test\Unit\Plugin\Cms\Model\Store; + +use Magento\Cms\Api\Data\PageSearchResultsInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page; +use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; +use Magento\CmsUrlRewrite\Plugin\Cms\Model\Store\View; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ResourceModel\Store; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\CmsUrlRewrite\Plugin\Cms\Model\Store\View. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ViewTest extends TestCase +{ + private const STUB_STORE_ID = 777; + private const STUB_URL_REWRITE = ['cms/page/view']; + + /** + * @var View + */ + private $model; + + /** + * @var SearchCriteria|MockObject + */ + private $searchCriteriaMock; + + /** + * @var PageSearchResultsInterface|MockObject + */ + private $pageSearchResultMock; + + /** + * @var Page|MockObject + */ + private $pageMock; + + /** + * @var Store|MockObject + */ + private $storeObjectMock; + + /** + * @var AbstractModel|MockObject + */ + private $abstractModelMock; + + /** + * @var UrlPersistInterface|MockObject + */ + private $urlPersistMock; + + /** + * @var PageRepositoryInterface|MockObject + */ + private $pageRepositoryMock; + + /** + * @var CmsPageUrlRewriteGenerator|MockObject + */ + private $cmsPageUrlGeneratorMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->storeObjectMock = $this->createMock(Store::class); + $this->searchCriteriaMock = $this->createMock(SearchCriteria::class); + $this->pageSearchResultMock = $this->createMock(PageSearchResultsInterface::class); + + $this->pageMock = $this->getMockBuilder(Page::class) + ->disableOriginalConstructor() + ->addMethods(['setStoreId']) + ->getMock(); + + $this->abstractModelMock = $this->getMockBuilder(AbstractModel::class) + ->onlyMethods(['isObjectNew', 'getId']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->urlPersistMock = $this->createMock(UrlPersistInterface::class); + $this->pageRepositoryMock = $this->createMock(PageRepositoryInterface::class); + $this->cmsPageUrlGeneratorMock = $this->createMock(CmsPageUrlRewriteGenerator::class); + + $searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + $searchCriteriaBuilderMock->expects($this->any()) + ->method('addFilter') + ->willReturnSelf(); + $searchCriteriaBuilderMock->expects($this->any()) + ->method('create') + ->willReturn($this->searchCriteriaMock); + + $this->model = $objectManager->getObject( + View::class, + [ + 'urlPersist' => $this->urlPersistMock, + 'searchCriteriaBuilder' => $searchCriteriaBuilderMock, + 'pageRepository' => $this->pageRepositoryMock, + 'cmsPageUrlRewriteGenerator' => $this->cmsPageUrlGeneratorMock, + ] + ); + } + + /** + * After save when object is not new + * + * @return void + */ + public function testAfterSaveObjectIsNotNew(): void + { + $storeResult = clone $this->storeObjectMock; + + $this->abstractModelMock->expects($this->once()) + ->method('isObjectNew') + ->willReturn(false); + + $this->urlPersistMock->expects($this->never()) + ->method('replace'); + + $result = $this->model->afterSave($this->storeObjectMock, $storeResult, $this->abstractModelMock); + $this->assertEquals($storeResult, $result); + } + + /** + * After save when object is new + * + * @return void + */ + public function testAfterSaveObjectIsNew(): void + { + $storeResult = clone $this->storeObjectMock; + + $this->abstractModelMock->expects($this->once()) + ->method('isObjectNew') + ->willReturn(true); + $this->abstractModelMock->expects($this->once()) + ->method('getId') + ->willReturn(self::STUB_STORE_ID); + $this->pageRepositoryMock->expects($this->once()) + ->method('getList') + ->with($this->searchCriteriaMock) + ->willReturn($this->pageSearchResultMock); + $this->pageSearchResultMock->expects($this->once()) + ->method('getItems') + ->willReturn([$this->pageMock]); + $this->pageMock->expects($this->once()) + ->method('setStoreId') + ->with(self::STUB_STORE_ID); + $this->cmsPageUrlGeneratorMock->expects($this->once()) + ->method('generate') + ->with($this->pageMock) + ->willReturn(self::STUB_URL_REWRITE); + $this->urlPersistMock->expects($this->once()) + ->method('replace') + ->with(self::STUB_URL_REWRITE); + + $result = $this->model->afterSave($this->storeObjectMock, $storeResult, $this->abstractModelMock); + $this->assertEquals($storeResult, $result); + } +} diff --git a/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php b/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php index d2b87b1ae2841..10f9af9268ae6 100644 --- a/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php @@ -15,7 +15,7 @@ * Class for retrieving configurations from environment variables. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class EnvironmentConfigSource implements ConfigSourceInterface { @@ -47,7 +47,7 @@ public function __construct( /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function get($path = '') { diff --git a/app/code/Magento/Config/App/Config/Source/InitialSnapshotConfigSource.php b/app/code/Magento/Config/App/Config/Source/InitialSnapshotConfigSource.php index 40978320797d4..a22639ec51641 100644 --- a/app/code/Magento/Config/App/Config/Source/InitialSnapshotConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/InitialSnapshotConfigSource.php @@ -12,7 +12,7 @@ /** * The source with previously imported configuration. * @api - * @since 100.2.0 + * @since 101.0.0 */ class InitialSnapshotConfigSource implements ConfigSourceInterface { @@ -45,7 +45,7 @@ public function __construct(FlagManager $flagManager, DataObjectFactory $dataObj * Snapshots are stored in flags. * * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function get($path = '') { diff --git a/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php b/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php index 7926708772a9f..641db6d035ca5 100644 --- a/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php @@ -70,7 +70,8 @@ public function __construct( public function get($path = '') { $data = new DataObject($this->deploymentConfig->isDbAvailable() ? $this->loadConfig() : []); - return $data->getData($path) ?: []; + + return $data->getData($path) !== null ? $data->getData($path) : null; } /** diff --git a/app/code/Magento/Config/Block/System/Config/Edit.php b/app/code/Magento/Config/Block/System/Config/Edit.php index ba27cb33b20f0..7955f28f59f4e 100644 --- a/app/code/Magento/Config/Block/System/Config/Edit.php +++ b/app/code/Magento/Config/Block/System/Config/Edit.php @@ -121,6 +121,7 @@ public function getSaveUrl() /** * @return string + * @since 101.1.0 */ public function getConfigSearchParamsJson() { diff --git a/app/code/Magento/Config/Block/System/Config/Form.php b/app/code/Magento/Config/Block/System/Config/Form.php index 8378c058c1955..8e07ef8b45203 100644 --- a/app/code/Magento/Config/Block/System/Config/Form.php +++ b/app/code/Magento/Config/Block/System/Config/Form.php @@ -838,10 +838,10 @@ private function getAppConfigDataValue($path) * Gets instance of ElementVisibilityInterface. * * @return ElementVisibilityInterface - * @deprecated 100.2.0 Added to not break backward compatibility of the constructor signature + * @deprecated 101.0.0 Added to not break backward compatibility of the constructor signature * by injecting the new dependency directly. * The method can be removed in a future major release, when constructor signature can be changed. - * @since 100.2.0 + * @since 101.0.0 */ public function getElementVisibility() { diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field.php b/app/code/Magento/Config/Block/System/Config/Form/Field.php index ac4a85b7d3bc6..9801467a4cf61 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field.php @@ -6,6 +6,10 @@ namespace Magento\Config\Block\System\Config\Form; +use Magento\Framework\App\ObjectManager; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Render field html element in Stores Configuration * @@ -17,6 +21,25 @@ class Field extends \Magento\Backend\Block\Template implements \Magento\Framework\Data\Form\Element\Renderer\RendererInterface { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Context $context, + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Retrieve element HTML markup * @@ -108,7 +131,12 @@ protected function _renderInheritCheckbox(\Magento\Framework\Data\Form\Element\A '[inherit]" type="checkbox" value="1"' . ' class="checkbox config-inherit" ' . $checkedHtml . $disabled . - ' onclick="toggleValueElements(this, Element.previous(this.parentNode))" /> '; + ' />'; + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, Element.previous(this.parentNode))", + 'input#' . $htmlId . '_inherit' + ); $html .= '<label for="' . $htmlId . '_inherit" class="inherit">' . $this->_getInheritCheckboxLabel( $element ) . '</label>'; @@ -174,7 +202,12 @@ protected function _renderHint(\Magento\Framework\Data\Form\Element\AbstractElem { $html = '<td class="">'; if ($element->getHint()) { - $html .= '<div class="hint"><div style="display: none;">' . $element->getHint() . '</div></div>'; + $html .= '<div class="hint"><div id="hint_' . $element->getHtmlId() . '">' . + $element->getHint() . '</div></div>'; + $html .= /* @noEscape */ $this->secureRenderer->renderStyleAsTag( + "display: none;", + 'div#hint_' . $element->getHtmlId() + ); } $html .= '</td>'; return $html; diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php b/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php index 3bb4632ee4517..cc6b7e4b441dc 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php @@ -282,7 +282,7 @@ public function getColumns() /** * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getAddButtonLabel() { diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php b/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php index b62584537e2b3..9a0bc416d4d46 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php @@ -11,8 +11,40 @@ */ namespace Magento\Config\Block\System\Config\Form\Field\Select; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Allowspecific extends \Magento\Framework\Data\Form\Element\Select { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * Allowspecific constructor. + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param Escaper $escaper + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + Escaper $escaper, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer); + $this->secureRenderer = $secureRenderer; + } + /** * Add additional Javascript code * @@ -25,7 +57,6 @@ public function getAfterElementHtml() $useDefaultElementId = $countryListId . '_inherit'; $elementJavaScript = <<<HTML -<script type="text/javascript"> //<![CDATA[ document.getElementById('{$elementId}').addEventListener('change', function(event) { var isCountrySpecific = event.target.value == 1, @@ -42,13 +73,15 @@ public function getAfterElementHtml() } }); //]]> -</script> HTML; - return $elementJavaScript . parent::getAfterElementHtml(); + return $this->secureRenderer->renderTag('script', [], $elementJavaScript, false) . + parent::getAfterElementHtml(); } /** + * Return generated html. + * * @return string */ public function getHtml() @@ -61,6 +94,8 @@ public function getHtml() } /** + * Return country specific element id. + * * @return string */ protected function _getSpecificCountryElementId() diff --git a/app/code/Magento/Config/Block/System/Config/Form/Fieldset.php b/app/code/Magento/Config/Block/System/Config/Form/Fieldset.php index 05c98a3eba99d..0e918c23857ac 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Fieldset.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Fieldset.php @@ -9,7 +9,9 @@ */ namespace Magento\Config\Block\System\Config\Form; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * @api @@ -35,21 +37,29 @@ class Fieldset extends \Magento\Backend\Block\AbstractBlock implements */ protected $isCollapsedDefault = false; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Backend\Model\Auth\Session $authSession * @param \Magento\Framework\View\Helper\Js $jsHelper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Backend\Model\Auth\Session $authSession, \Magento\Framework\View\Helper\Js $jsHelper, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsHelper = $jsHelper; $this->_authSession = $authSession; parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -71,6 +81,8 @@ public function render(AbstractElement $element) } /** + * Return children elements html. + * * @param AbstractElement $element * @return string * @since 100.1.0 @@ -84,6 +96,8 @@ protected function _getChildrenElementsHtml(AbstractElement $element) . '<td colspan="4">' . $field->toHtml() . '</td></tr>'; } else { $elements .= $field->toHtml(); + $styleTag = $this->addVisibilityTag($field); + $elements .= $styleTag; } } @@ -156,16 +170,20 @@ protected function _getFrontendClass($element) */ protected function _getHeaderTitleHtml($element) { + $styleTag = $this->addVisibilityTag($element); return '<a id="' . $element->getHtmlId() . '-head" href="#' . $element->getHtmlId() . - '-link" onclick="Fieldset.toggleCollapse(\'' . - $element->getHtmlId() . - '\', \'' . - $this->getUrl( - '*/*/state' - ) . '\'); return false;">' . $element->getLegend() . '</a>'; + '-link">' . $element->getLegend() . '</a>' . + $styleTag . + /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault();' . + "Fieldset.toggleCollapse('" . $element->getHtmlId() . "', '" . + $this->_urlBuilder->getUrl('*/*/state') . "'); return false;", + 'a#' . $element->getHtmlId() . '-head' + ); } /** @@ -194,6 +212,7 @@ protected function _getFieldsetCss() /** * Return footer html for fieldset + * * Add extra tooltip comments to elements * * @param AbstractElement $element @@ -205,10 +224,14 @@ protected function _getFooterHtml($element) foreach ($element->getElements() as $field) { if ($field->getTooltip()) { $html .= sprintf( - '<div id="row_%s_comment" class="system-tooltip-box" style="display:none;">%s</div>', + '<div id="row_%s_comment" class="system-tooltip-box">%s</div>', $field->getId(), $field->getTooltip() ); + $html .= $this->secureRenderer->renderStyleAsTag( + 'display:none;', + '#row_' . $field->getId() . '_comment' + ); } } $html .= '</fieldset>' . $this->_getExtraJs($element); @@ -233,6 +256,7 @@ protected function _getExtraJs($element) { $htmlId = $element->getHtmlId(); $output = "require(['prototype'], function(){Fieldset.applyCollapse('{$htmlId}');});"; + return $this->_jsHelper->getScript($output); } @@ -250,10 +274,70 @@ protected function _isCollapseState($element) return true; } + if ($this->isCollapseStateByDependentField($element)) { + return false; + } + $extra = $this->_authSession->getUser()->getExtra(); + if (isset($extra['configState'][$element->getId()])) { return $extra['configState'][$element->getId()]; } return $this->isCollapsedDefault; } + + /** + * Check if element should be collapsed by dependent field value. + * + * @param AbstractElement $element + * @return bool + */ + private function isCollapseStateByDependentField(AbstractElement $element): bool + { + if (!empty($element->getGroup()['depends']['fields'])) { + foreach ($element->getGroup()['depends']['fields'] as $dependFieldData) { + if (is_array($dependFieldData) && isset($dependFieldData['value'], $dependFieldData['id'])) { + $fieldSetForm = $this->getForm(); + $dependentFieldConfigValue = $this->_scopeConfig->getValue( + $dependFieldData['id'], + $fieldSetForm->getScope(), + $fieldSetForm->getScopeCode() + ); + + if ($dependFieldData['value'] !== $dependentFieldConfigValue) { + return true; + } + } + } + } + + return false; + } + + /** + * If element or it's parent depends on other element we hide it during page load. + * + * @param AbstractElement $field + * @return string + */ + private function addVisibilityTag(AbstractElement $field): string + { + $elementId = ''; + $styleTag = ''; + + if (!empty($field->getFieldConfig()['depends']['fields'])) { + $elementId = '#row_' . $field->getHtmlId(); + } elseif (!empty($field->getGroup()['depends']['fields'])) { + $elementId = '#' . $field->getHtmlId() . '-head'; + } + + if (!empty($elementId)) { + $styleTag .= $this->secureRenderer->renderStyleAsTag( + 'display: none;', + $elementId + ); + } + + return $styleTag; + } } diff --git a/app/code/Magento/Config/Block/System/Config/Form/Fieldset/Modules/DisableOutput.php b/app/code/Magento/Config/Block/System/Config/Form/Fieldset/Modules/DisableOutput.php index 99fa7b5addee8..4c2e6873d9593 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Fieldset/Modules/DisableOutput.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Fieldset/Modules/DisableOutput.php @@ -10,7 +10,7 @@ * on the store settings page. * * @method \Magento\Config\Block\System\Config\Form getForm() - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. However, this functionality should * not be used in future development. Module design should explicitly state dependencies to avoid requiring output * disabling. This functionality will temporarily be kept in Magento core, as there are unresolved modularity @@ -22,25 +22,25 @@ class DisableOutput extends \Magento\Config\Block\System\Config\Form\Fieldset { /** * @var \Magento\Framework\DataObject - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_dummyElement; /** * @var \Magento\Config\Block\System\Config\Form\Field - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_fieldRenderer; /** * @var array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_values; /** * @var \Magento\Framework\Module\ModuleListInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_moduleList; @@ -64,7 +64,7 @@ public function __construct( /** * {@inheritdoc} - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. However, this functionality should * not be used in future development. Module design should explicitly state dependencies to avoid requiring output * disabling. This functionality will temporarily be kept in Magento core, as there are unresolved modularity @@ -97,7 +97,7 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele } /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return \Magento\Framework\DataObject */ protected function _getDummyElement() @@ -109,7 +109,7 @@ protected function _getDummyElement() } /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return \Magento\Config\Block\System\Config\Form\Field */ protected function _getFieldRenderer() @@ -123,7 +123,7 @@ protected function _getFieldRenderer() } /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return array */ protected function _getValues() @@ -140,7 +140,7 @@ protected function _getValues() /** * @param \Magento\Framework\Data\Form\Element\Fieldset $fieldset * @param string $moduleName - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return mixed */ protected function _getFieldHtml($fieldset, $moduleName) diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php b/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php index 1b287573a9285..e95797658bb33 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php @@ -17,7 +17,7 @@ * @see ConfigSetCommand * * @api - * @since 100.2.0 + * @since 101.0.0 */ class ConfigSetProcessorFactory { @@ -68,7 +68,7 @@ public function __construct( * @return ConfigSetProcessorInterface New processor instance * @throws ConfigurationMismatchException If processor type is not exists in processors array * or declared class has wrong implementation - * @since 100.2.0 + * @since 101.0.0 */ public function create($processorName) { diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorInterface.php b/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorInterface.php index 01aa03b188e62..0abebb604d36f 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorInterface.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorInterface.php @@ -14,7 +14,7 @@ * @see ConfigSetCommand * * @api - * @since 100.2.0 + * @since 101.0.0 */ interface ConfigSetProcessorInterface { @@ -27,7 +27,7 @@ interface ConfigSetProcessorInterface * @param string $scopeCode The scope code * @return void * @throws CouldNotSaveException An exception on processing error - * @since 100.2.0 + * @since 101.0.0 */ public function process($path, $value, $scope, $scopeCode); } diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php index c622a48b7f2c8..d49d65774d0d2 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php @@ -21,7 +21,7 @@ * * @inheritdoc * @api - * @since 100.2.0 + * @since 101.0.0 */ class DefaultProcessor implements ConfigSetProcessorInterface { @@ -76,7 +76,7 @@ public function __construct( * Requires installed application. * * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function process($path, $value, $scope, $scopeCode) { diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php b/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php index fcd7c0d5335b1..aa33c96c7e0e2 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php @@ -24,7 +24,7 @@ * @see ConfigSetCommand * * @api - * @since 100.2.0 + * @since 101.0.0 */ class ProcessorFacade { @@ -99,8 +99,8 @@ public function __construct( * @param boolean $lock The lock flag * @return string Processor response message * @throws ValidatorException If some validation is wrong - * @since 100.2.0 - * @deprecated + * @since 101.0.0 + * @deprecated 101.0.4 * @see processWithLockTarget() */ public function process($path, $value, $scope, $scopeCode, $lock) @@ -119,6 +119,7 @@ public function process($path, $value, $scope, $scopeCode, $lock) * @param string $lockTarget * @return string Processor response message * @throws ValidatorException If some validation is wrong + * @since 101.0.4 */ public function processWithLockTarget( $path, diff --git a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php index 999d8e41af5bc..f278a07cc6806 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php +++ b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php @@ -24,7 +24,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @since 100.2.0 + * @since 101.0.0 */ class ConfigSetCommand extends Command { @@ -86,7 +86,7 @@ public function __construct( /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ protected function configure() { @@ -141,7 +141,7 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @since 100.2.0 + * @since 101.0.0 * @return int|null */ protected function execute(InputInterface $input, OutputInterface $output) diff --git a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php index aeb57010e4969..2465eecec71dc 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php @@ -19,7 +19,7 @@ * Class processes values using backend model which declared in system.xml. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class ValueProcessor { @@ -83,7 +83,7 @@ public function __construct( * @param string $value The value to process * @param string $path The configuration path for getting backend model. E.g. scope_id/group_id/field_id * @return string processed value result - * @since 100.2.0 + * @since 101.0.0 */ public function process($scope, $scopeCode, $value, $path) { diff --git a/app/code/Magento/Config/Console/Command/ConfigShowCommand.php b/app/code/Magento/Config/Console/Command/ConfigShowCommand.php index 2d3dabdb24e67..53c7445f9508a 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShowCommand.php +++ b/app/code/Magento/Config/Console/Command/ConfigShowCommand.php @@ -16,12 +16,15 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Config\Model\Config\PathValidatorFactory; /** * Command provides possibility to show saved system configuration. * * @api - * @since 100.2.0 + * @since 101.0.0 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ConfigShowCommand extends Command { @@ -78,29 +81,47 @@ class ConfigShowCommand extends Command */ private $inputPath; + /** + * @var PathValidatorFactory + */ + private $pathValidatorFactory; + + /** + * @var EmulatedAdminhtmlAreaProcessor + */ + private $emulatedAreaProcessor; + /** * @param ValidatorInterface $scopeValidator * @param ConfigSourceInterface $configSource * @param ConfigPathResolver $pathResolver * @param ValueProcessor $valueProcessor + * @param PathValidatorFactory|null $pathValidatorFactory + * @param EmulatedAdminhtmlAreaProcessor|null $emulatedAreaProcessor * @internal param ScopeConfigInterface $appConfig */ public function __construct( ValidatorInterface $scopeValidator, ConfigSourceInterface $configSource, ConfigPathResolver $pathResolver, - ValueProcessor $valueProcessor + ValueProcessor $valueProcessor, + ?PathValidatorFactory $pathValidatorFactory = null, + ?EmulatedAdminhtmlAreaProcessor $emulatedAreaProcessor = null ) { parent::__construct(); $this->scopeValidator = $scopeValidator; $this->configSource = $configSource; $this->pathResolver = $pathResolver; $this->valueProcessor = $valueProcessor; + $this->pathValidatorFactory = $pathValidatorFactory + ?: ObjectManager::getInstance()->get(PathValidatorFactory::class); + $this->emulatedAreaProcessor = $emulatedAreaProcessor + ?: ObjectManager::getInstance()->get(EmulatedAdminhtmlAreaProcessor::class); } /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ protected function configure() { @@ -136,8 +157,8 @@ protected function configure() * Shows error message if configuration for given path doesn't exist * or scope/scope-code doesn't pass validation. * - * {@inheritdoc} - * @since 100.2.0 + * @inheritdoc + * @since 101.0.0 */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -146,17 +167,17 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->scopeCode = $input->getOption(self::INPUT_OPTION_SCOPE_CODE); $this->inputPath = trim($input->getArgument(self::INPUT_ARGUMENT_PATH), '/'); - $this->scopeValidator->isValid($this->scope, $this->scopeCode); - $configPath = $this->pathResolver->resolve($this->inputPath, $this->scope, $this->scopeCode); - $configValue = $this->configSource->get($configPath); + $configValue = $this->emulatedAreaProcessor->process(function () { + $this->scopeValidator->isValid($this->scope, $this->scopeCode); + if ($this->inputPath) { + $pathValidator = $this->pathValidatorFactory->create(); + $pathValidator->validate($this->inputPath); + } - if (empty($configValue)) { - $output->writeln(sprintf( - '<error>%s</error>', - __('Configuration for path: "%1" doesn\'t exist', $this->inputPath)->render() - )); - return Cli::RETURN_FAILURE; - } + $configPath = $this->pathResolver->resolve($this->inputPath, $this->scope, $this->scopeCode); + + return $this->configSource->get($configPath); + }); $this->outputResult($output, $configValue, $this->inputPath); return Cli::RETURN_SUCCESS; diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php b/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php index 274ee3b8d2a6e..c644ec8eb15cc 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php @@ -31,7 +31,7 @@ abstract class AbstractConfig extends \Magento\Backend\App\AbstractAction protected $_configStructure; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_sectionChecker; diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/ConfigSectionChecker.php b/app/code/Magento/Config/Controller/Adminhtml/System/ConfigSectionChecker.php index 0af19b83a5a3f..b656498e97dba 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/ConfigSectionChecker.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/ConfigSectionChecker.php @@ -9,7 +9,7 @@ use Magento\Framework\Exception\NotFoundException; /** - * @deprecated 100.2.0 - unused class. + * @deprecated 101.0.0 - unused class. * @see \Magento\Config\Model\Config\Structure\Element\Section::isAllowed() */ class ConfigSectionChecker diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index 356c6ca17da18..f61e99529c3cc 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -208,6 +208,7 @@ public function save() ); $groupChangedPaths = $this->getChangedPaths($sectionId, $groupId, $groupData, $oldConfig, $extraOldGroups); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $changedPaths = \array_merge($changedPaths, $groupChangedPaths); } @@ -370,6 +371,7 @@ private function getChangedPaths( $oldConfig, $extraOldGroups ); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $changedPaths = \array_merge($changedPaths, $subGroupChangedPaths); } } @@ -435,11 +437,11 @@ protected function _processGroup( if (!isset($fieldData['value'])) { $fieldData['value'] = null; } - + if ($field->getType() == 'multiline' && is_array($fieldData['value'])) { $fieldData['value'] = trim(implode(PHP_EOL, $fieldData['value'])); } - + $data = [ 'field' => $fieldId, 'groups' => $groups, @@ -453,7 +455,7 @@ protected function _processGroup( $backendModel->addData($data); $this->_checkSingleStoreMode($field, $backendModel); - $path = $this->getFieldPath($field, $fieldId, $extraOldGroups, $oldConfig); + $path = $this->getFieldPath($field, $fieldId, $oldConfig, $extraOldGroups); $backendModel->setPath($path)->setValue($fieldData['value']); $inherit = !empty($fieldData['inherit']); diff --git a/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php b/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php index 7bbbafe826422..e6acd431be3d5 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php +++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php @@ -13,7 +13,7 @@ use Magento\Framework\App\ObjectManager; /** - * @deprecated 100.2.0 robots.txt file is no longer stored in filesystem. It generates as response on request. + * @deprecated 100.1.7 robots.txt file is no longer stored in filesystem. It generates as response on request. */ class Robots extends \Magento\Framework\App\Config\Value { diff --git a/app/code/Magento/Config/Model/Config/Backend/Baseurl.php b/app/code/Magento/Config/Model/Config/Backend/Baseurl.php index a218d2b0d07e6..5e43e53f1b64f 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Baseurl.php +++ b/app/code/Magento/Config/Model/Config/Backend/Baseurl.php @@ -231,7 +231,7 @@ public function afterSave() /** * Get URL Validator * - * @deprecated 100.2.0 + * @deprecated 100.1.12 * @return UrlValidator */ private function getUrlValidator() diff --git a/app/code/Magento/Config/Model/Config/Backend/Currency/Allow.php b/app/code/Magento/Config/Model/Config/Backend/Currency/Allow.php index 7ff1d367c5e58..b0db20e2fb25a 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Currency/Allow.php +++ b/app/code/Magento/Config/Model/Config/Backend/Currency/Allow.php @@ -81,7 +81,7 @@ public function afterSave() /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ protected function _getAllowedCurrencies() { diff --git a/app/code/Magento/Config/Model/Config/Export/ExcludeList.php b/app/code/Magento/Config/Model/Config/Export/ExcludeList.php index e556c42f66a99..e7efb5ac50b79 100644 --- a/app/code/Magento/Config/Model/Config/Export/ExcludeList.php +++ b/app/code/Magento/Config/Model/Config/Export/ExcludeList.php @@ -8,7 +8,7 @@ /** * Class ExcludeList contains list of config fields which should be excluded from config export file. * - * @deprecated 100.2.0 because in Magento since version 2.2.0 there are several + * @deprecated 101.0.0 because in Magento since version 2.2.0 there are several * types for configuration fields that require special processing. * @see \Magento\Config\Model\Config\TypePool */ @@ -32,7 +32,7 @@ public function __construct(array $configs = []) * * @param string $path * @return bool - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function isPresent($path) { @@ -43,7 +43,7 @@ public function isPresent($path) * Retrieves all excluded field paths for export * * @return array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function get() { diff --git a/app/code/Magento/Config/Model/Config/Importer.php b/app/code/Magento/Config/Model/Config/Importer.php index a54af2ead5048..a870b5f2403c3 100644 --- a/app/code/Magento/Config/Model/Config/Importer.php +++ b/app/code/Magento/Config/Model/Config/Importer.php @@ -23,7 +23,7 @@ * {@inheritdoc} * @see \Magento\Deploy\Console\Command\App\ConfigImport\Importer * @api - * @since 100.2.0 + * @since 101.0.0 */ class Importer implements ImporterInterface { @@ -103,7 +103,7 @@ public function __construct( * or current value is different from previously imported. * * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function import(array $data) { @@ -145,7 +145,7 @@ public function import(array $data) /** * @inheritdoc * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @since 100.2.0 + * @since 101.0.0 */ public function getWarningMessages(array $data) { diff --git a/app/code/Magento/Config/Model/Config/Parser/Comment.php b/app/code/Magento/Config/Model/Config/Parser/Comment.php index b46b2308f8df5..4a749ba030d80 100644 --- a/app/code/Magento/Config/Model/Config/Parser/Comment.php +++ b/app/code/Magento/Config/Model/Config/Parser/Comment.php @@ -19,7 +19,7 @@ * It is used to parse config paths from * comment section in provided configuration file. * @api - * @since 100.2.0 + * @since 101.0.0 */ class Comment implements CommentParserInterface { @@ -84,7 +84,7 @@ public function __construct( * @param string $fileName the basename of file * @return array * @throws FileSystemException - * @since 100.2.0 + * @since 101.0.0 */ public function execute($fileName) { diff --git a/app/code/Magento/Config/Model/Config/PathValidator.php b/app/code/Magento/Config/Model/Config/PathValidator.php index 68363bef69d91..bc4f863b7b05f 100644 --- a/app/code/Magento/Config/Model/Config/PathValidator.php +++ b/app/code/Magento/Config/Model/Config/PathValidator.php @@ -11,7 +11,7 @@ /** * Validates the config path by config structure schema. * @api - * @since 100.2.0 + * @since 101.0.0 */ class PathValidator { @@ -36,7 +36,7 @@ public function __construct(Structure $structure) * @param string $path The config path * @return true The result of validation * @throws ValidatorException If provided path is not valid - * @since 100.2.0 + * @since 101.0.0 */ public function validate($path) { diff --git a/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php b/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php index 5f8dc3f7ab4a7..bf59c729790a7 100644 --- a/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php +++ b/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php @@ -14,7 +14,7 @@ * Class DocumentRoot * @package Magento\Config\Model\Config\Reader\Source\Deployed * @api - * @since 100.2.0 + * @since 101.0.0 */ class DocumentRoot { @@ -37,7 +37,7 @@ public function __construct(DeploymentConfig $config) * deployment configuration. * * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getPath() { @@ -50,7 +50,7 @@ public function getPath() * likely be extended to control other areas). * * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function isPub() { diff --git a/app/code/Magento/Config/Model/Config/Source/Email/Template.php b/app/code/Magento/Config/Model/Config/Source/Email/Template.php index ac168f16ca182..182faa53e5288 100644 --- a/app/code/Magento/Config/Model/Config/Source/Email/Template.php +++ b/app/code/Magento/Config/Model/Config/Source/Email/Template.php @@ -60,10 +60,12 @@ public function toOptionArray() $this->_coreRegistry->register('config_system_email_template', $collection); } $options = $collection->toOptionArray(); - $templateId = str_replace('/', '_', $this->getPath()); - $templateLabel = $this->_emailConfig->getTemplateLabel($templateId); - $templateLabel = __('%1 (Default)', $templateLabel); - array_unshift($options, ['value' => $templateId, 'label' => $templateLabel]); + if (!empty($this->getPath())) { + $templateId = str_replace('/', '_', $this->getPath()); + $templateLabel = $this->_emailConfig->getTemplateLabel($templateId); + $templateLabel = __('%1 (Default)', $templateLabel); + array_unshift($options, ['value' => $templateId, 'label' => $templateLabel]); + } return $options; } } diff --git a/app/code/Magento/Config/Model/Config/Structure.php b/app/code/Magento/Config/Model/Config/Structure.php index a16920f0dc527..437aca04ec577 100644 --- a/app/code/Magento/Config/Model/Config/Structure.php +++ b/app/code/Magento/Config/Model/Config/Structure.php @@ -185,7 +185,7 @@ public function getElement($path) * * @param string $path The configuration path * @return \Magento\Config\Model\Config\Structure\ElementInterface|null - * @since 100.2.0 + * @since 101.0.0 */ public function getElementByConfigPath($path) { @@ -369,7 +369,7 @@ protected function _getGroupFieldPathsByAttribute(array $fields, $parentPath, $a * ``` * * @return array An array of config path to config structure path map - * @since 100.2.0 + * @since 100.1.12 */ public function getFieldPaths() { diff --git a/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php b/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php index c4a0cb5e886d9..8ce5c7f5f13d2 100644 --- a/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php +++ b/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php @@ -225,10 +225,10 @@ public function getPath($fieldPrefix = '') * Get instance of ElementVisibilityInterface. * * @return ElementVisibilityInterface - * @deprecated 100.2.0 Added to not break backward compatibility of the constructor signature + * @deprecated 101.0.0 Added to not break backward compatibility of the constructor signature * by injecting the new dependency directly. * The method can be removed in a future major release, when constructor signature can be changed. - * @since 100.2.0 + * @since 101.0.0 */ public function getElementVisibility() { diff --git a/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php b/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php index 252042a41cc1d..568adceda9430 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php +++ b/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php @@ -11,8 +11,9 @@ * Defines status of visibility of form elements on Stores > Settings > Configuration page * in Admin Panel in Production mode. * @api - * @deprecated class location was changed + * @deprecated 101.0.6 class location was changed * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction + * @since 101.0.0 */ class ConcealInProductionConfigList implements ElementVisibilityInterface { @@ -55,7 +56,8 @@ public function __construct(State $state, array $configs = []) /** * @inheritdoc - * @deprecated + * @deprecated 101.0.6 + * @since 101.0.0 */ public function isHidden($path) { @@ -68,7 +70,8 @@ public function isHidden($path) /** * @inheritdoc - * @deprecated + * @deprecated 101.0.6 + * @since 101.0.0 */ public function isDisabled($path) { diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementInterface.php b/app/code/Magento/Config/Model/Config/Structure/ElementInterface.php index 5ba6221601725..b29887219a258 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ElementInterface.php +++ b/app/code/Magento/Config/Model/Config/Structure/ElementInterface.php @@ -10,7 +10,7 @@ /** * @api * @since 100.0.2 - * @deprecated + * @deprecated 101.1.0 * @see StructureElementInterface */ interface ElementInterface diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php index ec57d629e61da..c5a0b6127f122 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php @@ -16,6 +16,7 @@ * Defines status of visibility of form elements on Stores > Settings > Configuration page * in Admin Panel in Production mode. * @api + * @since 101.0.6 */ class ConcealInProduction implements ElementVisibilityInterface { @@ -80,7 +81,7 @@ public function __construct(State $state, array $configs = [], array $exemptions /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.6 */ public function isHidden($path) { @@ -105,7 +106,7 @@ public function isHidden($path) /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.6 */ public function isDisabled($path) { diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php index 29148a244dcc6..ee6be789232e8 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php @@ -18,6 +18,7 @@ * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction * * @api + * @since 101.0.6 */ class ConcealInProductionWithoutScdOnDemand implements ElementVisibilityInterface { @@ -50,6 +51,7 @@ public function __construct( /** * @inheritdoc + * @since 101.0.6 */ public function isHidden($path): bool { @@ -61,6 +63,7 @@ public function isHidden($path): bool /** * @inheritdoc + * @since 101.0.6 */ public function isDisabled($path): bool { diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityComposite.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityComposite.php index 23074297e6323..e5b60c596d713 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityComposite.php +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityComposite.php @@ -11,7 +11,7 @@ * Contains list of classes which implement ElementVisibilityInterface for * checking of visibility of form elements on Stores > Settings > Configuration page in Admin Panel. * @api - * @since 100.2.0 + * @since 101.0.0 */ class ElementVisibilityComposite implements ElementVisibilityInterface { @@ -49,7 +49,7 @@ public function __construct(array $visibility = []) /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function isHidden($path) { @@ -64,7 +64,7 @@ public function isHidden($path) /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function isDisabled($path) { diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityInterface.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityInterface.php index 21dff52843765..a11a549eebc35 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityInterface.php +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityInterface.php @@ -9,7 +9,7 @@ * Checks visibility status of form elements on Stores > Settings > Configuration page in Admin Panel * by their paths in the system.xml structure. * @api - * @since 100.2.0 + * @since 101.0.0 */ interface ElementVisibilityInterface { @@ -25,7 +25,7 @@ interface ElementVisibilityInterface * * @param string $path The path of form element in the system.xml structure * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function isDisabled($path); @@ -34,7 +34,7 @@ public function isDisabled($path); * * @param string $path The path of form element in the system.xml structure * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function isHidden($path); } diff --git a/app/code/Magento/Config/Model/Config/StructureElementInterface.php b/app/code/Magento/Config/Model/Config/StructureElementInterface.php index 946d6e3c766a4..e1e855d37325b 100644 --- a/app/code/Magento/Config/Model/Config/StructureElementInterface.php +++ b/app/code/Magento/Config/Model/Config/StructureElementInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 101.1.0 */ interface StructureElementInterface extends Structure\ElementInterface { @@ -15,6 +16,7 @@ interface StructureElementInterface extends Structure\ElementInterface * * @param string $fieldPrefix * @return string + * @since 101.1.0 */ public function getPath($fieldPrefix = ''); } diff --git a/app/code/Magento/Config/Model/Config/TypePool.php b/app/code/Magento/Config/Model/Config/TypePool.php index e41ff8e88a595..9080db81d1f55 100644 --- a/app/code/Magento/Config/Model/Config/TypePool.php +++ b/app/code/Magento/Config/Model/Config/TypePool.php @@ -13,7 +13,7 @@ * Used when you need to know if the configuration path belongs to a certain type. * Participates in the mechanism for creating the configuration dump file. * @api - * @since 100.2.0 + * @since 101.0.0 */ class TypePool { @@ -52,7 +52,7 @@ class TypePool * Checks if the configuration path is contained in exclude list. * * @var ExcludeList - * @deprecated 100.2.0 We use it only to support backward compatibility. If some configurations + * @deprecated 101.0.0 We use it only to support backward compatibility. If some configurations * were set to this list before, we need to read them. * It will be supported for next 2 minor releases or until a major release. * TypePool should be used to mark configurations with types. @@ -83,7 +83,7 @@ public function __construct(array $sensitive = [], array $environment = [], Excl * @param string $path Configuration field path. For example, 'contact/email/recipient_email' * @param string $type Type of configuration fields * @return bool True when the path belongs to requested type, false otherwise - * @since 100.2.0 + * @since 101.0.0 */ public function isPresent($path, $type) { diff --git a/app/code/Magento/Config/Model/PreparedValueFactory.php b/app/code/Magento/Config/Model/PreparedValueFactory.php index 19d607ad3dc1a..c86c0a820e86c 100644 --- a/app/code/Magento/Config/Model/PreparedValueFactory.php +++ b/app/code/Magento/Config/Model/PreparedValueFactory.php @@ -21,7 +21,7 @@ * * @see ValueInterface * @api - * @since 100.2.0 + * @since 101.0.0 */ class PreparedValueFactory { @@ -92,7 +92,7 @@ public function __construct( * @return ValueInterface * @throws RuntimeException If Value can not be created * @see ValueInterface - * @since 100.2.0 + * @since 101.0.0 */ public function create($path, $value, $scope, $scopeCode = null) { diff --git a/app/code/Magento/Config/Setup/Patch/Data/UnsetTinymce3.php b/app/code/Magento/Config/Setup/Patch/Data/UnsetTinymce3.php new file mode 100644 index 0000000000000..115b3baeded8d --- /dev/null +++ b/app/code/Magento/Config/Setup/Patch/Data/UnsetTinymce3.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Config\Setup\Patch\Data; + +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; + +/** + * Update config to Tinymce4 if Tinymce3 adapter is used. + */ +class UnsetTinymce3 implements DataPatchInterface, PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * CreateDefaultPages constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup + ) { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + try { + $connection = $this->moduleDataSetup->getConnection(); + $table = $this->moduleDataSetup->getTable('core_config_data'); + $select = $connection + ->select() + ->from( + $table, + ['value'] + ) + ->where('path = ?', 'cms/wysiwyg/editor'); + + if (strpos($connection->fetchOne($select), 'Tinymce3/tinymce3Adapter') !== false) { + $row = [ + 'value' => 'mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter' + ]; + $where = $connection->quoteInto( + 'path = ?', + 'cms/wysiwyg/editor' + ); + $connection->update( + $table, + $row, + $where + ); + } + return $this; + } catch (\Exception $e) { + return $this; + } + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.3.6'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCollapseStorefrontTabActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCollapseStorefrontTabActionGroup.xml new file mode 100644 index 0000000000000..1b6148f64ce4c --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCollapseStorefrontTabActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminConfigCollapseStorefrontTabActionGroup"> + <conditionalClick selector="{{CatalogSection.storefront}}" dependentSelector="{{CatalogSection.CheckIfTabIsExpanded}}" visible="true" stepKey="collapseStorefrontTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigExpandStorefrontTabActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigExpandStorefrontTabActionGroup.xml new file mode 100644 index 0000000000000..e2afb3b56cd8d --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigExpandStorefrontTabActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminConfigExpandStorefrontTabActionGroup"> + <conditionalClick selector="{{CatalogSection.storefront}}" dependentSelector="{{CatalogSection.CheckIfTabExpand}}" visible="true" stepKey="expandStorefrontTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigFillInputFieldActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigFillInputFieldActionGroup.xml new file mode 100644 index 0000000000000..cce439185089a --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigFillInputFieldActionGroup.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="AdminConfigFillInputFilterFieldActionGroup"> + <arguments> + <argument name="selector"/> + <argument name="value" type="string"/> + </arguments> + <fillField selector="{{selector}}" userInput="{{value}}" stepKey="fillInputField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigCatalogPageActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigCatalogPageActionGroup.xml new file mode 100644 index 0000000000000..fe1eb1baff4c8 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigCatalogPageActionGroup.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="AdminOpenStoreConfigCatalogPageActionGroup"> + <annotations> + <description>Go to admin store configuration catalog page.</description> + </annotations> + + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="openAdminStoreConfigPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitchToTinyMCE3ActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitchToTinyMCE3ActionGroup.xml index b725610b7b2ee..837402005fecf 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitchToTinyMCE3ActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitchToTinyMCE3ActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="SwitchToTinyMCE3ActionGroup"> + <actionGroup name="SwitchToTinyMCE3ActionGroup" deprecated="This version of TinyMCE is no longer supported"> <annotations> <description>Goes to the 'Configuration' page for 'Content Management'. Sets 'WYSIWYG Editor' to 'TinyMCE 3'. Clicks on the Save button. PLEASE NOTE: The value is Hardcoded.</description> </annotations> 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/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml index e82ad4670f9b3..9b54697e2d6ba 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml @@ -7,9 +7,9 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCatalogSearchConfigurationSection"> - <element name="catalogSearchTab" type="button" selector="#catalog_search-head"/> + <element name="catalogSearchTab" type="button" selector="a#catalog_search-head"/> <element name="checkIfCatalogSearchTabExpand" type="button" selector="#catalog_search-head:not(.open)"/> <element name="searchEngineDefaultSystemValue" type="checkbox" selector="#catalog_search_engine_inherit"/> <element name="searchEngine" type="select" selector="#catalog_search_engine"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml index 851157c5d03c0..72675414576cf 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml @@ -10,6 +10,7 @@ <section name="CatalogSection"> <element name="storefront" type="select" selector="#catalog_frontend-head"/> <element name="CheckIfTabExpand" type="button" selector="#catalog_frontend-head:not(.open)"/> + <element name="CheckIfTabIsExpanded" type="button" selector="#catalog_frontend-head.open"/> <element name="price" type="button" selector="#catalog_price-head"/> <element name="checkIfPriceExpand" type="button" selector="//a[@id='catalog_price-head' and @class='open']"/> <element name="catalogPriceScope" type="select" selector="#catalog_price_scope"/> @@ -23,5 +24,6 @@ <element name="CheckIfSeoTabExpand" type="button" selector="#catalog_seo-head:not(.open)"/> <element name="GenerateUrlRewrites" type="select" selector="#catalog_seo_generate_category_product_rewrites"/> <element name="successMessage" type="text" selector="#messages"/> + <element name="productsPerPageOnGridAllowedValues" type="input" selector="//input[@id='catalog_frontend_grid_per_page_values']"/> </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/Test/AdminConfigCollapsedFieldsetValidationTest.xml b/app/code/Magento/Config/Test/Mftf/Test/AdminConfigCollapsedFieldsetValidationTest.xml new file mode 100644 index 0000000000000..877676e2a7cda --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Test/AdminConfigCollapsedFieldsetValidationTest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminConfigCollapsedFieldsetValidationTest"> + <annotations> + <features value="Backend"/> + <stories value="Configuration Form Validation"/> + <title value="Verify that form validation triggered on element inside hidden fieldset opens the fieldset in case of error"/> + <description value="Verify that form validation triggered on element inside hidden fieldset opens the fieldset in case of error"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-35785"/> + <group value="configuration"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenStoreConfigCatalogPageActionGroup" stepKey="navigateToConfigurationPage"/> + <actionGroup ref="AdminConfigExpandStorefrontTabActionGroup" stepKey="expandStorefrontTab"/> + <actionGroup ref="AdminUncheckUseSystemValueActionGroup" stepKey="uncheckUseSystemValue"> + <argument name="rowId" value="row_catalog_frontend_grid_per_page_values"/> + </actionGroup> + <actionGroup ref="AdminConfigFillInputFilterFieldActionGroup" stepKey="fillInputField"> + <argument name="selector" value="CatalogSection.productsPerPageOnGridAllowedValues"/> + <argument name="value" value=""/> + </actionGroup> + <actionGroup ref="AdminConfigCollapseStorefrontTabActionGroup" stepKey="collapseStorefrontTab"/> + <click selector="{{CatalogSection.save}}" stepKey="clickSaveConfigBtn"/> + + <actionGroup ref="AssertAdminValidationErrorActionGroup" stepKey="assertValidationError"> + <argument name="inputId" value="catalog_frontend_grid_per_page_values"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml index 5327979154389..d0edd4cf1cb64 100644 --- a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml +++ b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml @@ -22,7 +22,9 @@ <createData entity="EnableAdminAccountAllowCountry" stepKey="setAllowedCountries"/> </before> <after> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> @@ -33,7 +35,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Flush Magento Cache--> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Create a customer account from Storefront--> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountForm"> diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php index 4ab882a33f9af..a16208c0e61b0 100644 --- a/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php +++ b/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php @@ -17,118 +17,140 @@ use Magento\Framework\App\Config\Value; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\DB\Adapter\TableNotFoundException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** * Test Class for retrieving runtime configuration from database. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RuntimeConfigSourceTest extends TestCase { + /** + * @var RuntimeConfigSource + */ + private $model; + /** * @var CollectionFactory|MockObject */ - private $collectionFactory; + private $collectionFactoryMock; /** * @var ScopeCodeResolver|MockObject */ - private $scopeCodeResolver; + private $scopeCodeResolverMock; /** * @var Converter|MockObject */ - private $converter; + private $converterMock; /** * @var Value|MockObject */ - private $configItem; + private $configItemMock; /** * @var Value|MockObject */ - private $configItemTwo; + private $configItemMockTwo; - /** - * @var RuntimeConfigSource - */ - private $configSource; /** * @var DeploymentConfig|MockObject */ - private $deploymentConfig; + private $deploymentConfigMock; + /** + * @inheritdoc + */ protected function setUp(): void { - $this->collectionFactory = $this->getMockBuilder(CollectionFactory::class) + $objectManager = new ObjectManager($this); + + $this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) ->disableOriginalConstructor() - ->setMethods(['create']) ->getMock(); - $this->scopeCodeResolver = $this->getMockBuilder(ScopeCodeResolver::class) + $this->scopeCodeResolverMock = $this->getMockBuilder(ScopeCodeResolver::class) ->disableOriginalConstructor() ->getMock(); - $this->converter = $this->getMockBuilder(Converter::class) + $this->converterMock = $this->getMockBuilder(Converter::class) ->disableOriginalConstructor() ->getMock(); - $this->configItem = $this->getMockBuilder(Value::class) + $this->configItemMock = $this->getMockBuilder(Value::class) ->disableOriginalConstructor() - ->setMethods(['getScope', 'getPath', 'getValue']) + ->addMethods(['getScope', 'getPath', 'getValue']) ->getMock(); - $this->configItemTwo = $this->getMockBuilder(Value::class) + $this->configItemMockTwo = $this->getMockBuilder(Value::class) ->disableOriginalConstructor() - ->setMethods(['getScope', 'getPath', 'getValue', 'getScopeId']) + ->addMethods(['getScope', 'getPath', 'getValue', 'getScopeId']) ->getMock(); - $this->deploymentConfig = $this->createPartialMock(DeploymentConfig::class, ['isDbAvailable']); - $this->configSource = new RuntimeConfigSource( - $this->collectionFactory, - $this->scopeCodeResolver, - $this->converter, - $this->deploymentConfig + $this->deploymentConfigMock = $this->createPartialMock( + DeploymentConfig::class, + ['isDbAvailable'] + ); + $this->model = $objectManager->getObject( + RuntimeConfigSource::class, + [ + 'collectionFactory' => $this->collectionFactoryMock, + 'scopeCodeResolver' => $this->scopeCodeResolverMock, + 'converter' => $this->converterMock, + 'deploymentConfig' => $this->deploymentConfigMock, + ] ); } - public function testGet() + /** + * Test get initial data. + * + * @return void + */ + public function testGet(): void { - $this->deploymentConfig->method('isDbAvailable') + $this->deploymentConfigMock->expects($this->once()) + ->method('isDbAvailable') ->willReturn(true); $collection = $this->createPartialMock(Collection::class, ['load', 'getIterator']); - $collection->method('load') + $collection->expects($this->once()) + ->method('load') ->willReturn($collection); - $collection->method('getIterator') - ->willReturn(new ArrayIterator([$this->configItem, $this->configItemTwo])); + $collection->expects($this->once()) + ->method('getIterator') + ->willReturn(new ArrayIterator([$this->configItemMock, $this->configItemMockTwo])); $scope = 'websites'; $scopeCode = 'myWebsites'; - $this->collectionFactory->expects($this->once()) + $this->collectionFactoryMock->expects($this->once()) ->method('create') ->willReturn($collection); - $this->configItem->expects($this->exactly(2)) + $this->configItemMock->expects($this->exactly(2)) ->method('getScope') ->willReturn(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); - $this->configItem->expects($this->once()) + $this->configItemMock->expects($this->once()) ->method('getPath') ->willReturn('dev/test/setting'); - $this->configItem->expects($this->once()) + $this->configItemMock->expects($this->once()) ->method('getValue') ->willReturn(true); - $this->configItemTwo->expects($this->exactly(3)) + $this->configItemMockTwo->expects($this->exactly(3)) ->method('getScope') ->willReturn($scope); - $this->configItemTwo->expects($this->once()) + $this->configItemMockTwo->expects($this->once()) ->method('getScopeId') ->willReturn($scopeCode); - $this->configItemTwo->expects($this->once()) + $this->configItemMockTwo->expects($this->once()) ->method('getPath') ->willReturn('dev/test/setting2'); - $this->configItemTwo->expects($this->once()) + $this->configItemMockTwo->expects($this->once()) ->method('getValue') ->willReturn(false); - $this->scopeCodeResolver->expects($this->once()) + $this->scopeCodeResolverMock->expects($this->once()) ->method('resolve') ->with($scope, $scopeCode) ->willReturnArgument(1); - $this->converter->expects($this->exactly(2)) + $this->converterMock->expects($this->exactly(2)) ->method('convert') ->withConsecutive( [['dev/test/setting' => true]], @@ -150,25 +172,97 @@ public function testGet() ] ] ], - $this->configSource->get() + $this->model->get() ); } - public function testGetWhenDbIsNotAvailable() + /** + * Test get with not available db + * + * @return void + */ + public function testGetWhenDbIsNotAvailable(): void { - $this->deploymentConfig->method('isDbAvailable')->willReturn(false); - $this->assertEquals([], $this->configSource->get()); + $this->deploymentConfigMock->expects($this->once()) + ->method('isDbAvailable') + ->willReturn(false); + $this->assertEquals([], $this->model->get()); } - public function testGetWhenDbIsEmpty() + /** + * Test get with empty db + * + * @return void + */ + public function testGetWhenDbIsEmpty(): void { - $this->deploymentConfig->method('isDbAvailable') + $this->deploymentConfigMock->expects($this->once()) + ->method('isDbAvailable') ->willReturn(true); $collection = $this->createPartialMock(Collection::class, ['load']); - $collection->method('load') + $collection->expects($this->once()) + ->method('load') ->willThrowException($this->createMock(TableNotFoundException::class)); - $this->collectionFactory->method('create') + $this->collectionFactoryMock->expects($this->once()) + ->method('create') ->willReturn($collection); - $this->assertEquals([], $this->configSource->get()); + + $this->assertEquals([], $this->model->get()); + } + + /** + * Test get value for specified config + * + * @dataProvider configDataProvider + * + * @param string $path + * @param array $configData + * @param string $expectedResult + * @return void + */ + public function testGetConfigValue(string $path, array $configData, string $expectedResult): void + { + $this->deploymentConfigMock->expects($this->once()) + ->method('isDbAvailable') + ->willReturn(true); + + $collection = $this->createPartialMock(Collection::class, ['load', 'getIterator']); + $collection->expects($this->once()) + ->method('load') + ->willReturn($collection); + $collection->expects($this->once()) + ->method('getIterator') + ->willReturn(new ArrayIterator([$this->configItemMock])); + + $this->collectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($collection); + + $this->configItemMock->expects($this->exactly(2)) + ->method('getScope') + ->willReturn(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + $this->configItemMock->expects($this->once()) + ->method('getPath') + ->willReturn($path); + + $this->converterMock->expects($this->once()) + ->method('convert') + ->willReturn($configData); + + $this->assertEquals($expectedResult, $this->model->get($path)); + } + + /** + * DataProvider for testGetConfigValue + * + * @return array + */ + public function configDataProvider(): array + { + return [ + 'config value 0' => ['default/test/option', ['test' => ['option' => 0]], '0'], + 'config value blank' => ['default/test/option', ['test' => ['option' => '']], ''], + 'config value null' => ['default/test/option', ['test' => ['option' => null]], ''], + ]; } } diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php index 822779a5736a8..120e83a70ffc4 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php @@ -17,6 +17,8 @@ use Magento\Framework\Url; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class ImageTest extends TestCase { @@ -39,11 +41,22 @@ protected function setUp(): void { $objectManager = new ObjectManager($this); $this->urlBuilderMock = $this->createMock(Url::class); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('some-rando-string'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $listener, string $selector): string { + return "<script>document.querySelector('{$selector}').{$event} = () => { {$listener} };</script>"; + } + ); $this->image = $objectManager->getObject( Image::class, [ 'urlBuilder' => $this->urlBuilderMock, - '_escaper' => $objectManager->getObject(Escaper::class) + '_escaper' => $objectManager->getObject(Escaper::class), + 'random' => $randomMock, + 'secureRenderer' => $secureRendererMock ] ); @@ -108,14 +121,15 @@ public function testGetElementHtmlWithValue() $this->assertStringContainsString('type="file"', $html); $this->assertStringContainsString('value="test_value"', $html); $this->assertStringContainsString( - '<a href="' + '<a previewlinkid="linkIdsome-rando-string" href="' . $url . $this->testData['path'] . '/' . $this->testData['value'] - . '" onclick="imagePreview(\'' . $expectedHtmlId . '_image\'); return false;"', + . '"', $html ); + $this->assertStringContainsString("imagePreview('{$expectedHtmlId}_image');\nreturn false;", $html); $this->assertStringContainsString('<input type="checkbox"', $html); } } diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php index 04b0bad314b11..129dfc902963f 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php @@ -14,6 +14,9 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +use Magento\Framework\DataObject; class AllowspecificTest extends TestCase { @@ -30,10 +33,30 @@ class AllowspecificTest extends TestCase protected function setUp(): void { $testHelper = new ObjectManager($this); + + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('some-rando-string'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $listener, string $selector): string { + return "<script>document.querySelector('{$selector}').{$event} = () => { {$listener} };</script>"; + } + ); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); $this->_object = $testHelper->getObject( Allowspecific::class, [ - '_escaper' => $testHelper->getObject(Escaper::class) + '_escaper' => $testHelper->getObject(Escaper::class), + 'random' => $randomMock, + 'secureRenderer' => $secureRendererMock ] ); $this->_object->setData('html_id', 'spec_element'); @@ -68,7 +91,7 @@ public function testGetAfterElementHtml() $actual = $this->_object->getAfterElementHtml(); $this->assertStringEndsWith('</script>' . $afterHtmlCode, $actual); - $this->assertStringStartsWith('<script type="text/javascript">', trim($actual)); + $this->assertStringStartsWith('<script >', trim($actual)); $this->assertStringContainsString('test_prefix_spec_element_test_suffix', $actual); } diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldTest.php index 679b240cf13ab..3193d4f737984 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldTest.php @@ -14,6 +14,7 @@ use Magento\Store\Model\StoreManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Test how class render field html element in Stores Configuration @@ -48,10 +49,24 @@ class FieldTest extends TestCase protected function setUp(): void { $this->_storeManagerMock = $this->createMock(StoreManager::class); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); $data = [ 'storeManager' => $this->_storeManagerMock, 'urlBuilder' => $this->createMock(Url::class), + 'secureRenderer' => $secureRendererMock ]; $helper = new ObjectManager($this); $this->_object = $helper->getObject(Field::class, $data); @@ -157,7 +172,7 @@ public function testRenderHint() { $testHint = 'test_hint'; $this->_elementMock->expects($this->any())->method('getHint')->willReturn($testHint); - $expected = '<td class=""><div class="hint"><div style="display: none;">' . $testHint . '</div></div>'; + $expected = '<td class=""><div class="hint"><div id="hint_test_field_id">' . $testHint . '</div></div>'; $actual = $this->_object->render($this->_elementMock); $this->assertStringContainsString($expected, $actual); } @@ -194,8 +209,9 @@ public function testRenderInheritCheckbox() '_inherit" name="' . $this->_testData['name'] . '[inherit]" type="checkbox" value="1"' . - ' class="checkbox config-inherit" checked="checked"' . ' disabled="disabled"' . ' readonly="1"' . - ' onclick="toggleValueElements(this, Element.previous(this.parentNode))" /> '; + ' class="checkbox config-inherit" checked="checked"' . ' disabled="disabled"' . ' readonly="1" />' . + '<script>document.querySelector(\'input#test_field_id_inherit\').onclick = function () '. + '{ toggleValueElements(this, Element.previous(this.parentNode)) };</script>'; $expected .= '<label for="' . $this->_testData['htmlId'] . '_inherit" class="inherit">Use Website</label>'; $actual = $this->_object->render($this->_elementMock); diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php index 87e42953ddd49..df028ea27f01c 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php @@ -23,6 +23,7 @@ use Magento\User\Model\User; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -160,6 +161,14 @@ protected function setUp(): void ] ); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $data = [ 'context' => $context, 'authSession' => $this->authSessionMock, @@ -169,6 +178,7 @@ protected function setUp(): void 'group' => $groupMock, 'form' => $formMock, ], + 'secureRenderer' => $secureRendererMock ]; $this->object = $this->objectManager->getObject( diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldsetTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldsetTest.php index 07db12a282cc9..fd5fbe7c4cdc6 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldsetTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldsetTest.php @@ -24,6 +24,7 @@ use Magento\User\Model\User; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -120,6 +121,13 @@ protected function setUp(): void $groupMock->expects($this->any())->method('getFieldsetCss')->willReturn('test_fieldset_css'); $this->_helperMock = $this->createMock(Js::class); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); $data = [ 'request' => $this->_requestMock, @@ -128,6 +136,7 @@ protected function setUp(): void 'layout' => $this->_layoutMock, 'jsHelper' => $this->_helperMock, 'data' => ['group' => $groupMock], + 'secureRenderer' => $secureRendererMock ]; $this->_testHelper = new ObjectManager($this); $this->_object = $this->_testHelper->getObject(Fieldset::class, $data); @@ -233,8 +242,8 @@ public function testRenderWithStoredElements($expanded, $nested, $extra) $this->assertStringContainsString('test_field_toHTML', $actual); - $expected = '<div id="row_test_field_id_comment" class="system-tooltip-box"' . - ' style="display:none;">test_field_tootip</div>'; + $expected = '<div id="row_test_field_id_comment" class="system-tooltip-box">test_field_tootip</div>' . + '<style>#row_test_field_id_comment { display:none; }</style>'; $this->assertStringContainsString($expected, $actual); if ($nested) { $this->assertStringContainsString('nested', $actual); diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php index 59511d9a947ab..dc3db6ab926f7 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php @@ -3,23 +3,41 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Config\Test\Unit\Console\Command; use Magento\Config\Console\Command\ConfigShow\ValueProcessor; use Magento\Config\Console\Command\ConfigShowCommand; +use Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\ConfigSourceInterface; use Magento\Framework\App\Scope\ValidatorInterface; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\LocalizedException; +use Magento\Config\Model\Config\PathValidatorFactory; +use Magento\Config\Model\Config\PathValidator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; +/** + * Test for \Magento\Config\Console\Command\ConfigShowCommand. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ConfigShowCommandTest extends TestCase { + private const CONFIG_PATH = 'some/config/path'; + private const SCOPE = 'some/config/path'; + private const SCOPE_CODE = 'someScopeCode'; + + /** + * @var ConfigShowCommand + */ + private $model; + /** * @var ValidatorInterface|MockObject */ @@ -41,12 +59,22 @@ class ConfigShowCommandTest extends TestCase private $pathResolverMock; /** - * @var ConfigShowCommand + * @var EmulatedAdminhtmlAreaProcessor|MockObject + */ + private $emulatedAreProcessorMock; + + /** + * @var PathValidator|MockObject */ - private $command; + private $pathValidatorMock; + /** + * @inheritdoc + */ protected function setUp(): void { + $objectManager = new ObjectManager($this); + $this->valueProcessorMock = $this->getMockBuilder(ValueProcessor::class) ->disableOriginalConstructor() ->getMock(); @@ -57,29 +85,49 @@ protected function setUp(): void ->getMockForAbstractClass(); $this->configSourceMock = $this->getMockBuilder(ConfigSourceInterface::class) ->getMockForAbstractClass(); + $this->pathValidatorMock = $this->getMockBuilder(PathValidator::class) + ->disableOriginalConstructor() + ->getMock(); + $pathValidatorFactoryMock = $this->getMockBuilder(PathValidatorFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $pathValidatorFactoryMock->expects($this->atMost(1)) + ->method('create') + ->willReturn($this->pathValidatorMock); - $this->command = new ConfigShowCommand( - $this->scopeValidatorMock, - $this->configSourceMock, - $this->pathResolverMock, - $this->valueProcessorMock + $this->emulatedAreProcessorMock = $this->getMockBuilder(EmulatedAdminhtmlAreaProcessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = $objectManager->getObject( + ConfigShowCommand::class, + [ + 'scopeValidator' => $this->scopeValidatorMock, + 'configSource' => $this->configSourceMock, + 'pathResolver' => $this->pathResolverMock, + 'valueProcessor' => $this->valueProcessorMock, + 'pathValidatorFactory' => $pathValidatorFactoryMock, + 'emulatedAreaProcessor' => $this->emulatedAreProcessorMock, + ] ); } - public function testExecute() + /** + * Test get config value + * + * @return void + */ + public function testExecute(): void { - $configPath = 'some/config/path'; $resolvedConfigPath = 'someScope/someScopeCode/some/config/path'; - $scope = 'someScope'; - $scopeCode = 'someScopeCode'; $this->scopeValidatorMock->expects($this->once()) ->method('isValid') - ->with($scope, $scopeCode) + ->with(self::SCOPE, self::SCOPE_CODE) ->willReturn(true); $this->pathResolverMock->expects($this->once()) ->method('resolve') - ->with($configPath, $scope, $scopeCode) + ->with(self::CONFIG_PATH, self::SCOPE, self::SCOPE_CODE) ->willReturn($resolvedConfigPath); $this->configSourceMock->expects($this->once()) ->method('get') @@ -87,10 +135,19 @@ public function testExecute() ->willReturn('someValue'); $this->valueProcessorMock->expects($this->once()) ->method('process') - ->with($scope, $scopeCode, 'someValue', $configPath) + ->with(self::SCOPE, self::SCOPE_CODE, 'someValue', self::CONFIG_PATH) ->willReturn('someProcessedValue'); - - $tester = $this->getConfigShowCommandTester($configPath, $scope, $scopeCode); + $this->emulatedAreProcessorMock->expects($this->once()) + ->method('process') + ->willReturnCallback(function ($function) { + return $function(); + }); + + $tester = $this->getConfigShowCommandTester( + self::CONFIG_PATH, + self::SCOPE, + self::SCOPE_CODE + ); $this->assertEquals( Cli::RETURN_SUCCESS, @@ -102,18 +159,28 @@ public function testExecute() ); } - public function testNotValidScopeOrScopeCode() + /** + * Test not valid scope or scope code + * + * @return void + */ + public function testNotValidScopeOrScopeCode(): void { - $configPath = 'some/config/path'; - $scope = 'someScope'; - $scopeCode = 'someScopeCode'; - $this->scopeValidatorMock->expects($this->once()) ->method('isValid') - ->with($scope, $scopeCode) + ->with(self::SCOPE, self::SCOPE_CODE) ->willThrowException(new LocalizedException(__('error message'))); - - $tester = $this->getConfigShowCommandTester($configPath, $scope, $scopeCode); + $this->emulatedAreProcessorMock->expects($this->once()) + ->method('process') + ->willReturnCallback(function ($function) { + return $function(); + }); + + $tester = $this->getConfigShowCommandTester( + self::CONFIG_PATH, + self::SCOPE, + self::SCOPE_CODE + ); $this->assertEquals( Cli::RETURN_FAILURE, @@ -125,17 +192,35 @@ public function testNotValidScopeOrScopeCode() ); } - public function testConfigPathNotExist() + /** + * Test get config value for not existed path. + * + * @return void + */ + public function testConfigPathNotExist(): void { - $configPath = 'some/path'; - $tester = $this->getConfigShowCommandTester($configPath); + $exception = new LocalizedException( + __('The "%1" path doesn\'t exist. Verify and try again.', self::CONFIG_PATH) + ); + + $this->pathValidatorMock->expects($this->once()) + ->method('validate') + ->with(self::CONFIG_PATH) + ->willThrowException($exception); + $this->emulatedAreProcessorMock->expects($this->once()) + ->method('process') + ->willReturnCallback(function ($function) { + return $function(); + }); + + $tester = $this->getConfigShowCommandTester(self::CONFIG_PATH); $this->assertEquals( Cli::RETURN_FAILURE, $tester->getStatusCode() ); $this->assertStringContainsString( - __('Configuration for path: "%1" doesn\'t exist', $configPath)->render(), + __('The "%1" path doesn\'t exist. Verify and try again.', self::CONFIG_PATH)->render(), $tester->getDisplay() ); } @@ -159,7 +244,7 @@ private function getConfigShowCommandTester($configPath, $scope = null, $scopeCo $arguments['--' . ConfigShowCommand::INPUT_OPTION_SCOPE_CODE] = $scopeCode; } - $tester = new CommandTester($this->command); + $tester = new CommandTester($this->model); $tester->execute($arguments); return $tester; diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php index 356d1133aca81..9b531280f66c6 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php @@ -102,4 +102,53 @@ public function testToOptionArray() $this->_model->setPath('template/new'); $this->assertEquals($expectedResult, $this->_model->toOptionArray()); } + + public function testToOptionArrayWithoutPath() + { + $collection = $this->createMock(Collection::class); + $collection->expects( + $this->once() + )->method( + 'toOptionArray' + )->willReturn( + [ + ['value' => 'template_one', 'label' => 'Template One'], + ['value' => 'template_two', 'label' => 'Template Two'], + ] + ); + + $this->_coreRegistry->expects( + $this->once() + )->method( + 'registry' + )->with( + 'config_system_email_template' + )->willReturn( + $collection + ); + + $this->_emailConfig->expects( + $this->never() + )->method( + 'getTemplateLabel' + )->with( + '' + ) + ->willThrowException( + new \UnexpectedValueException("Email template '' is not defined.") + ); + + $expectedResult = [ + [ + 'value' => 'template_one', + 'label' => 'Template One', + ], + [ + 'value' => 'template_two', + 'label' => 'Template Two', + ], + ]; + + $this->assertEquals($expectedResult, $this->_model->toOptionArray()); + } } diff --git a/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml b/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml index 49a75d36fd8a5..d8fb7cd412a7a 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml @@ -7,21 +7,24 @@ /** * @deprecated * @var $block \Magento\Backend\Block\Page\System\Config\Robots\Reset - * @var $jsonHelper \Magento\Framework\Json\Helper\Data + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ -$jsonHelper = $this->helper(\Magento\Framework\Json\Helper\Data::class); -?> -<script> +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +?> +<?php +$robotsDefault = /* @noEscape */ $jsonHelper->jsonEncode($block->getRobotsDefaultCustomInstructions()); +$scriptString = <<<script require([ 'jquery' ], function ($) { window.resetRobotsToDefault = function(){ - $('#design_search_engine_robots_custom_instructions').val(<?= - /* @noEscape */ $jsonHelper->jsonEncode($block->getRobotsDefaultCustomInstructions()) - ?>); + $('#design_search_engine_robots_custom_instructions').val({$robotsDefault}); } }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?= $block->getButtonHtml() ?> diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml index 7e7a540e88b2e..4dbc70efd25e3 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml @@ -3,6 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?php /** @@ -14,11 +18,13 @@ * getConfigSearchParamsJson() - string */ ?> -<style> - .highlighted { - background-color: #DFF7FF!important; - } -</style> + +<?= /* @noEscape */ $secureRenderer->renderTag( + 'style', + [], + '.highlighted { background-color: #DFF7FF!important; }' +) ?> + <form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="config-edit-form" enctype="multipart/form-data"> <?= $block->getBlockHtml('formkey') ?> @@ -26,7 +32,8 @@ <?= $block->getChildHtml('form') ?> </div> </form> -<script> + +<?php $scriptString = <<<script require([ "jquery", "uiRegistry", @@ -73,14 +80,14 @@ require([ } }, getUp: function (element, tag) { - var $element = Element.extend(element); - if (typeof $element.upTag == 'undefined') { - $element.upTag = {}; + var _element = Element.extend(element); + if (typeof _element.upTag == 'undefined') { + _element.upTag = {}; } - if (typeof $element.upTag[tag] == 'undefined') { - $element.upTag[tag] = Element.extend($element.up(tag)); + if (typeof _element.upTag[tag] == 'undefined') { + _element.upTag[tag] = Element.extend(_element.up(tag)); } - return $element.upTag[tag]; + return _element.upTag[tag]; }, getUpTd: function (element) { return this.getUp(element, 'td'); @@ -89,26 +96,26 @@ require([ return this.getUp(element, 'tr'); }, getScopeElement: function(element) { - var $element = Element.extend(element); - if (typeof $element.scopeElement == 'undefined') { + var _element = Element.extend(element); + if (typeof _element.scopeElement == 'undefined') { var scopeElementName = element.getAttribute('name').replace(/\[value\]$/, '[inherit]'); - $element.scopeElement = this.getUpTr(element).select('input[name="' + scopeElementName + '"]')[0]; - if (typeof $element.scopeElement == 'undefined') { - $element.scopeElement = false; + _element.scopeElement = this.getUpTr(element).select('input[name="' + scopeElementName + '"]')[0]; + if (typeof _element.scopeElement == 'undefined') { + _element.scopeElement = false; } } - return $element.scopeElement; + return _element.scopeElement; }, getDeleteElement: function(element) { - var $element = Element.extend(element); - if (typeof $element.deleteElement == 'undefined') { - $element.deleteElement = this.getUpTd(element) + var _element = Element.extend(element); + if (typeof _element.deleteElement == 'undefined') { + _element.deleteElement = this.getUpTd(element) .select('input[name="'+ element.getAttribute('name') + '[delete]"]')[0]; - if (typeof $element.deleteElement == 'undefined') { - $element.deleteElement = false; + if (typeof _element.deleteElement == 'undefined') { + _element.deleteElement = false; } } - return $element.deleteElement; + return _element.deleteElement; }, mapClasses: function(element, full, callback, classPrefix) { if (typeof classPrefix == 'undefined') { @@ -159,11 +166,11 @@ require([ var tagName = el.tagName.toLowerCase(); if (tagName == 'input' && el.getAttribute('type') == 'file') { - var $el = Element.extend(el); + var _el = Element.extend(el); var events = adminSystemConfig.getRegisteredEvents(el); - $el.stopObserving('change'); - var elId = $el.id; - $el.replace($el.outerHTML); + _el.stopObserving('change'); + var elId = _el.id; + _el.replace(_el.outerHTML); events.each(function(event) { Event.observe( Element.extend(document.getElementById(elId)), event.eventName, event.handler @@ -176,9 +183,9 @@ require([ Element.extend(el).click(); } } else if (tagName == 'select') { - var $el = Element.extend(el); + var _el = Element.extend(el); Element.extend(element).select('option').each(function(option) { - var relatedOption = $el.select('option[value="' + option.value + '"]')[0]; + var relatedOption = _el.select('option[value="' + option.value + '"]')[0]; if (typeof relatedOption != 'undefined') { relatedOption.selected = option.selected; } @@ -255,6 +262,24 @@ require([ } }); + window.configForm.on('invalid-form.validate', function (event, validation) { + if (validation.errorList.length === 0) { + return; + } + + jQuery.each(validation.errorList, function () { + var element = jQuery(this.element || []); + + if (element.length) { + jQuery(element.parents('.section-config')).each(function () { + if (!jQuery(this).hasClass('active')) { + Fieldset.toggleCollapse(jQuery(this).children('.config.admin__collapsible-block').attr('id')); + } + }); + } + }); + }) + $$('.shared').each(function(element){ Event.observe(element, 'change', adminSystemConfig.onchangeSharedElement); @@ -392,7 +417,8 @@ require([ handleHash(); registry.set('adminSystemConfig', adminSystemConfig); +script; +$scriptString .= 'adminSystemConfig.navigateToElement(' . /* @noEscape */ $block->getConfigSearchParamsJson() . '); +});'; - adminSystemConfig.navigateToElement(<?= /* @noEscape */ $block->getConfigSearchParamsJson(); ?>); -}); -</script> +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml index cf188bfeb6868..f08cc77249582 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php @@ -15,7 +17,7 @@ $_colspan = $block->isAddAfter() ? 2 : 1; <table class="admin__control-table" id="<?= $block->escapeHtmlAttr($block->getElement()->getId()) ?>"> <thead> <tr> - <?php foreach ($block->getColumns() as $columnName => $column) : ?> + <?php foreach ($block->getColumns() as $columnName => $column): ?> <th><?= $block->escapeHtml($column['label']) ?></th> <?php endforeach; ?> <th class="col-actions" colspan="<?= (int)$_colspan ?>"><?= $block->escapeHtml(__('Action')) ?></th> @@ -24,7 +26,10 @@ $_colspan = $block->isAddAfter() ? 2 : 1; <tfoot> <tr> <td colspan="<?= count($block->getColumns())+$_colspan ?>" class="col-actions-add"> - <button id="addToEndBtn<?= $block->escapeHtmlAttr($_htmlId) ?>" class="action-add" title="<?= $block->escapeHtmlAttr(__('Add')) ?>" type="button"> + <button id="addToEndBtn<?= $block->escapeHtmlAttr($_htmlId) ?>" + class="action-add" + title="<?= $block->escapeHtmlAttr(__('Add')) ?>" + type="button"> <span><?= $block->escapeHtml($block->getAddButtonLabel()) ?></span> </button> </td> @@ -35,34 +40,52 @@ $_colspan = $block->isAddAfter() ? 2 : 1; </div> <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) ?>[__empty]" value="" /> - <script> + <?php $scriptString = <<<script require([ 'mage/template', 'prototype' ], function (mageTemplate) { // create row creator - window.arrayRow<?= $block->escapeJs($_htmlId) ?> = { + window.arrayRow{$block->escapeJs($_htmlId)} = { // define row prototypeJS template template: mageTemplate( '<tr id="<%- _id %>">' - <?php foreach ($block->getColumns() as $columnName => $column) : ?> +script; + foreach ($block->getColumns() as $columnName => $column): + $scriptString .= <<<script + + '<td>' - + '<?= $block->escapeJs($block->renderCellTemplate($columnName)) ?>' + + '{$block->escapeJs($block->renderCellTemplate($columnName))}' + '<\/td>' - <?php endforeach; ?> +script; + endforeach; + + if ($block->isAddAfter()): + $scriptString .= <<<script - <?php if ($block->isAddAfter()) : ?> + '<td><button class="action-add" type="button" id="addAfterBtn<%- _id %>"><span>' - + '<?= $block->escapeJs($block->escapeHtml(__('Add after'))) ?>' + + '{$block->escapeJs(__('Add after'))}' + '<\/span><\/button><\/td>' - <?php endif; ?> +script; + endif; + $scriptString .= <<<script + '<td class="col-actions"><button ' - + 'onclick="arrayRow<?= $block->escapeJs($_htmlId) ?>.del(\'<%- _id %>\')" ' + 'class="action-delete" type="button">' - + '<span><?= $block->escapeJs($block->escapeHtml(__('Delete'))) ?><\/span><\/button><\/td>' + + '<span>{$block->escapeJs(__('Delete'))}<\/span><\/button><\/td>' + '<\/tr>' + +script; + $scriptString1 = /* $noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "arrayRow" . $block->escapeJs($_htmlId) . ".del('<%- _id %>')", + "tr#<%- _id %> button.action-delete" + ); + + $scriptString .= " + '" . $block->escapeJs($scriptString1) . "'" . PHP_EOL; + + $scriptString .= <<<script ), add: function(rowData, insertAfterId) { @@ -75,10 +98,16 @@ $_colspan = $block->isAddAfter() ? 2 : 1; } else { var d = new Date(); templateValues = { - <?php foreach ($block->getColumns() as $columnName => $column) : ?> - <?= $block->escapeJs($columnName) ?>: '', +script; + foreach ($block->getColumns() as $columnName => $column): + $scriptString .= <<<script + + {$block->escapeJs($columnName)}: '', 'option_extra_attrs': {}, - <?php endforeach; ?> +script; + endforeach; + $scriptString .= <<<script + _id: '_' + d.getTime() + '_' + d.getMilliseconds() }; } @@ -87,7 +116,7 @@ $_colspan = $block->isAddAfter() ? 2 : 1; if (insertAfterId) { Element.insert($(insertAfterId), {after: this.template(templateValues)}); } else { - Element.insert($('addRow<?= $block->escapeJs($_htmlId) ?>'), {bottom: this.template(templateValues)}); + Element.insert($('addRow{$block->escapeJs($_htmlId)}'), {bottom: this.template(templateValues)}); } // Fill controls with data @@ -101,9 +130,17 @@ $_colspan = $block->isAddAfter() ? 2 : 1; } // Add event for {addAfterBtn} button - <?php if ($block->isAddAfter()) : ?> + +script; + if ($block->isAddAfter()): + $scriptString .= <<<script + Event.observe('addAfterBtn' + templateValues._id, 'click', this.add.bind(this, false, templateValues._id)); - <?php endif; ?> + +script; + endif; + $scriptString .= <<<script + }, del: function(rowId) { @@ -112,24 +149,35 @@ $_colspan = $block->isAddAfter() ? 2 : 1; } // bind add action to "Add" button in last row - Event.observe('addToEndBtn<?= $block->escapeJs($_htmlId) ?>', + Event.observe('addToEndBtn{$block->escapeJs($_htmlId)}', 'click', - arrayRow<?= $block->escapeJs($_htmlId) ?>.add.bind( - arrayRow<?= $block->escapeJs($_htmlId) ?>, false, false + arrayRow{$block->escapeJs($_htmlId)}.add.bind( + arrayRow{$block->escapeJs($_htmlId)}, false, false ) ); // add existing rows - <?php - foreach ($block->getArrayRows() as $_rowId => $_row) { - echo /** @noEscape */ "arrayRow{$block->escapeJs($_htmlId)}.add(" . /** @noEscape */ $_row->toJson() . ");\n"; - } - ?> + +script; + + foreach ($block->getArrayRows() as $_rowId => $_row) { + $scriptString .= /** @noEscape */ " arrayRow" .$block->escapeJs($_htmlId) . + ".add(" . /** @noEscape */ $_row->toJson() . ");\n"; + } + $scriptString .= <<<script // Toggle the grid availability, if element is disabled (depending on scope) - <?php if ($block->getElement()->getDisabled()) : ?> - toggleValueElements({checked: true}, $('grid<?= $block->escapeJs($_htmlId) ?>').parentNode); - <?php endif; ?> +script; + if ($block->getElement()->getDisabled()): + $scriptString .= <<<script + + toggleValueElements({checked: true}, $('grid{$block->escapeJs($_htmlId)}').parentNode); +script; + endif; + $scriptString .= <<<script + }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml index 297687786833d..a0ada7814cee8 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script> + +<?php $scriptString = <<<script require([ 'prototype' ], function () { @@ -68,7 +73,10 @@ originModel.prototype = { { this.reload = false; this.loader = new varienLoader(true); - this.regionsUrl = "<?= $block->escapeJs($block->escapeUrl($block->getUrl('directory/json/countryRegion'))) ?>"; +script; + +$scriptString .= 'this.regionsUrl = "' . $block->escapeJs($block->getUrl('directory/json/countryRegion')) . '";'; +$scriptString .= <<<script this.bindCountryRegionRelation(); }, @@ -259,4 +267,7 @@ function showHint() { Event.observe(window, 'load', showHint); }); -</script> +script; +?> + +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml index 0d07051e6667d..19f7a73739400 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml @@ -3,34 +3,46 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Backend\Block\Template */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /* @var $block \Magento\Backend\Block\Template */ ?> <div class="field field-store-switcher"> <label class="label" for="store_switcher"><?= $block->escapeHtml(__('Current Configuration Scope:')) ?></label> <div class="control"> - <select id="store_switcher" class="system-config-store-switcher" - onchange="location.href=this.options[this.selectedIndex].getAttribute('url')"> - <?php foreach ($block->getStoreSelectOptions() as $_value => $_option) : ?> - <?php if (isset($_option['is_group'])) : ?> - <?php if ($_option['is_close']) : ?> + <select id="store_switcher" class="system-config-store-switcher"> + <?php foreach ($block->getStoreSelectOptions() as $_value => $_option): ?> + <?php if (isset($_option['is_group'])): ?> + <?php if ($_option['is_close']): ?> </optgroup> - <?php else : ?> - <optgroup label="<?= $block->escapeHtmlAttr($_option['label']) ?>" - style="<?= $block->escapeHtmlAttr($_option['style']) ?>"> + <?php else: ?> + <optgroup label="<?= $block->escapeHtmlAttr($_option['label']) ?>"> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $_option['style'], + "optgroup[label='" . $block->escapeJs($_option['label']) . "']" + ) ?> <?php endif; ?> <?php continue ?> <?php endif; ?> <option value="<?= $block->escapeHtmlAttr($_value) ?>" url="<?= $block->escapeUrl($_option['url']) ?>" - <?= $_option['selected'] ? 'selected="selected"' : '' ?> - style="<?= $block->escapeHtmlAttr($_option['style']) ?>"> + <?= $_option['selected'] ? 'selected="selected"' : '' ?>> <?= $block->escapeHtml($_option['label']) ?> </option> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $_option['style'], + "optgroup[url='" . $block->escapeJs($_option['url']) . "']" + ) ?> <?php endforeach ?> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + "location.href=this.options[this.selectedIndex].getAttribute('url')", + '#store_switcher' + ) ?> </div> <?= $block->getHintHtml() ?> - <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::store')) : ?> + <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::store')): ?> <div class="actions"> <a href="<?= $block->escapeUrl($block->getUrl('*/system_store')) ?>"> <?= $block->escapeHtml(__('Stores')) ?> diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/Configurable.php index 11e75839ec33c..1718a460d7544 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/Configurable.php @@ -7,12 +7,68 @@ */ namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset; +use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\ConfigurableProduct\Model\ConfigurableAttributeData; +use Magento\Customer\Helper\Session\CurrentCustomer; +use Magento\Customer\Model\Session; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data; +use Magento\Framework\Locale\Format; +use Magento\Framework\Pricing\PriceCurrencyInterface; + /** * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Configurable extends \Magento\ConfigurableProduct\Block\Product\View\Type\Configurable { + /** + * @param \Magento\Catalog\Block\Product\Context $context + * @param \Magento\Framework\Stdlib\ArrayUtils $arrayUtils + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param \Magento\ConfigurableProduct\Helper\Data $helper + * @param \Magento\Catalog\Helper\Product $catalogProduct + * @param CurrentCustomer $currentCustomer + * @param PriceCurrencyInterface $priceCurrency + * @param ConfigurableAttributeData $configurableAttributeData + * @param array $data + * @param Format|null $localeFormat + * @param Session|null $customerSession + * @param \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices|null $variationPrices + */ + public function __construct( + \Magento\Catalog\Block\Product\Context $context, + \Magento\Framework\Stdlib\ArrayUtils $arrayUtils, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + \Magento\ConfigurableProduct\Helper\Data $helper, + \Magento\Catalog\Helper\Product $catalogProduct, + CurrentCustomer $currentCustomer, + PriceCurrencyInterface $priceCurrency, + ConfigurableAttributeData $configurableAttributeData, + array $data = [], + Format $localeFormat = null, + Session $customerSession = null, + \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices $variationPrices = null + ) { + $data['productHelper'] = $catalogProduct; + parent::__construct( + $context, + $arrayUtils, + $jsonEncoder, + $helper, + $catalogProduct, + $currentCustomer, + $priceCurrency, + $configurableAttributeData, + $data, + $localeFormat, + $customerSession, + $variationPrices + ); + } + /** * Retrieve product * diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/Bulk.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/Bulk.php index bb5c8d8b49ca2..b400ef5f97efb 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/Bulk.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/Bulk.php @@ -5,13 +5,16 @@ */ namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps; +use Magento\Backend\Helper\Js; use Magento\Catalog\Helper\Image; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Media\Config; use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\ProductFactory; use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\Json\Helper\Data as JsonHelper; /** * Adminhtml block for fieldset of configurable product @@ -41,21 +44,26 @@ class Bulk extends \Magento\Ui\Block\Component\StepsWizard\StepAbstract * @param Image $image * @param Config $catalogProductMediaConfig * @param ProductFactory $productFactory + * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( Context $context, Image $image, Config $catalogProductMediaConfig, - ProductFactory $productFactory + ProductFactory $productFactory, + array $data = [], + JsonHelper $jsonHelper = null ) { - parent::__construct($context); + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); + parent::__construct($context, $data); $this->image = $image; $this->productFactory = $productFactory; $this->catalogProductMediaConfig = $catalogProductMediaConfig; } /** - * {@inheritdoc} + * @inheritdoc */ public function getCaption() { @@ -63,6 +71,8 @@ public function getCaption() } /** + * Return no image url. + * * @return string */ public function getNoImageUrl() @@ -92,6 +102,8 @@ public function getImageTypes() } /** + * Return media attributes. + * * @return array */ public function getMediaAttributes() diff --git a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php index 77110975401ff..a73e7e7277d34 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php @@ -75,6 +75,7 @@ public function getIdentities() * Get price for exact simple product added to cart * * @inheritdoc + * @since 100.3.1 */ public function getProductPriceHtml(\Magento\Catalog\Model\Product $product) { diff --git a/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php b/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php new file mode 100644 index 0000000000000..fbc45a9cfc791 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Block\DataProviders; + +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Provides permissions data into template. + */ +class PermissionsData implements ArgumentInterface +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor + * + * @param AuthorizationInterface $authorization + */ + public function __construct(AuthorizationInterface $authorization) + { + $this->authorization = $authorization; + } + + /** + * Check that user is allowed to manage attributes + * + * @return bool + */ + public function isAllowedToManageAttributes(): bool + { + return $this->authorization->isAllowed('Magento_Catalog::attributes_attributes'); + } +} 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 55c0c8f6ca4ce..6a9e84e345985 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -35,7 +35,7 @@ class Configurable extends \Magento\Catalog\Block\Product\View\AbstractView /** * Current customer * - * @deprecated 100.2.0, as unused property + * @deprecated 100.2.0 as unused property * @var CurrentCustomer */ protected $currentCustomer; @@ -134,7 +134,7 @@ public function __construct( * Get cache key informative items. * * @return array - * @since 100.2.0 + * @since 100.1.10 */ public function getCacheKeyInfo() { @@ -253,7 +253,7 @@ public function getJsonConfig() * Get product images for configurable variations * * @return array - * @since 100.2.0 + * @since 100.1.10 */ protected function getOptionImages() { @@ -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() @@ -321,7 +326,7 @@ protected function getOptionPrices() 'tierPrices' => $tierPrices, 'msrpPrice' => [ 'amount' => $this->localeFormat->getNumber( - $product->getMsrp() + $this->priceCurrency->convertAndRound($product->getMsrp()) ), ], ]; @@ -332,7 +337,7 @@ protected function getOptionPrices() /** * Replace ',' on '.' for js * - * @deprecated 100.2.0 Will be removed in major release + * @deprecated 100.1.10 Will be removed in major release * @param float $price * @return string */ @@ -345,7 +350,7 @@ protected function _registerJsPrice($price) * Should we generate "As low as" block or not * * @return bool - * @since 100.2.0 + * @since 100.1.10 */ public function showMinimalPrice() { diff --git a/app/code/Magento/ConfigurableProduct/Model/AttributeOptionProviderInterface.php b/app/code/Magento/ConfigurableProduct/Model/AttributeOptionProviderInterface.php index ca289122a2126..e9e91485ea7a5 100644 --- a/app/code/Magento/ConfigurableProduct/Model/AttributeOptionProviderInterface.php +++ b/app/code/Magento/ConfigurableProduct/Model/AttributeOptionProviderInterface.php @@ -8,7 +8,7 @@ /** * Interface to retrieve options for attribute * @api - * @since 100.2.0 + * @since 100.1.11 */ interface AttributeOptionProviderInterface { @@ -18,7 +18,7 @@ interface AttributeOptionProviderInterface * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute * @param int $productId * @return array - * @since 100.2.0 + * @since 100.1.11 */ public function getAttributeOptions(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute, $productId); } diff --git a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php index 890564fdb303c..c7217dc9df80a 100644 --- a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php +++ b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php @@ -199,7 +199,7 @@ public function removeChild($sku, $childSku) * * @return \Magento\ConfigurableProduct\Helper\Product\Options\Factory * - * @deprecated 100.1.2 + * @deprecated 100.2.0 */ private function getOptionsFactory() { diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php deleted file mode 100644 index 92b7ab0d88ea8..0000000000000 --- a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; - -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Catalog\Model\Product; - -/** - * Extender of product identities for child of configurable products - */ -class ProductIdentitiesExtender -{ - /** - * @var Configurable - */ - private $configurableType; - - /** - * @param Configurable $configurableType - */ - public function __construct(Configurable $configurableType) - { - $this->configurableType = $configurableType; - } - - /** - * Add child identities to product identities - * - * @param Product $subject - * @param array $identities - * @return array - */ - public function afterGetIdentities(Product $subject, array $identities): array - { - foreach ($this->configurableType->getChildrenIds($subject->getId()) as $childIds) { - foreach ($childIds as $childId) { - $identities[] = Product::CACHE_TAG . '_' . $childId; - } - } - - return array_unique($identities); - } -} diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php index 8bc7f05b49e30..dc4ad39752e4f 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Model\Plugin; use Magento\Catalog\Api\Data\ProductInterface; @@ -56,7 +59,7 @@ public function beforeSave( ProductRepositoryInterface $subject, ProductInterface $product, $saveOptions = false - ) { + ): array { $result[] = $product; if ($product->getTypeId() !== Configurable::TYPE_CODE) { return $result; @@ -102,7 +105,7 @@ public function afterSave( ProductInterface $result, ProductInterface $product, $saveOptions = false - ) { + ): ProductInterface { if ($product->getTypeId() !== Configurable::TYPE_CODE) { return $result; } @@ -120,19 +123,23 @@ public function afterSave( * @throws InputException * @throws NoSuchEntityException */ - private function validateProductLinks(array $attributeCodes, array $linkIds) + private function validateProductLinks(array $attributeCodes, array $linkIds): void { $valueMap = []; foreach ($linkIds as $productId) { $variation = $this->productRepository->getById($productId); $valueKey = ''; foreach ($attributeCodes as $attributeCode) { - if (!$variation->getData($attributeCode)) { + if ($variation->getData($attributeCode) === null) { throw new InputException( - __('Product with id "%1" does not contain required attribute "%2".', $productId, $attributeCode) + __( + 'Product with id "%1" does not contain required attribute "%2".', + $productId, + $attributeCode + ) ); } - $valueKey = $valueKey . $attributeCode . ':' . $variation->getData($attributeCode) . ';'; + $valueKey .= $attributeCode . ':' . $variation->getData($attributeCode) . ';'; } if (isset($valueMap[$valueKey])) { throw new InputException( diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index 1b3ecfb1d222a..c2ae381b345c6 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -106,6 +106,7 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * Local cache * * @var array + * @since 100.4.0 */ protected $isSaleableBySku = []; @@ -591,6 +592,7 @@ protected function getGalleryReadHandler() * * @param \Magento\Catalog\Model\Product $product * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection + * @since 100.4.0 */ protected function getLinkedProductCollection($product) { @@ -1266,7 +1268,7 @@ private function getCatalogConfig() /** * @inheritdoc - * @since 100.2.0 + * @since 100.1.11 */ public function isPossibleBuyFromList($product) { 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 b7bbf7aa1871c..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 @@ -134,8 +134,7 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE, iterator_to_array($entityIds) ); - $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false); - $this->tableMaintainer->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []); $this->basePriceModifier->modifyPrice($temporaryPriceTable, iterator_to_array($entityIds)); $this->applyConfigurableOption($temporaryPriceTable, $dimensions, iterator_to_array($entityIds)); @@ -222,10 +221,9 @@ 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); } - $query = $select->insertFromSelect($temporaryOptionsTableName); - $this->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $temporaryOptionsTableName, []); } /** @@ -269,7 +267,7 @@ private function getMainTable($dimensions) if ($this->fullReindexAction) { return $this->tableMaintainer->getMainReplicaTable($dimensions); } - return $this->tableMaintainer->getMainTable($dimensions); + return $this->tableMaintainer->getMainTableByDimensions($dimensions); } /** 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 57f701721a6f3..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 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; @@ -174,6 +174,7 @@ public function getStoreId() * * @return $this * @throws \Exception + * @since 100.3.0 */ protected function _beforeLoad() { @@ -285,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 b76954075bcde..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) @@ -74,6 +74,7 @@ public function setProductFilter($product) * Add parent ids to `in` filter before load. * * @return $this + * @since 100.3.0 */ protected function _renderFilters() { @@ -84,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/Plugin/Model/ResourceModel/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php index 1555e88700a45..2f333e7ca6f6e 100644 --- a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php +++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php @@ -4,11 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\ConfigurableProduct\Api\Data\OptionInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Indexer\ActionInterface; +/** + * Plugin product resource model + */ class Product { /** @@ -21,18 +31,45 @@ class Product */ private $productIndexer; + /** + * @var ProductAttributeRepositoryInterface + */ + private $productAttributeRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + /** * Initialize Product dependencies. * * @param Configurable $configurable * @param ActionInterface $productIndexer + * @param ProductAttributeRepositoryInterface $productAttributeRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param FilterBuilder $filterBuilder */ public function __construct( Configurable $configurable, - ActionInterface $productIndexer + ActionInterface $productIndexer, + ProductAttributeRepositoryInterface $productAttributeRepository = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null, + FilterBuilder $filterBuilder = null ) { $this->configurable = $configurable; $this->productIndexer = $productIndexer; + $this->productAttributeRepository = $productAttributeRepository ?: ObjectManager::getInstance() + ->get(ProductAttributeRepositoryInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() + ->get(SearchCriteriaBuilder::class); + $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance() + ->get(FilterBuilder::class); } /** @@ -41,6 +78,7 @@ public function __construct( * @param \Magento\Catalog\Model\ResourceModel\Product $subject * @param \Magento\Framework\DataObject $object * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -51,6 +89,39 @@ public function beforeSave( /** @var \Magento\Catalog\Model\Product $object */ if ($object->getTypeId() == Configurable::TYPE_CODE) { $object->getTypeInstance()->getSetAttributes($object); + $this->resetConfigurableOptionsData($object); + } + } + + /** + * Set null for configurable options attribute of configurable product + * + * @param \Magento\Catalog\Model\Product $object + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function resetConfigurableOptionsData($object) + { + $extensionAttribute = $object->getExtensionAttributes(); + if ($extensionAttribute && $extensionAttribute->getConfigurableProductOptions()) { + $attributeIds = []; + /** @var OptionInterface $option */ + foreach ($extensionAttribute->getConfigurableProductOptions() as $option) { + $attributeIds[] = $option->getAttributeId(); + } + + $filter = $this->filterBuilder + ->setField(ProductAttributeInterface::ATTRIBUTE_ID) + ->setConditionType('in') + ->setValue($attributeIds) + ->create(); + $this->searchCriteriaBuilder->addFilters([$filter]); + $searchCriteria = $this->searchCriteriaBuilder->create(); + $optionAttributes = $this->productAttributeRepository->getList($searchCriteria)->getItems(); + + foreach ($optionAttributes as $optionAttribute) { + $object->setData($optionAttribute->getAttributeCode(), null); + } } } diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php index 5581fcc07b861..af9e6e7bdebcd 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php @@ -19,13 +19,13 @@ class ConfigurablePriceResolver implements PriceResolverInterface /** * @var PriceCurrencyInterface - * @deprecated 100.1.1 + * @deprecated 100.0.2 */ protected $priceCurrency; /** * @var Configurable - * @deprecated 100.1.1 + * @deprecated 100.0.2 */ protected $configurable; diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml new file mode 100644 index 0000000000000..c48f22a3656d5 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup"> + <annotations> + <description>Adds 3 provided Options to a new Attribute on the Configurable Product creation/edit page. Selected default first option. Set "Use in Layered Navigation" to "Yes".</description> + </annotations> + <arguments> + <argument name="label" defaultValue="colorProductAttribute" /> + <argument name="option1" defaultValue="colorProductAttribute1"/> + <argument name="option2" defaultValue="colorProductAttribute2"/> + <argument name="option3" defaultValue="colorProductAttribute3"/> + </arguments> + + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickOnNewAttribute"/> + <waitForPageLoad stepKey="waitForIFrame"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{label.default_label}}" stepKey="fillDefaultLabel"/> + + <!--Add option 1 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption1"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('1')}}" time="30" stepKey="waitForOptionRow1" after="clickAddOption1"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('0')}}" userInput="{{option1.name}}" stepKey="fillAdminLabel1" after="waitForOptionRow1"/> + <click selector="{{AdminNewAttributePanel.isDefault('1')}}" stepKey="selectDefault" after="fillAdminLabel1"/> + + <!--Add option 2 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption2" after="selectDefault"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('2')}}" time="30" stepKey="waitForOptionRow2" after="clickAddOption2"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('1')}}" userInput="{{option2.name}}" stepKey="fillAdminLabel2" after="waitForOptionRow2"/> + + <!--Add option 3 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption3" after="fillAdminLabel2"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('3')}}" time="30" stepKey="waitForOptionRow3" after="clickAddOption3"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('2')}}" userInput="{{option3.name}}" stepKey="fillAdminLabel3" after="waitForOptionRow3"/> + + <!-- Set Use In Layered Navigation --> + <click selector="{{AdminNewAttributePanel.storefrontPropertiesTab}}" stepKey="goToStorefrontPropertiesTab" after="fillAdminLabel3"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.storefrontPropertiesTitle}}" stepKey="waitTabLoad" after="goToStorefrontPropertiesTab"/> + <selectOption selector="{{AdminNewAttributePanel.useInLayeredNavigation}}" stepKey="selectUseInLayer" userInput="Filterable (with results)" after="waitTabLoad"/> + + <!--Save attribute--> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickSaveAttribute"/> + <waitForPageLoad stepKey="waitForSavingAttribute"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml new file mode 100644 index 0000000000000..cc709b80efebb --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillBasicValueConfigurableProductActionGroup"> + <annotations> + <description>Goes to the Admin Product grid page. Fill basic value for Configurable Product using the default Product Options.</description> + </annotations> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + <argument name="category" defaultValue="_defaultCategory"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad time="30" stepKey="wait1"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> + <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> + <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="fillCategory"/> + <selectOption userInput="{{product.visibility}}" selector="{{AdminProductFormSection.visibility}}" stepKey="fillVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml new file mode 100644 index 0000000000000..969a41e27d459 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGotoSelectValueAttributePageActionGroup"> + <annotations> + <description>Goes to the select values page from each attribute to include in the product.</description> + </annotations> + + <arguments> + <argument name="defaultLabelAttribute" type="string" defaultValue="{{colorProductAttribute.default_label}}"/> + </arguments> + + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="{{defaultLabelAttribute}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml new file mode 100644 index 0000000000000..cc2ff9a63ae40 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.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="AdminSelectValueFromAttributeActionGroup"> + <annotations> + <description>Click to check option.</description> + </annotations> + + <arguments> + <argument name="option" defaultValue="colorProductAttribute1"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeOption(option.name)}}" stepKey="clickOnCreateNewValue2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml new file mode 100644 index 0000000000000..3cca319d9569c --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.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="AdminSetQuantityToEachSkusConfigurableProductActionGroup"> + <annotations> + <description>Set quantity 1 to all child skus for configurable product. Save a configurable product and confirm.</description> + </annotations> + + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="1" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontUpdateCartItemEditParametersProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontUpdateCartItemEditParametersProductActionGroup.xml new file mode 100644 index 0000000000000..595cfa7c77409 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontUpdateCartItemEditParametersProductActionGroup.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="StorefrontUpdateCartItemEditParametersProductActionGroup"> + <arguments> + <argument name="rowNumber" type="string" defaultValue="1"/> + </arguments> + <click selector="{{CheckoutCartProductSection.nthEditButton(rowNumber)}}" stepKey="clickEditConfigurableProductButton"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml index a1a499f33eda0..c827b9998450a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml @@ -26,4 +26,23 @@ <requiredEntity type="ValueIndex">ValueIndex2</requiredEntity> <requiredEntity type="ValueIndex">ValueIndex3</requiredEntity> </entity> + <entity name="ConfigurableProduct15Options" type="ConfigurableProductOption"> + <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> + <data key="label">option</data> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + </entity> </entities> 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/AdminAddDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml index 0d83cc6610194..4190dafb927e1 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml @@ -81,7 +81,7 @@ </createData> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2"/> <deleteData createDataKey="childProductHandle1" stepKey="deleteChild1"/> @@ -94,8 +94,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="productIndexPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGrid"> <argument name="product" value="$$baseConfigProductHandle$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml index 72ebd7962f420..c318b3e37cd0f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml @@ -29,7 +29,7 @@ <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct2"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> <argument name="productName" value="$$createConfigProductCreateConfigurableProduct.name$$"/> @@ -108,6 +108,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <magentoCron stepKey="runCronIndex" groups="index"/> <!--Go to frontend and check image and price--> <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml index 6e26d73f3a36f..d83b994b5d6b3 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml @@ -36,8 +36,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -64,7 +63,7 @@ <!-- Save product --> <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProductAgain"/> <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml index 8962efbb8dd26..804de69e38280 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml @@ -46,13 +46,11 @@ </createData> <!--Go to created product page--> <comment userInput="Go to created product page" stepKey="goToProdPage"/> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductGrid"/> - <waitForPageLoad stepKey="waitForProductPage1"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductGrid"/> <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterByName"> <argument name="name" value="$$createConfigProduct.name$$"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductName"/> - <waitForPageLoad stepKey="waitForProductEditPageToLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductName"/> <!--Create configurations for the product--> <comment userInput="Create configurations for the product" stepKey="createConfigurations"/> <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="expandConfigurationsTab1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml index 597e95117349f..f4cad6590e1f6 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml @@ -115,8 +115,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> <!-- Create three configurable products with options --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <!-- Edit created first product as configurable product with options --> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterGridByFirstProduct"> <argument name="product" value="$$createFirstConfigurableProduct$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml index dc8c09864d0ab..b9e331bbfe5b4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml @@ -50,16 +50,14 @@ </after> <!-- Find the product that we just created using the product grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <waitForPageLoad stepKey="waitForProductFilterLoad"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- Create configurations based off the Text Swatch we created earlier --> <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> @@ -101,7 +99,7 @@ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitForNextPageOpened3"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveButtonVisible"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitForPopUpVisible"/> <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> <dontSeeElement selector="{{AdminMessagesSection.success}}" stepKey="dontSeeSaveProductMessage"/> @@ -119,7 +117,7 @@ <!--Click on "Save"--> <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProductAgain"/> <!--Click on "Confirm". Product is saved, success message appears --> <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml index 274a75aedbc5f..43d1ed40e92ad 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml @@ -53,15 +53,14 @@ <waitForPageLoad stepKey="waitForGenerateConfigure"/> <grabValueFrom selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" stepKey="grabTextFromContent"/> <fillField stepKey="fillMoreThan64Symbols" selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" userInput="01234567890123456789012345678901234567890123456789012345678901234"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct1"/> <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" visible="true" stepKey="clickOnCloseInPopup"/> <see stepKey="seeErrorMessage" userInput="Please enter less or equal than 64 symbols."/> <fillField stepKey="fillCorrectSKU" selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" userInput="$grabTextFromContent"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct2"/> <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="clickOnConfirmInPopup"/> <see userInput="You saved the product." stepKey="seeSaveConfirmation"/> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid1"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> @@ -72,8 +71,7 @@ <actionGroup stepKey="deleteProduct1" ref="DeleteProductBySkuActionGroup"> <argument name="sku" value="$grabTextFromContent"/> </actionGroup> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml index a7615d5565828..4f6407ca4150c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml @@ -139,8 +139,7 @@ </after> <!-- Search for prefix of the 3 products we created via api --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" stepKey="clearAll" visible="true"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchForProduct"> <argument name="keyword" value="ApiConfigurableProduct.name"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml index 807ea69bb3958..186752fd52684 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml @@ -81,8 +81,7 @@ <!-- go to admin and delete --> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="wait2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" stepKey="clearAll" visible="true"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchForProduct"> <argument name="keyword" value="ApiConfigurableProduct.name"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml index 10cdcea2855d6..f8cd1760788a8 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml @@ -71,7 +71,7 @@ </actionGroup> <!--See SKU length errors in Current Variations grid--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductFail"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProductFail"/> <seeInCurrentUrl url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'configurable')}}" stepKey="seeRemainOnCreateProductPage"/> <see selector="{{AdminProductFormConfigurationsSection.variationsSkuInputErrorByRow('1')}}" userInput="Please enter less or equal than 64 symbols." stepKey="seeSkuTooLongError1"/> <see selector="{{AdminProductFormConfigurationsSection.variationsSkuInputErrorByRow('2')}}" userInput="Please enter less or equal than 64 symbols." stepKey="seeSkuTooLongError2"/> @@ -79,7 +79,7 @@ <fillField selector="{{AdminProductFormConfigurationsSection.variationsSkuInputByRow('1')}}" userInput="LongSku-$$getConfigAttributeOption1.label$$" stepKey="fixConfigurationSku1"/> <fillField selector="{{AdminProductFormConfigurationsSection.variationsSkuInputByRow('2')}}" userInput="LongSku-$$getConfigAttributeOption2.label$$" stepKey="fixConfigurationSku2"/> <!--Save product successfully--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductSuccess"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProductSuccess"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> <!--Assert configurations on the product edit pag--> @@ -91,7 +91,9 @@ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{ProductWithLongNameSku.price}}" stepKey="seeConfigurationsPrice"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Assert storefront category list page--> <amOnPage url="/" stepKey="amOnStorefront"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml index 8d2f80ef262fd..3f21007a76282 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml @@ -93,19 +93,17 @@ <see stepKey="checkForOutOfStock" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> <!-- Find the first simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="ApiSimpleOne"/> </actionGroup> <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- Edit the quantity of the simple first product as 0 --> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- Check to make sure that the configurable product shows up as in stock --> <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage2"/> @@ -113,19 +111,17 @@ <see stepKey="checkForOutOfStock2" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> <!-- Find the second simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage2"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct2"> <argument name="product" value="ApiSimpleTwo"/> </actionGroup> <waitForPageLoad stepKey="waitForFiltersToBeApplied2"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> - <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage2"/> <!-- Edit the quantity of the second simple product as 0 --> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct2"/> <!-- Check to make sure that the configurable product shows up as out of stock --> <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage3"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml index 3121725c23fe9..5a97fb14abda9 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml @@ -102,19 +102,17 @@ <see stepKey="checkForOutOfStock2" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> <!-- Find the second simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage2"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct2"> <argument name="product" value="ApiSimpleTwo"/> </actionGroup> <waitForPageLoad stepKey="waitForFiltersToBeApplied2"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> - <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage2"/> <!-- Edit the quantity of the second simple product as 0 --> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct2"/> <!-- Check to make sure that the configurable product shows up as out of stock --> <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage3"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml index a35ef058dfd80..6311eaa9f2f99 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml @@ -77,8 +77,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" stepKey="clearAll" visible="true"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickShowFilters"/> <selectOption selector="{{AdminProductGridFilterSection.typeFilter}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="selectConfigurableType"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml index 6d9015b5d1cbf..421fcb0c03263 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml @@ -77,15 +77,18 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" stepKey="clearAll" visible="true"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchForProduct"> <argument name="keyword" value="ApiConfigurableProduct.name"/> </actionGroup> <waitForPageLoad stepKey="wait2"/> <seeNumberOfElements selector="{{AdminProductGridSection.productGridRows}}" userInput="1" stepKey="seeOneResult"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="{{ApiConfigurableProduct.name}}" stepKey="seeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="{{ApiConfigurableProduct.name}}"/> + </actionGroup> <see selector="{{AdminProductGridFilterSection.enabledFilters}}" userInput="{{ApiConfigurableProduct.name}}" stepKey="seeInActiveFilters"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml index 4b6baf8c58493..ba120d75f8e62 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml @@ -116,16 +116,13 @@ <grabTextFrom stepKey="getBeforeOption" selector="{{StorefrontProductInfoMainSection.nthAttributeOnPage('1')}}"/> <!-- Find the product that we just created using the product grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <waitForPageLoad stepKey="waitForProductFilterLoad"/> - - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- change the option on the first attribute --> <selectOption stepKey="clickFirstAttribute" selector="{{ModifyAttributes.nthExistingAttribute($$createModifiableProductAttribute.default_frontend_label$$)}}" userInput="option1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml index 56f53519e69af..e0150f08d8360 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml @@ -94,8 +94,7 @@ </after> <!-- Find the product that we just created using the product grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="$$createConfigProduct$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml index 589f20d0d544c..854cfb98607a7 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml @@ -51,7 +51,7 @@ <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="42" stepKey="enterAttributeQuantity"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!-- Verify that the added option is present in the storefront --> <amOnPage url="{{StorefrontProductPage.url(_defaultProduct.urlKey)}}" stepKey="amOnStorefrontPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml index 186799bf4626b..556ede0bdc06f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml @@ -37,15 +37,13 @@ <deleteData createDataKey="createProduct1" stepKey="deleteFirstProduct"/> <deleteData createDataKey="createProduct2" stepKey="deleteSecondProduct"/> <deleteData createDataKey="createProduct3" stepKey="deleteThirdProduct"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Search for prefix of the 3 products we created via api --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clearAll"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchForProduct"> <argument name="keyword" value="ApiConfigurableProduct.name"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml index 1eb3df993dd1c..345b21246f30f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml @@ -88,8 +88,7 @@ <waitForPageLoad stepKey="wait2"/> <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandActions"/> <click selector="{{AdminProductFormConfigurationsSection.disableProductBtn}}" stepKey="clickDisable"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="wait3"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <!--check storefront for one option--> <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="amOnStorefront2"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml index 00b17fda944f1..ec0ed623d2b0c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml @@ -92,8 +92,7 @@ <!--remove an option--> <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandActions"/> <click selector="{{AdminProductFormConfigurationsSection.removeProductBtn}}" stepKey="clickRemove"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="wait3"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <!--check admin for one option--> <dontSee selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="$$createConfigChildProduct1.name$$" stepKey="dontSeeOption1Admin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml index a4051adfe5d28..75fbaa7f43887 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml @@ -52,7 +52,7 @@ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!-- Verify that the removed option is not present in the storefront --> <amOnPage url="{{StorefrontProductPage.url(_defaultProduct.urlKey)}}" stepKey="amOnStorefrontPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml index 98bd5a0fed4ed..ebefab1f6650a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml @@ -24,6 +24,10 @@ <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillProductForm"> <argument name="product" value="_defaultProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Simple Product"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToVirtualTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToVirtualTest.xml index 756cdfd5d5d6f..a0ad8475b7d41 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToVirtualTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToVirtualTest.xml @@ -21,6 +21,10 @@ <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="openProductFillForm"> <argument name="productType" value="configurable"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Virtual Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Virtual Product"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml index db5c824341c57..17c7426dc547f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml @@ -27,7 +27,7 @@ </createData> </before> <after> - <actionGroup ref="GoToProductCatalogPageActionGroup" stepKey="goToProductCatalogPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteConfigurableProduct"> <argument name="product" value="_defaultProduct"/> </actionGroup> @@ -62,12 +62,16 @@ <actionGroup ref="SaveConfiguredProductActionGroup" stepKey="saveProductForm"/> <!-- Check that product was added with implicit type change --> <comment stepKey="beforeVerify" userInput="Verify Product Type Assigned Correctly"/> - <actionGroup ref="GoToProductCatalogPageActionGroup" stepKey="goToProductCatalogPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetSearch"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="searchForProduct"> <argument name="product" value="_defaultProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="2"/> + <argument name="column" value="Type"/> + <argument name="value" value="Configurable Product"/> + </actionGroup> <actionGroup ref="AssertProductInStorefrontProductPageActionGroup" stepKey="assertProductInStorefrontProductPage"> <argument name="product" value="_defaultProduct"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToConfigurableTest.xml index cbfa1cc2b8bd6..bf7d97df75bb0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToConfigurableTest.xml @@ -40,7 +40,11 @@ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> </actionGroup> <actionGroup ref="SaveConfiguredProductActionGroup" stepKey="saveProductForm"/> - <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="2"/> + <argument name="column" value="Type"/> + <argument name="value" value="Configurable Product"/> + </actionGroup> <!-- Verify product on store front --> <comment userInput="Verify product on store front" stepKey="commentVerifyProductGrid"/> <actionGroup ref="VerifyOptionInProductStorefrontActionGroup" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToConfigurableTest.xml index cfeb95afc4924..b1df023e7deec 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToConfigurableTest.xml @@ -38,7 +38,11 @@ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> </actionGroup> <actionGroup ref="SaveConfiguredProductActionGroup" stepKey="saveProductForm"/> - <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="2"/> + <argument name="column" value="Type"/> + <argument name="value" value="Configurable Product"/> + </actionGroup> <actionGroup ref="VerifyOptionInProductStorefrontActionGroup" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml index e5456429373e1..044346041d30c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml @@ -36,8 +36,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -68,15 +67,22 @@ <actionGroup ref="SaveConfigurableProductAddToCurrentAttributeSetActionGroup" stepKey="saveProduct"/> <!-- Assert child products generated sku in grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPageLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterFirstProductByNameInGrid"> <argument name="name" value="{{colorConfigurableProductAttribute1.name}}"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute1.name}}" stepKey="seeFirstProductSkuInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeFirstProductSkuInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="SKU"/> + <argument name="value" value="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute1.name}}"/> + </actionGroup> <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterSecondProductByNameInGrid"> <argument name="name" value="{{colorConfigurableProductAttribute2.name}}"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute2.name}}" stepKey="seeSecondProductSkuInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeSecondProductSkuInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="SKU"/> + <argument name="value" value="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute2.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml index 32117fdfe4366..c285287130aca 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml @@ -53,8 +53,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -88,21 +87,21 @@ <actionGroup ref="SaveConfigurableProductWithNewAttributeSetActionGroup" stepKey="saveConfigurableProduct"/> <!-- Find configurable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <!-- Assert configurable product on admin product page --> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <actionGroup ref="AssertConfigurableProductOnAdminProductPageActionGroup" stepKey="assertConfigurableProductOnAdminProductPage"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml index 3bf5666d5a997..d210f90779d99 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml @@ -60,8 +60,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -89,15 +88,13 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveConfigurableProduct"/> <!-- Find configurable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <!-- Assert configurable product on admin product page --> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <actionGroup ref="AssertConfigurableProductOnAdminProductPageActionGroup" stepKey="assertConfigurableProductOnAdminProductPage"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -111,7 +108,9 @@ <actionGroup ref="DisplayOutOfStockProductActionGroup" stepKey="displayOutOfStockProduct"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Assert configurable product is not present in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml index fa8866fa7d91c..120734d679d09 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml @@ -63,8 +63,7 @@ </after> <!--Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -133,7 +132,9 @@ <actionGroup ref="SaveConfigurableProductAddToCurrentAttributeSetActionGroup" stepKey="saveProduct"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml index e76d14f3a6aae..4afc95f9a6355 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml @@ -83,8 +83,7 @@ </after> <!--Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -125,7 +124,9 @@ <actionGroup ref="DisplayOutOfStockProductActionGroup" stepKey="displayOutOfStockProduct"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml index 9516216d4a62e..ad634ed0144ae 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml @@ -82,8 +82,7 @@ </after> <!--Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -121,7 +120,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveConfigurableProduct"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml index 660eb82a9eacb..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> @@ -71,8 +71,7 @@ </after> <!--Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -110,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/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml index f2a8e78523758..2fcf9a622a97f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml @@ -56,8 +56,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -102,11 +101,17 @@ <actionGroup ref="FilterProductGridBySkuAndNameActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="{{ApiConfigurableProduct.type_id}}"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml index 273e37089973b..b562c8ab6fb1a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml @@ -49,8 +49,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -92,11 +91,17 @@ <actionGroup ref="FilterProductGridBySkuAndNameActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="{{ApiConfigurableProduct.type_id}}"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Assert configurable product on product page --> <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml index e625a1cf6f2be..1a6d802987cd3 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml @@ -36,10 +36,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createConfigurableProduct.name$$)}}" stepKey="amOnConfigurableProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigurableProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchBarByProductSku"> + <argument name="query" value="$$createConfigurableProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createConfigurableProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml index dd176455a03ba..f287aca332b48 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml @@ -36,7 +36,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveVirtualProductForm"/> <!--Assert virtual product on Admin product page grid--> <comment userInput="Assert virtual product on Admin product page grid" stepKey="commentAssertVirtualProductOnAdmin"/> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPageForVirtual"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPageForVirtual"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySkuForVirtual"> <argument name="sku" value="$createProduct.sku$"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml index 14979f93ca423..f3b79765f746d 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml @@ -61,7 +61,7 @@ <actionGroup ref="SaveConfiguredProductActionGroup" stepKey="saveConfigProductForm"/> <!--Assert configurable product on Admin product page grid--> <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertConfigProductOnAdmin"/> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySku"> <argument name="sku" value="$createProduct.sku$"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml index 90a396b970c3a..bb5baf33d95fb 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml @@ -60,14 +60,30 @@ <actionGroup ref="SaveConfiguredProductActionGroup" stepKey="saveNewConfigurableProductForm"/> <!--Assert configurable product on Admin product page grid--> <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertConfigurableProductOnAdmin"/> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPageForConfigurable"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPageForConfigurable"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySkuForConfigurable"> <argument name="sku" value="$$createProduct.sku$$"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeConfigurableProductNameInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Configurable Product" stepKey="seeConfigurableProductTypeInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('2', 'Name')}}" userInput="$$createProduct.name$$-option1" stepKey="seeConfigurableProductNameInGrid1"/> - <see selector="{{AdminProductGridSection.productGridCell('3', 'Name')}}" userInput="$$createProduct.name$$-option2" stepKey="seeConfigurableProductNameInGrid2"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeConfigurableProductNameInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeConfigurableProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Configurable Product"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeConfigurableProductNameInGrid1"> + <argument name="row" value="2"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$-option1"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeConfigurableProductNameInGrid2"> + <argument name="row" value="3"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$-option2"/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearConfigurableProductFilters"/> <!--Assert configurable product on storefront--> <comment userInput="Assert configurable product on storefront" stepKey="commentAssertConfigurableProductOnStorefront"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml index b6b3d21c8a626..fa25277554b74 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml @@ -81,7 +81,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2"/> <deleteData createDataKey="childProductHandle1" stepKey="deleteChild1"/> @@ -95,8 +95,7 @@ </after> <comment userInput="Filter and edit simple product 1" stepKey="filterAndEditComment1"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="productIndexPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridSimple"> <argument name="product" value="$$simple1Handle$$"/> @@ -140,8 +139,7 @@ </actionGroup> <comment userInput="Filter and edit config product" stepKey="filterAndEditComment2"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="productIndexPage2"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage2"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridConfig"> <argument name="product" value="$$baseConfigProductHandle$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml index 86d4070a9a2c8..076d55025aca5 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml @@ -81,7 +81,7 @@ </createData> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2"/> <deleteData createDataKey="childProductHandle1" stepKey="deleteChild1"/> @@ -93,8 +93,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="productIndexPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGrid"> <argument name="product" value="$$baseConfigProductHandle$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml index a34dfd06ce844..5ecc0c33ad7a2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml @@ -15,11 +15,10 @@ <title value="Configurable product prices should not disappear on storefront for additional store"/> <description value="Configurable product price should not disappear for additional stores on frontEnd if disabled for default store"/> <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-92247"/> + <testCaseId value="MC-25761"/> <group value="ConfigurableProduct"/> </annotations> <before> - <createData entity="ApiCategory" stepKey="createCategory"/> <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> <requiredEntity createDataKey="createCategory"/> @@ -65,6 +64,22 @@ <requiredEntity createDataKey="createConfigChildProduct2"/> </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> + + <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> + + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> </before> <after> @@ -75,46 +90,21 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> - <argument name="websiteName" value="Second Website"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> - <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="addNewWebsite"> - <argument name="newWebsiteName" value="Second Website"/> - <argument name="websiteCode" value="second_website"/> - </actionGroup> - <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="addNewStoreGroup"> - <argument name="website" value="Second Website"/> - <argument name="storeGroupName" value="Second Store"/> - <argument name="storeGroupCode" value="second_store"/> - </actionGroup> - - <!--Create Store view --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> - <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> - <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption userInput="1" selector="{{AdminNewStoreSection.statusDropdown}}" stepKey="enableStoreViewStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickStoreViewSaveButton"/> - <waitForElementVisible selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" stepKey="waitForAcceptNewStoreViewCreationModal" /> - <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="AcceptNewStoreViewCreation"/> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReolad"/> - <see userInput="You saved the store view." stepKey="seeSaveMessage" /> - <!--go to admin and open product edit page to disable product all store view --> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> - <argument name="productId" value="$$createConfigProduct.id$$"/> + <argument name="productId" value="$createConfigProduct.id$"/> </actionGroup> <waitForPageLoad stepKey="waitEditPage"/> <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="disableProductForAllStoreView"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> - <waitForLoadingMaskToDisappear stepKey="waitForProductPageSave1" /> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveWithThreeOptions"/> <dontSeeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="dontSeeCheckboxEnableProductIsChecked"/> <!-- Disable each of the child products for All Store views --> @@ -126,17 +116,17 @@ <!-- Add product to second website --> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsitesSection1"/> - <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> <waitForLoadingMaskToDisappear stepKey="waitForProductPageSave"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> <!-- switch to the second store view --> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStore.name)}}" stepKey="chooseStoreView"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> + <see userInput="{{customStore.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> <!-- enable the config product for the second store --> <waitForElementVisible selector="{{AdminProductFormSection.productStatusUseDefault}}" stepKey="waitForDefaultValueCheckBox"/> @@ -152,10 +142,10 @@ </actionGroup> <waitForPageLoad stepKey="waitEditPage2"/> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher1"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView1"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStore.name)}}" stepKey="chooseStoreView1"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage1"/> <waitForPageLoad time="30" stepKey="waitForPageLoad8"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName1"/> + <see userInput="{{customStore.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName1"/> <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandActionsForFirstVariation2"/> <click selector="{{AdminProductFormConfigurationsSection.enableProductBtn}}" stepKey="clickEnableChildProduct1"/> <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('2')}}" stepKey="clickToExpandActionsForSecondVariation2"/> @@ -163,7 +153,7 @@ <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveAll"/> <!-- assert second store view storefront category list page --> - <amOnPage url="/second_store_view/" stepKey="amOnsecondStoreFront1"/> + <amOnPage url="/{{customStore.code}}/" stepKey="amOnsecondStoreFront1"/> <waitForPageLoad stepKey="waitForPageLoad31"/> <click userInput="$$createCategory.name$$" stepKey="clickOnCategoryName1"/> <waitForPageLoad stepKey="waitForPageLoad41"/> @@ -175,7 +165,7 @@ </actionGroup> <waitForPageLoad stepKey="waitChild1EditPageToLoad"/> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProduct1InWebsitesSection"/> - <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite1"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite1"/> <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveUpdatedChild1Again"/> <!--go to admin again and open child product1 and enable for second store view--> @@ -184,10 +174,10 @@ </actionGroup> <waitForPageLoad stepKey="waitChild1EditPageToLoad1"/> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcherP1"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView2P1"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStore.name)}}" stepKey="chooseStoreView2P1"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessageP1"/> <waitForPageLoad time="30" stepKey="waitForStoreViewSwitchedP1"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP1"/> + <see userInput="{{customStore.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP1"/> <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSliderP1"/> <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="seeThatProduct1IsEnabled"/> <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="save2UpdatedChild1"/> @@ -198,7 +188,7 @@ </actionGroup> <waitForPageLoad stepKey="waitChild2EditPageToLoad"/> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProduct2InWebsitesSection"/> - <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite2"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite2"/> <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveUpdatedChild2"/> <!--go to admin again and open child product2 and enable for second store view--> @@ -207,16 +197,16 @@ </actionGroup> <waitForPageLoad stepKey="waitChild2EditPageToLoad1"/> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcherP2"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView2P2"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStore.name)}}" stepKey="chooseStoreView2P2"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessageP2"/> <waitForPageLoad time="30" stepKey="waitForStoreViewSwitchedP2"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP2"/> + <see userInput="{{customStore.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP2"/> <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSliderP2"/> <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="seeThatProduct2IsEnabled"/> <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="save2UpdatedChild2"/> <!-- assert storefront category list page --> - <amOnPage url="/second_store_view/" stepKey="amOnsecondStoreFront"/> + <amOnPage url="/{{customStore.code}}/" stepKey="amOnsecondStoreFront"/> <waitForPageLoad stepKey="waitForPageLoad3"/> <click userInput="$$createCategory.name$$" stepKey="clickOnCategoryName"/> <waitForPageLoad stepKey="waitForPageLoad4"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index 13c4cad312188..e34bf7c22f06b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -78,8 +78,12 @@ <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> - <magentoCLI command="indexer:reindex" stepKey="reindexAll"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Verify Configurable Product in checkout cart items --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml index 372aa03e4e152..7f1034db062df 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml @@ -84,7 +84,7 @@ <waitForPageLoad stepKey="waitForEditPage"/> <fillField selector="{{AdminProductFormSection.setProductAsNewFrom}}" userInput="01/1/2000" stepKey="fillProductNewFrom"/> <fillField selector="{{AdminProductFormSection.setProductAsNewTo}}" userInput="01/1/2099" stepKey="fillProductNewTo"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here, via merge --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml index 902787ac58e8c..b87ddf612be19 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml @@ -176,7 +176,7 @@ <waitForPageLoad stepKey="waitForShippingMethods"/> <click selector="{{AdminInvoicePaymentShippingSection.shippingMethod}}" stepKey="chooseShippingMethod"/> <waitForPageLoad stepKey="waitForShippingMethodLoad"/> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="clickSubmitOrder" /> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="checkOrderSuccessfullyCreated"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml index a8856288b422a..0b7bca201ec32 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml @@ -35,15 +35,14 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage1"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage1"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> <argument name="product" value="$$createConfigProduct$$"/> </actionGroup> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="changeProductQuantity"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveChanges"/> - <waitForPageLoad stepKey="waitProductGridToBeLoaded"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveChanges"/> <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="navigateToProductPage"/> <waitForPageLoad stepKey="waitForProductPage"/> @@ -71,12 +70,11 @@ </actionGroup> <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> - <waitForPageLoad stepKey="waitForNewInvoicePageLoad"/> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceButton"/> <fillField selector="{{AdminInvoiceItemsSection.qtyToInvoiceColumn}}" userInput="1" stepKey="ChangeQtyToInvoice"/> <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQuantity"/> <waitForPageLoad stepKey="waitPageToBeLoaded"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> <waitForPageLoad stepKey="waitOrderDetailToLoad"/> <fillField selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" userInput="1" stepKey="changeItemQtyToShip"/> @@ -88,12 +86,16 @@ <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Canceled 3" stepKey="seeCanceledQuantity"/> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySku"> <argument name="sku" value="$$createConfigProduct.sku$$"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Quantity')}}" userInput="99" stepKey="seeProductSkuInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductSkuInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Quantity"/> + <argument name="value" value="99"/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> </test> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml index fbf23597a3927..4ad2d0dc936eb 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml @@ -117,8 +117,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <!-- Go to the product page for the first product --> - <amOnPage stepKey="goToProductGrid" url="{{ProductCatalogPage.url}}"/> - <waitForPageLoad stepKey="waitForProductGridLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductGrid"/> <actionGroup stepKey="searchForSimpleProduct" ref="FilterProductGridBySku2ActionGroup"> <argument name="sku" value="$$createConfigChildProduct1.sku$$"/> </actionGroup> @@ -127,7 +126,7 @@ <!-- Edit the attribute for the first simple product --> <selectOption stepKey="editSelectAttribute" selector="{{ModifyAttributes.nthExistingAttribute($$createConfigProductAttributeSelect.default_frontend_label$$)}}" userInput="$$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$"/> <scrollToTopOfPage stepKey="scrollToTop"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> </before> @@ -145,12 +144,15 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <magentoCLI command="indexer:reindex" stepKey="reindexAll"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Quick search the storefront for the first attribute option --> - <amOnPage stepKey="goToStoreFront" url="{{StorefrontHomePage.url}}"/> - <waitForPageLoad stepKey="waitForStorefront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStoreFront"/> <submitForm selector="#search_mini_form" parameterArray="['q' => $$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$]" stepKey="searchStorefront1" /> <seeElement stepKey="seeProduct1" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> 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/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml index ca0426f1b97d5..e20a6dcfa09b8 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml @@ -27,8 +27,12 @@ <argument name="category" value="$$createCategory$$"/> </actionGroup> <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush eav" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="eav"/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductMSRPCovertTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductMSRPCovertTest.xml new file mode 100644 index 0000000000000..9526e8568b26d --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductMSRPCovertTest.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontConfigurableProductMSRPCovertTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="View configurable product options, verify convert MSRP currency on storefront."/> + <title value="Verify convert MSRP currency of configurable product options"/> + <description value="Check convert MSRP currency of configurable product options."/> + <testCaseId value="MC-37575"/> + <severity value="MAJOR"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <createData entity="ApiSimpleProductWithPrice50" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleProductWithPrice60" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <createData entity="MsrpEnableMAP" stepKey="enableMAP"/> + <magentoCLI command="config:set currency/options/allow EUR,USD" stepKey="setCurrencyAllow"/> + </before> + <after> + + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <createData entity="MsrpDisableMAP" stepKey="disableMAP"/> + <magentoCLI command="config:set currency/options/allow USD" stepKey="setCurrencyAllow"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToFirstChildProductEditPage"> + <argument name="productId" value="$$createConfigChildProduct1.id$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminSetAdvancedPricingActionGroup" stepKey="setAdvancedPricingFirst"> + <argument name="advancedPrice" value="100"/> + </actionGroup> + + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToSecondChildProductEditPage"> + <argument name="productId" value="$$createConfigChildProduct2.id$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <actionGroup ref="AdminSetAdvancedPricingActionGroup" stepKey="setAdvancedPricingSecond"> + <argument name="advancedPrice" value="100"/> + </actionGroup> + + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateToProduct"> + <argument name="productUrlKey" value="$$createConfigProduct.custom_attributes[url_key]$$"/> + </actionGroup> + + <actionGroup ref="StorefrontSwitchCurrencyActionGroup" stepKey="switchEURCurrency"> + <argument name="currency" value="EUR"/> + </actionGroup> + + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1.value$$" stepKey="selectFirstOption"/> + <waitForElement selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="waitForLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="grabProductMapPrice"/> + <assertNotEquals stepKey="assertProductMapPrice"> + <actualResult type="const">($grabProductMapPrice)</actualResult> + <expectedResult type="string">€100.00</expectedResult> + </assertNotEquals> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml index bbd5dbd8068f7..d9ad32df872f7 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml @@ -37,7 +37,7 @@ </actionGroup> <!--Add custom option to configurable product--> <actionGroup ref="AddProductCustomOptionFileActionGroup" stepKey="addCustomOptionToProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!--Go to storefront--> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml index 7662779a6955f..3519503c1e287 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml @@ -112,7 +112,9 @@ <argument name="categoryName" value="$$secondCategory.name$$"/> </actionGroup> - <magentoCLI command="indexer:reindex" arguments="catalogsearch_fulltext" stepKey="reindexSearchIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexSearchIndex"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> <!-- Go to storefront to view child product --> <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToSecondCategoryStorefront"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml index ef9f71da0ebca..363a8ea4d4fd6 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -153,8 +153,12 @@ <argument name="discountAmount" value="{{CatalogRuleByPercentWith96Amount.discount_amount}}"/> </actionGroup> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <magentoCLI command="indexer:reindex" arguments="catalogsearch_fulltext catalog_category_product catalog_product_price catalogrule_rule" stepKey="reindexIndices"/> - <magentoCLI command="cache:clean" arguments="full_page" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexIndices"> + <argument name="indices" value="catalogsearch_fulltext catalog_category_product catalog_product_price catalogrule_rule"/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="fullCache"> + <argument name="tags" value="full_page"/> + </actionGroup> <!--Reopen category with products and Sort by price desc--> <actionGroup ref="GoToStorefrontCategoryPageByParametersActionGroup" stepKey="goToStorefrontCategoryPage2"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml index 7acece767760d..9b046d5c71cfc 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml @@ -123,8 +123,7 @@ </after> <!-- Open Product Index Page and Filter First Child product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="ApiSimpleOne"/> </actionGroup> @@ -134,12 +133,13 @@ <waitForPageLoad stepKey="waitForProductPageToLoad"/> <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToProductQuantity"/> <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="disableProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Open Category in Store Front and select product attribute option from sidebar --> <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index 801dfdb8540e8..976be77122547 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -190,8 +190,12 @@ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateConfigsForDuplicatedProduct"/> <waitForPageLoad stepKey="waitForDuplicatedProductPageLoad"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveDuplicatedProduct"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Assert configurable product in category--> <comment userInput="Assert configurable product in category" stepKey="commentAssertProductInCategoryPage"/> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage"/> 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/Plugin/Frontend/ProductIdentitiesExtenderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php deleted file mode 100644 index f96da3a7967bf..0000000000000 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\ConfigurableProduct\Test\Unit\Model\Plugin\Frontend; - -use Magento\Catalog\Model\Product; -use Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use PHPUnit\Framework\TestCase; - -class ProductIdentitiesExtenderTest extends TestCase -{ - /** - * @var \PHPUnit\Framework\MockObject\MockObject|Configurable - */ - private $configurableTypeMock; - - /** - * @var ProductIdentitiesExtender - */ - private $plugin; - - /** @var MockObject|Product */ - private $product; - - protected function setUp(): void - { - $this->product = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->setMethods(['getId']) - ->getMock(); - - $this->configurableTypeMock = $this->getMockBuilder(Configurable::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->plugin = new ProductIdentitiesExtender($this->configurableTypeMock); - } - - public function testAfterGetIdentities() - { - $identities = [ - 'SomeCacheId', - 'AnotherCacheId', - ]; - $productId = 12345; - $childIdentities = [ - 0 => [1, 2, 5, 100500] - ]; - $expectedIdentities = [ - 'SomeCacheId', - 'AnotherCacheId', - Product::CACHE_TAG . '_' . 1, - Product::CACHE_TAG . '_' . 2, - Product::CACHE_TAG . '_' . 5, - Product::CACHE_TAG . '_' . 100500, - ]; - - $this->product->expects($this->once()) - ->method('getId') - ->willReturn($productId); - - $this->configurableTypeMock->expects($this->once()) - ->method('getChildrenIds') - ->with($productId) - ->willReturn($childIdentities); - - $productIdentities = $this->plugin->afterGetIdentities($this->product, $identities); - $this->assertEquals($expectedIdentities, $productIdentities); - } -} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php index a79c2ebbceca9..07b4a1faf3db4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\ConfigurableProduct\Test\Unit\Model\Plugin; @@ -18,6 +19,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Exception\InputException; /** * Test for ProductRepositorySave plugin @@ -71,7 +73,8 @@ class ProductRepositorySaveTest extends TestCase */ protected function setUp(): void { - $this->productAttributeRepository = $this->getMockForAbstractClass(ProductAttributeRepositoryInterface::class); + $this->productAttributeRepository = + $this->getMockForAbstractClass(ProductAttributeRepositoryInterface::class); $this->product = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() @@ -105,8 +108,10 @@ protected function setUp(): void /** * Validating the result after saving a configurable product + * + * @return void */ - public function testBeforeSaveWhenProductIsSimple() + public function testBeforeSaveWhenProductIsSimple(): void { $this->product->expects(static::once()) ->method('getTypeId') @@ -122,8 +127,10 @@ public function testBeforeSaveWhenProductIsSimple() /** * Test saving a configurable product without attribute options + * + * @return void */ - public function testBeforeSaveWithoutOptions() + public function testBeforeSaveWithoutOptions(): void { $this->product->expects(static::once()) ->method('getTypeId') @@ -151,10 +158,12 @@ public function testBeforeSaveWithoutOptions() /** * Test saving a configurable product with same set of attribute values + * + * @return void */ - public function testBeforeSaveWithLinks() + public function testBeforeSaveWithLinks(): void { - $this->expectException('Magento\Framework\Exception\InputException'); + $this->expectException(InputException::class); $this->expectExceptionMessage('Products "5" and "4" have the same set of attribute values.'); $links = [4, 5]; $this->product->expects(static::once()) @@ -191,10 +200,12 @@ public function testBeforeSaveWithLinks() /** * Test saving a configurable product with missing attribute + * + * @return void */ - public function testBeforeSaveWithLinksWithMissingAttribute() + public function testBeforeSaveWithLinksWithMissingAttribute(): void { - $this->expectException('Magento\Framework\Exception\InputException'); + $this->expectException(InputException::class); $this->expectExceptionMessage('Product with id "4" does not contain required attribute "color".'); $simpleProductId = 4; $links = [$simpleProductId, 5]; @@ -239,17 +250,19 @@ public function testBeforeSaveWithLinksWithMissingAttribute() $product->expects(static::once()) ->method('getData') ->with($attributeCode) - ->willReturn(false); + ->willReturn(null); $this->plugin->beforeSave($this->productRepository, $this->product); } /** * Test saving a configurable product with duplicate attributes + * + * @return void */ - public function testBeforeSaveWithLinksWithDuplicateAttributes() + public function testBeforeSaveWithLinksWithDuplicateAttributes(): void { - $this->expectException('Magento\Framework\Exception\InputException'); + $this->expectException(InputException::class); $this->expectExceptionMessage('Products "5" and "4" have the same set of attribute values.'); $links = [4, 5]; $attributeCode = 'color'; 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/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php index abab103fa6d37..3d5a0d1cc6a3f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php @@ -7,20 +7,38 @@ namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Model\ResourceModel; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product as ModelProduct; use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductAttributeSearchResults; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; +use Magento\Catalog\Model\ResourceModel\Product as ResourceModelProduct; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as ConfigurableAttribute; +use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product as PluginResourceModelProduct; +use Magento\Framework\Api\ExtensionAttributesInterface; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Indexer\ActionInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ProductTest extends TestCase { /** - * @var ObjectManager + * @var PluginResourceModelProduct */ - private $objectManager; + private $model; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; /** * @var Configurable|MockObject @@ -33,39 +51,128 @@ class ProductTest extends TestCase private $actionMock; /** - * @var Product + * @var ProductAttributeRepositoryInterface|MockObject */ - private $model; + private $productAttributeRepositoryMock; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private $searchCriteriaBuilderMock; + + /** + * @var FilterBuilder|MockObject + */ + private $filterBuilderMock; protected function setUp(): void { - $this->objectManager = new ObjectManager($this); $this->configurableMock = $this->createMock(Configurable::class); $this->actionMock = $this->getMockForAbstractClass(ActionInterface::class); - - $this->model = $this->objectManager->getObject( - Product::class, + $this->productAttributeRepositoryMock = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getList']) + ->getMockForAbstractClass(); + $this->searchCriteriaBuilderMock = $this->createPartialMock( + SearchCriteriaBuilder::class, + ['addFilters', 'create'] + ); + $this->filterBuilderMock = $this->createPartialMock( + FilterBuilder::class, + ['setField', 'setConditionType', 'setValue', 'create'] + ); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $this->objectManagerHelper->getObject( + PluginResourceModelProduct::class, [ 'configurable' => $this->configurableMock, 'productIndexer' => $this->actionMock, + 'productAttributeRepository' => $this->productAttributeRepositoryMock, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + 'filterBuilder' => $this->filterBuilderMock ] ); } - public function testBeforeSaveConfigurable() + public function testBeforeSaveConfigurable(): void { - /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */ - $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - /** @var \Magento\Catalog\Model\Product|MockObject $object */ - $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']); + /** @var ResourceModelProduct|MockObject $subject */ + $subject = $this->createMock(ResourceModelProduct::class); + /** @var ModelProduct|MockObject $object */ + $object = $this->createPartialMock( + ModelProduct::class, + [ + 'getTypeId', + 'getTypeInstance', + 'getExtensionAttributes', + 'setData' + ] + ); $type = $this->createPartialMock( Configurable::class, ['getSetAttributes'] ); - $type->expects($this->once())->method('getSetAttributes')->with($object); - - $object->expects($this->once())->method('getTypeId')->willReturn(Configurable::TYPE_CODE); - $object->expects($this->once())->method('getTypeInstance')->willReturn($type); + $extensionAttributes = $this->getMockBuilder(ExtensionAttributesInterface::class) + ->disableOriginalConstructor() + ->addMethods(['getConfigurableProductOptions']) + ->getMock(); + $option = $this->createPartialMock( + ConfigurableAttribute::class, + ['getAttributeId'] + ); + $extensionAttributes->expects($this->exactly(2)) + ->method('getConfigurableProductOptions') + ->willReturn([$option]); + $object->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($extensionAttributes); + + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('setField') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('setValue') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('setConditionType') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturnSelf(); + $searchCriteria = $this->createMock(SearchCriteria::class); + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('create') + ->willReturn($searchCriteria); + $searchResultMockClass = $this->createPartialMock( + ProductAttributeSearchResults::class, + ['getItems'] + ); + $this->productAttributeRepositoryMock->expects($this->once()) + ->method('getList') + ->with($searchCriteria) + ->willReturn($searchResultMockClass); + $optionAttribute = $this->createPartialMock( + EavAttribute::class, + ['getAttributeCode'] + ); + $searchResultMockClass->expects($this->once()) + ->method('getItems') + ->willReturn([$optionAttribute]); + $type->expects($this->once()) + ->method('getSetAttributes') + ->with($object); + $object->expects($this->once()) + ->method('getTypeId') + ->will($this->returnValue(Configurable::TYPE_CODE)); + $object->expects($this->once()) + ->method('getTypeInstance') + ->will($this->returnValue($type)); + $object->expects($this->once()) + ->method('setData'); + $option->expects($this->once()) + ->method('getAttributeId'); + $optionAttribute->expects($this->once()) + ->method('getAttributeCode'); $this->model->beforeSave( $subject, @@ -73,14 +180,23 @@ public function testBeforeSaveConfigurable() ); } - public function testBeforeSaveSimple() + public function testBeforeSaveSimple(): void { - /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */ - $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - /** @var \Magento\Catalog\Model\Product|MockObject $object */ - $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']); - $object->expects($this->once())->method('getTypeId')->willReturn(Type::TYPE_SIMPLE); - $object->expects($this->never())->method('getTypeInstance'); + /** @var ResourceModelProduct|MockObject$subject */ + $subject = $this->createMock(ResourceModelProduct::class); + /** @var ModelProduct|MockObject $object */ + $object = $this->createPartialMock( + ModelProduct::class, + [ + 'getTypeId', + 'getTypeInstance' + ] + ); + $object->expects($this->once()) + ->method('getTypeId') + ->will($this->returnValue(Type::TYPE_SIMPLE)); + $object->expects($this->never()) + ->method('getTypeInstance'); $this->model->beforeSave( $subject, @@ -88,29 +204,35 @@ public function testBeforeSaveSimple() ); } - public function testAroundDelete() + public function testAroundDelete(): void { $productId = '1'; $parentConfigId = ['2']; - /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */ - $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - /** @var \Magento\Catalog\Model\Product|MockObject $product */ + /** @var ResourceModelProduct|MockObject $subject */ + $subject = $this->createMock(ResourceModelProduct::class); + /** @var ModelProduct|MockObject $product */ $product = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, + ModelProduct::class, ['getId', 'delete'] ); - $product->expects($this->once())->method('getId')->willReturn($productId); - $product->expects($this->once())->method('delete')->willReturn(true); + $product->expects($this->once()) + ->method('getId') + ->willReturn($productId); + $product->expects($this->once()) + ->method('delete') + ->willReturn(true); $this->configurableMock->expects($this->once()) ->method('getParentIdsByChild') ->with($productId) ->willReturn($parentConfigId); - $this->actionMock->expects($this->once())->method('executeList')->with($parentConfigId); + $this->actionMock->expects($this->once()) + ->method('executeList') + ->with($parentConfigId); $return = $this->model->aroundDelete( $subject, - /** @var \Magento\Catalog\Model\Product|MockObject $prod */ - function (\Magento\Catalog\Model\Product $prod) use ($subject) { + /** @var ModelProduct|MockObject $prod */ + function (ModelProduct $prod) use ($subject) { $prod->delete(); return $subject; }, diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php index 0a014b9aeef99..bb79c13bba82a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php @@ -64,7 +64,7 @@ class CleanConfigurationTmpImagesTest extends TestCase /** * @var Json|MockObject */ - private $seralizer; + private $serializer; /** * @var ProductInitializationHelper|MockObject @@ -87,7 +87,7 @@ protected function setUp(): void $this->writeFolder = $this->getMockBuilder(Write::class) ->disableOriginalConstructor() ->getMock(); - $this->seralizer = $this->getMockBuilder(Json::class) + $this->serializer = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->getMock(); $this->subjectMock = $this->getMockBuilder(ProductInitializationHelper::class) @@ -106,7 +106,7 @@ protected function setUp(): void 'fileStorageDb' => $this->fileStorageDb, 'mediaConfig' => $this->mediaConfig, 'filesystem' => $this->filesystem, - 'seralizer' => $this->seralizer + 'serializer' => $this->serializer ] ); } diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml index b2d50f54f5334..f60234453dc60 100644 --- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml @@ -10,9 +10,6 @@ <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> </type> - <type name="Magento\Catalog\Model\Product"> - <plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender" /> - </type> <type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable"> <plugin name="used_products_cache" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" /> </type> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml b/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml index a084abfc31eaa..ffd17a8bf4734 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml @@ -48,6 +48,7 @@ <item name="modal" xsi:type="string">configurableModal</item> <item name="dataScope" xsi:type="string">productFormConfigurable</item> </argument> + <argument name="permissions" xsi:type="object">Magento\ConfigurableProduct\Block\DataProviders\PermissionsData</argument> </arguments> </block> <block class="Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\Bulk" name="step3" template="Magento_ConfigurableProduct::catalog/product/edit/attribute/steps/bulk.phtml"> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml index 9307da21e6659..4ad7a6419ca63 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml @@ -4,11 +4,13 @@ * See COPYING.txt for license details. */ /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Attribute\NewAttribute\Product\Created */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $attributes = /* @noEscape */ $block->getAttributesBlockJson(); +$scriptString = <<<script (function ($) { - var data = <?= /* @noEscape */ $block->getAttributesBlockJson() ?>; + var data = {$attributes}; var set = data.set || {id: $('#attribute_set_id').val()}; if (data.tab == 'variations') { $('[data-role=product-variations-matrix]').trigger('add', data.attribute); @@ -18,4 +20,6 @@ $('#create_new_attribute').modal('closeModal'); })(window.parent.jQuery); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml index 5f49d5eb47442..272234a0ee074 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml @@ -5,8 +5,11 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script + require([ "Magento_Ui/js/modal/alert", "tree-panel" @@ -27,7 +30,10 @@ editSet.submit = editSet.submit.wrap(function(original) { if (editSet.currentNode){ if (ConfigurableNodeExists(editSet.currentNode)) { alert({ - content: '<?= $block->escapeJs(__('This group contains attributes used in configurable products. Please move these attributes to another group and try again.')) ?>' + content: '{$block->escapeJs( + __('This group contains attributes used in configurable products. ' . + 'Please move these attributes to another group and try again.') + )}' }); return; } @@ -38,7 +44,9 @@ editSet.submit = editSet.submit.wrap(function(original) { editSet.rightBeforeAppend = editSet.rightBeforeAppend.wrap(function(original, tree, nodeThis, node, newParent) { if (node.attributes.is_configurable == 1) { alert({ - content: '<?= $block->escapeJs(__('This attribute is used in configurable products. You cannot remove it from the attribute set.')) ?>' + content: '{$block->escapeJs( + __('This attribute is used in configurable products. You cannot remove it from the attribute set.') + )}' }); return false; } @@ -48,7 +56,9 @@ editSet.rightBeforeAppend = editSet.rightBeforeAppend.wrap(function(original, tr editSet.rightBeforeInsert = editSet.rightBeforeInsert.wrap(function(original, tree, nodeThis, node, newParent) { if (node.attributes.is_configurable == 1) { alert({ - content: '<?= $block->escapeJs(__('This attribute is used in configurable products. You cannot remove it from the attribute set.')) ?>' + content: '{$block->escapeJs( + __('This attribute is used in configurable products. You cannot remove it from the attribute set.') + )}' }); return false; } @@ -56,4 +66,6 @@ editSet.rightBeforeInsert = editSet.rightBeforeInsert.wrap(function(original, tr }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml index 844422b2a2d7a..a46d50176369a 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml @@ -3,26 +3,30 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset\Configurable */ ?> +/* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset\Configurable */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> <?php $_product = $block->getProduct(); ?> <?php $_attributes = $block->decorateArray($block->getAllowAttributes()); ?> -<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> -<?php if (($_product->isSaleable() || $_skipSaleableCheck) && count($_attributes)) :?> +<?php +/** @var \Magento\Catalog\Helper\Product $productHelper */ +$productHelper = $block->getData('productHelper'); +?> +<?php $_skipSaleableCheck = $productHelper->getSkipSaleableCheck(); ?> +<?php if (($_product->isSaleable() || $_skipSaleableCheck) && count($_attributes)):?> <fieldset id="catalog_product_composite_configure_fields_configurable" class="fieldset admin__fieldset"> <legend class="legend admin__legend"> <span><?= $block->escapeHtml(__('Associated Products')) ?></span> </legend> <div class="product-options fieldset admin__fieldset"> - <?php foreach ($_attributes as $_attribute) : ?> + <?php foreach ($_attributes as $_attribute): ?> <div class="field admin__field required"> <label class="label admin__field-label"><?= $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel($_product->getStoreId())); ?></label> <div class="control admin__field-control <?php - if ($_attribute->getDecoratedIsLast()) : + if ($_attribute->getDecoratedIsLast()): ?> last<?php endif; ?>"> <select name="super_attribute[<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>]" @@ -35,13 +39,14 @@ <?php endforeach; ?> </div> </fieldset> -<script> + <?php $config = /* @noEscape */ $block->getJsonConfig(); + $scriptString = <<<script require([ "Magento_ConfigurableProduct/js/configurable", "Magento_Catalog/catalog/product/composite/configure" ], function(){ - var config = <?= /* @noEscape */ $block->getJsonConfig() ?>; + var config = {$config}; if (window.productConfigure) { config.containerId = window.productConfigure.blockFormFields.id; if (window.productConfigure.restorePhase) { @@ -52,5 +57,7 @@ require([ ProductConfigure.spConfig = new Product.Config(config); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml index e996df8260719..e94d94e0ded55 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml @@ -5,6 +5,9 @@ */ /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\AttributeValues */ +$isAllowedToManageAttributes = $block->getPermissions()->isAllowedToManageAttributes(); +$attributesUrl = $block->getUrl('catalog/product_attribute/getAttributes'); +$optionsUrl = $block->getUrl('catalog/product_attribute/createOptions'); ?> <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'"> <h2 class="steps-wizard-title"><?= $block->escapeHtml( @@ -12,7 +15,8 @@ ); ?></h2> <div class="steps-wizard-info"> <span><?= $block->escapeHtml( - __('Select values from each attribute to include in this product. Each unique combination of values creates a unique product SKU.') + __('Select values from each attribute to include in this product. ' . + 'Each unique combination of values creates a unique product SKU.') );?></span> </div> <div data-bind="foreach: attributes, sortableList: attributes"> @@ -72,7 +76,8 @@ <label data-bind="text: label, visible: label, attr:{for:id}" class="admin__field-label"></label> </div> - <div class="admin__field admin__field-create-new" data-bind="attr:{'data-role':id}, visible: !label"> + <div class="admin__field admin__field-create-new" + data-bind="attr:{'data-role':id}, visible: !label"> <div class="admin__field-control"> <input class="admin__control-text" name="label" @@ -101,14 +106,14 @@ </li> </ul> </fieldset> - <button class="action-create-new action-tertiary" - type="button" - data-action="addOption" - data-bind="click: $parent.createOption, visible: canCreateOption"> - <span><?= $block->escapeHtml( - __('Create New Value') - ); ?></span> - </button> + <?php if ($isAllowedToManageAttributes): ?> + <button class="action-create-new action-tertiary" + type="button" + data-action="addOption" + data-bind="click: $parent.createOption, visible: canCreateOption"> + <span><?= $block->escapeHtml(__('Create New Value')); ?></span> + </button> + <?php endif; ?> </div> </div> </div> @@ -120,8 +125,8 @@ "<?= /* @noEscape */ $block->getComponentName() ?>": { "component": "Magento_ConfigurableProduct/js/variations/steps/attributes_values", "appendTo": "<?= /* @noEscape */ $block->getParentComponentName() ?>", - "optionsUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product_attribute/getAttributes') ?>", - "createOptionsUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product_attribute/createOptions') ?>" + "optionsUrl": "<?= /* @noEscape */ $attributesUrl ?>", + "createOptionsUrl": "<?= /* @noEscape */ $optionsUrl ?>" } } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml index a792a35da8051..6cd930978c85f 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml @@ -3,20 +3,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\Bulk */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +?> <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'" data-role="bulk-step"> <h2 class="steps-wizard-title"><?= $block->escapeHtml(__('Step 3: Bulk Images, Price and Quantity')) ?></h2> <div class="steps-wizard-info"> - <?= /* @noEscape */ __('Based on your selections %1 new products will be created. Use this step to customize images and price for your new products.', '<span class="new-products-count" data-bind="text:countVariations"></span>') ?> + <?= /* @noEscape */ __( + 'Based on your selections %1 new products will be created. ' . + 'Use this step to customize images and price for your new products.', + '<span class="new-products-count" data-bind="text:countVariations"></span>' + ) ?> </div> <div data-bind="with: sections().images" class="steps-wizard-section"> <div data-role="section"> <div class="steps-wizard-section-title"> - <span><?= $block->escapeHtml(__('Images')); ?></span> + <span><?= $block->escapeHtml(__('Images')); ?></span> </div> <ul class="steps-wizard-section-list"> @@ -65,7 +74,9 @@ <div data-role="gallery" class="gallery" data-images="[]" - data-types="<?= $block->escapeHtml($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes())) ?>"> + data-types="<?= $block->escapeHtmlAttr($jsonHelper->jsonEncode( + $block->getImageTypes() + )) ?>"> <div class="image image-placeholder"> <div data-role="uploader" class="uploader"> <div class="image-browse"> @@ -75,32 +86,39 @@ name="image" class="admin__control-file" multiple="multiple" - data-url="<?= /* @noEscape */ $block->getUrl('catalog/product_gallery/upload') ?>" /> + data-url="<?= /* @noEscape */ $block->getUrl('catalog/product_gallery/upload') + ?>" /> </div> </div> <div class="product-image-wrapper"> - <p class="image-placeholder-text"><?= $block->escapeHtml(__('Browse to find or drag image here')) ?></p> + <p class="image-placeholder-text"><?= $block->escapeHtml(__( + 'Browse to find or drag image here' + )) ?></p> </div> </div> - <?php foreach ($block->getImageTypes() as $typeData) : ?> + <?php foreach ($block->getImageTypes() as $typeData): ?> <input name="<?= $block->escapeHtml($typeData['name']) ?>" class="image-<?= $block->escapeHtml($typeData['code']) ?>" type="hidden" value="<?= $block->escapeHtml($typeData['value']) ?>"/> - <?php endforeach; ?> + <?php endforeach; ?> <script data-template="uploader" type="text/x-magento-template"> <div id="<%- data.id %>" class="file-row"> <span class="file-info"><%- data.name %> (<%- data.size %>)</span> <div class="progressbar-container"> - <div class="progressbar upload-progress" style="width: 0%;"></div> + <div class="progressbar upload-progress"></div> </div> <div class="spinner"> <span></span><span></span><span></span><span></span> <span></span><span></span><span></span><span></span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width: 0%;", + "div.progressbar-container div.progressbar.upload-progress" + ) ?> </script> <script data-template="gallery-content" type="text/x-magento-template"> @@ -124,7 +142,8 @@ <input type="hidden" name="product[media_gallery][images][<%- data.file_id %>][removed]"/> <div class="product-image-wrapper"> - <img class="product-image" data-role="image-element" src="<%- data.url %>" alt="<%- data.label %>"/> + <img class="product-image" data-role="image-element" src="<%- data.url %>" + alt="<%- data.label %>"/> <div class="actions"> <button type="button" class="action-remove" @@ -139,16 +158,18 @@ <div class="item-description"> <div class="item-title" data-role="img-title"><%- data.label %></div> <div class="item-size"> - <span data-role="image-dimens"></span>, <span data-role="image-size"><%- data.sizeLabel %></span> + <span data-role="image-dimens"></span>, + <span data-role="image-size"><%- data.sizeLabel %></span> </div> </div> <ul class="item-roles" data-role="roles-labels"> - <?php foreach ($block->getMediaAttributes() as $attribute) :?> + <?php foreach ($block->getMediaAttributes() as $attribute):?> <li data-role-code="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" - class="item-role item-role-<?= $block->escapeHtml($attribute->getAttributeCode()) ?>"> + class="item-role item-role-<?= + $block->escapeHtml($attribute->getAttributeCode()) ?>"> <?= /* @noEscape */ $attribute->getFrontendLabel() ?> </li> - <?php endforeach; ?> + <?php endforeach; ?> </ul> </div> </script> @@ -210,40 +231,44 @@ <div class="admin__field field-image-role"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Role')); ?></span> + <span><?= $block->escapeHtml(__('Role')); ?></span> </label> <div class="admin__field-control"> <ul class="multiselect-alt"> <?php - foreach ($block->getMediaAttributes() as $attribute) : + foreach ($block->getMediaAttributes() as $attribute): ?> <li class="item"> <label> <input class="image-type" data-role="type-selector" type="checkbox" - value="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" + value="<?= + $block->escapeHtml($attribute->getAttributeCode()) ?>" /> <?= $block->escapeHtml($attribute->getFrontendLabel()); ?> </label> </li> - <?php endforeach; ?> + <?php endforeach; ?> </ul> </div> </div> <div class="admin__field admin__field-inline field-image-size" data-role="size"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Image Size')); ?></span> + <span><?= $block->escapeHtml(__('Image Size')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{size}'));?>"></div> + <div class="admin__field-value" + data-message="<?= $block->escapeHtml(__('{size}'));?>"></div> </div> - <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> + <div class="admin__field admin__field-inline field-image-resolution" + data-role="resolution"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> + <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{width}^{height} px'));?>"></div> + <div class="admin__field-value" + data-message="<?= $block->escapeHtml(__('{width}^{height} px'));?>"></div> </div> <div class="admin__field field-image-hide"> @@ -273,7 +298,7 @@ <fieldset class="admin__fieldset bulk-attribute-values"> <div class="admin__field _required"> <label class="admin__field-label" for="apply-images-attributes"> - <span><?= $block->escapeHtml(__('Select attribute')); ?></span> + <span><?= $block->escapeHtml(__('Select attribute')); ?></span> </label> <div class="admin__field-control"> <select @@ -300,17 +325,22 @@ <div data-role="gallery" class="gallery" data-images="[]" - data-types="<?= $block->escapeHtml($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes())) ?>"> + data-types="<?= $block->escapeHtmlAttr( + $jsonHelper->jsonEncode($block->getImageTypes()) + ) ?>"> <div class="image image-placeholder"> <div data-role="uploader" class="uploader"> <div class="image-browse"> - <span><?= $block->escapeHtml(__('Browse Files...')); ?></span> + <span><?= $block->escapeHtml(__('Browse Files...')); ?></span> <input type="file" name="image" multiple="multiple" - data-url="<?= /* @noEscape */ $block->getUrl('catalog/product_gallery/upload') ?>" /> + data-url="<?= + /* @noEscape */ $block->getUrl('catalog/product_gallery/upload') ?>" /> </div> </div> <div class="product-image-wrapper"> - <p class="image-placeholder-text"><?= $block->escapeHtml(__('Browse to find or drag image here')); ?></p> + <p class="image-placeholder-text"> + <?= $block->escapeHtml(__('Browse to find or drag image here')); ?> + </p> </div> <div class="spinner"> <span></span><span></span><span></span><span></span> @@ -318,24 +348,28 @@ </div> </div> - <?php foreach ($block->getImageTypes() as $typeData) :?> + <?php foreach ($block->getImageTypes() as $typeData): ?> <input name="<?= $block->escapeHtml($typeData['name']) ?>" class="image-<?= $block->escapeHtml($typeData['code']) ?>" type="hidden" value="<?= $block->escapeHtml($typeData['value']) ?>"/> - <?php endforeach; ?> + <?php endforeach; ?> <script data-template="uploader" type="text/x-magento-template"> <div id="<%- data.id %>" class="file-row"> <span class="file-info"><%- data.name %> (<%- data.size %>)</span> <div class="progressbar-container"> - <div class="progressbar upload-progress" style="width: 0%;"></div> + <div class="progressbar upload-progress"></div> </div> <div class="spinner"> <span></span><span></span><span></span><span></span> <span></span><span></span><span></span><span></span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width: 0%;", + "div.progressbar-container div.progressbar.upload-progress" + ) ?> </script> <script data-template="gallery-content" type="text/x-magento-template"> @@ -361,31 +395,36 @@ value="" class="is-removed"/> <div class="product-image-wrapper"> - <img class="product-image" data-role="image-element" src="<%- data.url %>" alt="<%- data.label %>"/> + <img class="product-image" data-role="image-element" src="<%- data.url %>" + alt="<%- data.label %>"/> <div class="actions"> <button type="button" class="action-remove" data-role="delete-button" title="<?= $block->escapeHtml(__('Remove image')) ?>"> - <span><?= $block->escapeHtml(__('Remove image')); ?></span> + <span><?= $block->escapeHtml(__('Remove image')); ?></span> </button> <div class="draggable-handle"></div> </div> - <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')); ?></span></div> + <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')); ?></span> + </div> </div> <div class="item-description"> <div class="item-title" data-role="img-title"><%- data.label %></div> <div class="item-size"> - <span data-role="image-dimens"></span>, <span data-role="image-size"><%- data.sizeLabel %></span> + <span data-role="image-dimens"></span>, + <span data-role="image-size"><%- data.sizeLabel %></span> </div> </div> <ul class="item-roles" data-role="roles-labels"> - <?php foreach ($block->getMediaAttributes() as $attribute) :?> - <li data-role-code="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" - class="item-role item-role-<?= $block->escapeHtml($attribute->getAttributeCode()) ?>"> + <?php foreach ($block->getMediaAttributes() as $attribute):?> + <li data-role-code="<?= $block->escapeHtml($attribute->getAttributeCode()) + ?>" + class="item-role item-role-<?= + $block->escapeHtml($attribute->getAttributeCode()) ?>"> <?= $block->escapeHtml($attribute->getFrontendLabel()) ?> </li> - <?php endforeach; ?> + <?php endforeach; ?> </ul> </div> </script> @@ -407,7 +446,8 @@ </button> <div class="draggable-handle"></div> </div> - <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')) ?></span></div> + <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')) ?></span> + </div> </div> <!--<ul class="item-roles"> <li class="item-role item-role-base">Base</li> @@ -416,7 +456,8 @@ </script> <script data-role="img-dialog-container-tmpl" type="text/x-magento-template"> - <div class="image-panel ui-tabs-panel ui-widget-content ui-corner-bottom" data-role="dialog"> + <div class="image-panel ui-tabs-panel ui-widget-content ui-corner-bottom" + data-role="dialog"> </div> </script> @@ -430,7 +471,7 @@ <fieldset class="admin__fieldset fieldset-image-panel"> <div class="admin__field field-image-description"> <label class="admin__field-label" for="image-description"> - <span><?= $block->escapeHtml(__('Alt Text'));?></span> + <span><?= $block->escapeHtml(__('Alt Text'));?></span> </label> <div class="admin__field-control"> @@ -444,38 +485,43 @@ <div class="admin__field field-image-role"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Role'));?></span> + <span><?= $block->escapeHtml(__('Role'));?></span> </label> <div class="admin__field-control"> <ul class="multiselect-alt"> - <?php foreach ($block->getMediaAttributes() as $attribute) :?> + <?php foreach ($block->getMediaAttributes() as $attribute):?> <li class="item"> <label> <input class="image-type" data-role="type-selector" type="checkbox" - value="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" + // @codingStandardsIgnoreLine + value="<?= $block->escapeHtmlAttr($attribute->getAttributeCode()) ?>" /> - <?= $block->escapeHtml($attribute->getFrontendLabel()) ?> + <?= $block->escapeHtml($attribute->getFrontendLabel())?> </label> </li> - <?php endforeach; ?> + <?php endforeach; ?> </ul> </div> </div> <div class="admin__field admin__field-inline field-image-size" data-role="size"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Image Size')); ?></span> + <span><?= $block->escapeHtml(__('Image Size')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{size}')); ?>"></div> + <div class="admin__field-value" + data-message="<?= $block->escapeHtml(__('{size}')); ?>"></div> </div> - <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> + <div class="admin__field admin__field-inline field-image-resolution" + data-role="resolution"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> + <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{width}^{height} px')); ?>"></div> + <div class="admin__field-value" + data-message="<?= $block->escapeHtml(__('{width}^{height} px')); ?>"> + </div> </div> <div class="admin__field field-image-hide"> @@ -486,7 +532,8 @@ data-role="visibility-trigger" value="1" class="admin__control-checkbox" - name="product[media_gallery][images][<%- data.file_id %>][disabled]" + // @codingStandardsIgnoreLine + name="product[media_gallery][images][<%- data.file_id %>][disabled]" <% if (data.disabled == 1) { %>checked="checked"<% } %> /> <label for="hide-from-product-page" class="admin__field-label"> @@ -556,7 +603,7 @@ <fieldset class="admin__fieldset bulk-attribute-values" data-bind="visible: type() == 'single'"> <div class="admin__field _required"> <label for="apply-single-price-input" class="admin__field-label"> - <span><?= $block->escapeHtml(__('Price')); ?></span> + <span><?= $block->escapeHtml(__('Price')); ?></span> </label> <div class="admin__field-control"> <div class="currency-addon"> @@ -589,7 +636,8 @@ <fieldset class="admin__fieldset bulk-attribute-values" data-bind="if:attribute"> <!-- ko foreach: attribute().chosen --> <div class="admin__field _required"> - <label data-bind="attr: {for: 'apply-single-price-input-' + $index()}" class="admin__field-label"> + <label data-bind="attr: {for: 'apply-single-price-input-' + $index()}" + class="admin__field-label"> <span data-bind="text:label"></span> </label> <div class="admin__field-control"> @@ -717,7 +765,7 @@ "component": "Magento_ConfigurableProduct/js/variations/steps/bulk", "appendTo": "<?= /* @noEscape */ $block->getParentComponentName() ?>", "noImage": "<?= /* @noEscape */ $block->getNoImageUrl() ?>", - "variationsComponent": "<?= /* @noEscape */ $block->getData('config/form') ?>.configurableVariations" + "variationsComponent": "<?= /* @noEscape */ $block->getData('config/form')?>.configurableVariations" } } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml index c3dc614232201..92fae99f6ec24 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml @@ -5,8 +5,10 @@ */ /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\SelectAttributes */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div class="select-attributes-block <?= /* @noEscape */ $block->getData('config/dataScope') ?>" data-role="select-attributes-step"> +<div class="select-attributes-block <?= /* @noEscape */ $block->getData('config/dataScope') ?>" + data-role="select-attributes-step"> <div class="select-attributes-actions" data-type="skipKO"> <?= /* @noEscape */ $block->getAddNewAttributeButton() ?> </div> @@ -37,9 +39,12 @@ } } </script> -<script> +<?php $dataScope = /* @noEscape */ $block->getData('config/dataScope'); +$scriptString = <<<script require(['jquery'], function ($) { - $('.<?= /* @noEscape */ $block->getData('config/dataScope') ?>[data-role=select-attributes-step]').applyBindings(); + $('.{$dataScope}[data-role=select-attributes-step]').applyBindings(); $('body').trigger('contentUpdated'); }) -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml index 22ff1992c94a7..73067fdee3b84 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $productMatrix = $block->getProductMatrix(); $attributes = $block->getProductAttributes(); $currencySymbol = $block->getCurrencySymbol(); + +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <div id="product-variations-matrix" data-role="product-variations-matrix"> @@ -26,15 +30,25 @@ $currencySymbol = $block->getCurrencySymbol(); <div data-role="configurable-attributes-container"> <!-- ko foreach: {data: attributes, as: 'attribute'} --> <div data-role="attribute-info"> - <input name="attributes[]" data-bind="value: attribute.id, attr:{id: 'configurable_attribute_' + attribute.id}" type="hidden"/> - <input data-bind="value: attribute.id, attr: {name: $parent.getAttributeRowName(attribute, 'attribute_id')}" type="hidden"/> - <input data-bind="value: attribute.code, attr: {name: $parent.getAttributeRowName(attribute, 'code')}" type="hidden"/> - <input data-bind="value: attribute.label, attr: {name: $parent.getAttributeRowName(attribute, 'label')}" type="hidden"/> - <input data-bind="value: $index(), attr: {name: $parent.getAttributeRowName(attribute, 'position')}" type="hidden"/> + <input name="attributes[]" + data-bind="value: attribute.id, attr:{id: 'configurable_attribute_' + attribute.id}" + type="hidden"/> + <input data-bind="value: attribute.id, + attr: {name: $parent.getAttributeRowName(attribute, 'attribute_id')}" type="hidden"/> + <input data-bind="value: attribute.code, + attr: {name: $parent.getAttributeRowName(attribute, 'code')}" type="hidden"/> + <input data-bind="value: attribute.label, + attr: {name: $parent.getAttributeRowName(attribute, 'label')}" type="hidden"/> + <input data-bind="value: $index(), + attr: {name: $parent.getAttributeRowName(attribute, 'position')}" type="hidden"/> <!-- ko foreach: {data: attribute.chosen, as: 'option'} --> <div data-role="option-info"> - <input value="1" data-bind="attr: {name: $parents[1].getOptionRowName(attribute, option, 'include')}" type="hidden"/> - <input data-bind="value: option.value, attr: {name: $parents[1].getOptionRowName(attribute, option, 'value_index')}" type="hidden"/> + <input value="1" + data-bind="attr: {name: $parents[1].getOptionRowName(attribute, option, + 'include')}" type="hidden"/> + <input data-bind="value: option.value, + attr: {name: $parents[1].getOptionRowName(attribute, option, 'value_index')}" + type="hidden"/> </div> <!-- /ko --> </div> @@ -104,7 +118,9 @@ $currencySymbol = $block->getCurrencySymbol(); </button> <ul class="dropdown"> <li> - <a class="item" data-action="no-image"><?= $block->escapeHtml(__('No Image')) ?></a> + <a class="item" data-action="no-image"> + <?= $block->escapeHtml(__('No Image')) ?> + </a> </li> </ul> </div> @@ -167,7 +183,8 @@ $currencySymbol = $block->getCurrencySymbol(); <input type="text" class="validate-zero-or-greater" data-bind="attr: {id: $parent.getRowId(variation, 'qty'), - name: $parent.getVariationRowName(variation, 'quantity_and_stock_status/qty'), + name: $parent.getVariationRowName(variation, + 'quantity_and_stock_status/qty'), value: variation.quantity}"/> <!-- /ko --> </td> @@ -187,17 +204,20 @@ $currencySymbol = $block->getCurrencySymbol(); <td data-bind="text: label"></td> <!-- /ko --> <td class="data-grid-actions-cell"> - <input type="hidden" name="associated_product_ids[]" data-bind="value: variation.productId" data-column="entity_id"/> + <input type="hidden" name="associated_product_ids[]" + data-bind="value: variation.productId" data-column="entity_id"/> <div class="action-select-wrap" data-bind=" css : { '_active' : $parent.opened() === $index() }, outerClick: $parent.closeList.bind($parent, $index)" > - <button class="action-select" data-bind="click: $parent.toggleList.bind($parent, $index())"> + <button class="action-select" + data-bind="click: $parent.toggleList.bind($parent, $index())"> <span data-bind="i18n: 'Select'"></span> </button> - <ul class="action-menu _active" data-bind="css: {'_active': $parent.opened() === $index()}"> + <ul class="action-menu _active" + data-bind="css: {'_active': $parent.opened() === $index()}"> <li> <a class="action-menu-item" data-bind=" @@ -211,12 +231,14 @@ $currencySymbol = $block->getCurrencySymbol(); </li> <li> <a class="action-menu-item" data-bind=" - text: variation.status == 1 ? $t('Disable Product') : $t('Enable Product'), + text: variation.status == 1 ? $t('Disable Product') : + $t('Enable Product'), click: $parent.toggleProduct.bind($parent, $index())"> </a> </li> <li> - <a class="action-menu-item" data-bind="click: $parent.removeProduct.bind($parent, $index())"> + <a class="action-menu-item" + data-bind="click: $parent.removeProduct.bind($parent, $index())"> <?= $block->escapeHtml(__('Remove Product')) ?> </a> </li> @@ -231,7 +253,8 @@ $currencySymbol = $block->getCurrencySymbol(); <!-- /ko --> </div> <div data-role="step-wizard-dialog" - data-mage-init='{"Magento_Ui/js/modal/modal":{"type":"slide","title":"<?= $block->escapeJs(__('Create Product Configurations')) ?>", + data-mage-init='{"Magento_Ui/js/modal/modal":{"type":"slide","title":"<?= + $block->escapeJs(__('Create Product Configurations')) ?>", "buttons":[]}}' class="no-display"> <?= /* @noEscape */ $block->getVariationWizard([ @@ -249,8 +272,8 @@ $currencySymbol = $block->getCurrencySymbol(); "components": { "configurableVariations": { "component": "Magento_ConfigurableProduct/js/variations/variations", - "variations": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($productMatrix) ?>, - "productAttributes": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($attributes) ?>, + "variations": <?= /* @noEscape */ $jsonHelper->jsonEncode($productMatrix) ?>, + "productAttributes":<?=/* @noEscape */ $jsonHelper->jsonEncode($attributes)?>, "productUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product/edit', ['id' => '%id%']) ?>", "currencySymbol": "<?= /* @noEscape */ $currencySymbol ?>", "configurableProductGrid": "configurableProductGrid" @@ -260,9 +283,11 @@ $currencySymbol = $block->getCurrencySymbol(); } } </script> -<script> +<?php $scriptString = <<<script require(['jquery'], function ($) { $('body').trigger('contentUpdated'); $('[data-panel=product-variations]').applyBindings(); }) -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml index 7b85efdbb73aa..9d0e78d6ad14c 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $productMatrix = $block->getProductMatrix(); $attributes = $block->getProductAttributes(); @@ -15,13 +16,18 @@ $attributes = $block->getProductAttributes(); 'configurableModal' => $block->getForm() . '.' . $block->getModal() ]); ?> -<script> + +<?php $dataScope = /* @noEscape */ $block->getData('config/dataScope'); +$nameStep = /* @noEscape */ $block->getData('config/nameStepWizard'); +$scriptString = <<<script require(['jquery', 'uiRegistry', 'underscore'], function ($, registry, _) { $('body').trigger('contentUpdated'); - $('.<?= /* @noEscape */ $block->getData('config/dataScope') ?>[data-role=steps-wizard-main]').applyBindings(); + $('.{$dataScope}[data-role=steps-wizard-main]').applyBindings(); - registry.async('<?= /* @noEscape */ $block->getData('config/nameStepWizard') ?>')(function (component) { + registry.async('{$nameStep}')(function (component) { _.delay(component.open.bind(component), 500); // TODO: MAGETWO-50246 }) }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml index f009962bb97ff..2cd5a32ce5449 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml @@ -3,18 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); $productMatrix = $block->getProductMatrix(); $attributes = $block->getProductAttributes(); $currencySymbol = $block->getCurrencySymbol(); ?> -<div class="<?= /* @noEscape */ $block->getData('config/dataScope') ?>" data-role="step-wizard-dialog" data-bind="scope: '<?= /* @noEscape */ $block->getForm() ?>.<?= /* @noEscape */ $block->getModal() ?>'"> +<div class="<?= /* @noEscape */ $block->getData('config/dataScope') ?>" + data-role="step-wizard-dialog" + data-bind="scope: '<?= /* @noEscape */ $block->getForm() ?>.<?= /* @noEscape */ $block->getModal() ?>'"> <!-- ko template: getTemplate() --><!-- /ko --> </div> -<div class="<?= /* @noEscape */ $block->getData('config/dataScope') ?>" id="product-variations-matrix" data-role="product-variations-matrix"> +<div class="<?= /* @noEscape */ $block->getData('config/dataScope') ?>" + id="product-variations-matrix" data-role="product-variations-matrix"> <div data-bind="scope: 'configurableVariations'"></div> </div> <script type="text/x-magento-init"> @@ -24,13 +30,17 @@ $currencySymbol = $block->getCurrencySymbol(); "components": { "<?= /* @noEscape */ $block->getData('config/form') ?>.<?= /* @noEscape */ $block->getModal() ?>": { "component": "Magento_ConfigurableProduct/js/components/modal-configurable", - "options": {"type": "slide", "title": "<?= $block->escapeHtml(__('Create Product Configurations')) ?>"}, + "options": {"type": "slide", + "title": "<?= $block->escapeHtml(__('Create Product Configurations')) ?>"}, "formName": "<?= /* @noEscape */ $block->getForm() ?>", "isTemplate": false, "stepWizard": "<?= /* @noEscape */ $block->getData('config/nameStepWizard') ?>", "children": { "wizard": { - "url": "<?= /* @noEscape */ $block->getUrl($block->getData('config/urlWizard'), ['id' => $block->getProduct()->getId()]) ?>", + "url": "<?= /* @noEscape */ $block->getUrl( + $block->getData('config/urlWizard'), + ['id' => $block->getProduct()->getId()] + ) ?>", "component": "Magento_Ui/js/form/components/html" } } @@ -43,12 +53,14 @@ $currencySymbol = $block->getCurrencySymbol(); "dataScopeAttributeCodes": "data.attribute_codes", "dataScopeAttributesData": "data.product.configurable_attributes_data", "formName": "<?= /* @noEscape */ $block->getForm() ?>", - "attributeSetHandler": "<?= /* @noEscape */ $block->getForm() ?>.configurable_attribute_set_handler_modal", - "wizardModalButtonName": "<?= /* @noEscape */ $block->getForm() ?>.configurable.configurable_products_button_set.create_configurable_products_button", + "attributeSetHandler": "<?= /* @noEscape */ $block->getForm() + ?>.configurable_attribute_set_handler_modal", + "wizardModalButtonName": "<?= /* @noEscape */ $block->getForm() + ?>.configurable.configurable_products_button_set.create_configurable_products_button", "wizardModalButtonTitle": "<?= $block->escapeHtml(__('Edit Configurations')) ?>", - "productAttributes": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($attributes) ?>, + "productAttributes":<?=/* @noEscape */ $jsonHelper->jsonEncode($attributes)?>, "productUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product/edit', ['id' => '%id%']) ?>", - "variations": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($productMatrix) ?>, + "variations": <?= /* @noEscape */ $jsonHelper->jsonEncode($productMatrix) ?>, "currencySymbol": "<?= /* @noEscape */ $currencySymbol ?>", "attributeSetCreationUrl": "<?= /* @noEscape */ $block->getUrl('*/product_set/save') ?>" } @@ -57,10 +69,13 @@ $currencySymbol = $block->getCurrencySymbol(); } } </script> -<script> +<?php $dataScope = /* @noEscape */ $block->getData('config/dataScope'); +$scriptString = <<<script require(['jquery', 'mage/apply/main'], function ($, main) { main.apply(); - $('.<?= /* @noEscape */ $block->getData('config/dataScope') ?>[data-role=step-wizard-dialog]').applyBindings(); - $('.<?= /* @noEscape */ $block->getData('config/dataScope') ?>[data-role=product-variations-matrix]').applyBindings(); + $('.{$dataScope}[data-role=step-wizard-dialog]').applyBindings(); + $('.{$dataScope}[data-role=product-variations-matrix]').applyBindings(); }) -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml index cdb12b54e5e67..d5c946621e90d 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml @@ -5,22 +5,24 @@ */ /* @var $block \Magento\ConfigurableProduct\Block\Product\Configurable\AttributeSelector */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script (function(){ 'use strict'; - - var $form; + + var _form; require([ 'jquery', 'jquery/ui', 'Magento_Ui/js/modal/modal' ], function($){ - $form = $('[data-role=affected-attribute-set-selector]'); + _form = $('[data-role=affected-attribute-set-selector]'); var resetValidation = function() { - $form.find('.messages .message.error').hide(); - $form.find('form').validation().data('validator').resetForm(); + _form.find('.messages .message.error').hide(); + _form.find('form').validation().data('validator').resetForm(); }, setAttributeSetId = function (id) { $('[data-role=new-variations-attribute-set-id]').val(id); @@ -31,10 +33,10 @@ newAttributeSetContainer = $('[data-role=affected-attribute-set-new-name-container]'), existingAttributeSetContainer = $('[data-role=affected-attribute-set-existing-name-container]'); - $form.find('input[type=text]').on('keypress',function(e){ + _form.find('input[type=text]').on('keypress',function(e){ if(e.keyCode === 13){ e.preventDefault(); - $form.closest('[data-role=modal]').find('button[data-action=confirm]').click(); + _form.closest('[data-role=modal]').find('button[data-action=confirm]').click(); } }); @@ -44,66 +46,67 @@ 'data-role': 'new-variations-attribute-set-id' })); - $form + _form .modal({ - title: '<?= $block->escapeJs(__('Choose Affected Attribute Set')) ?>', + title: '{$block->escapeJs(__('Choose Affected Attribute Set'))}', closed: function () { resetValidation(); }, buttons: [{ - text: '<?= $block->escapeJs(__('Confirm')) ?>', + text: '{$block->escapeJs(__('Confirm'))}', attr: { 'data-action': 'confirm' }, 'class': 'action-primary', click: function() { - var affectedAttributeSetId = $form.find('input[name=affected-attribute-set]:checked').val(); + var affectedAttributeSetId = _form.find('input[name=affected-attribute-set]:checked').val(); if (affectedAttributeSetId == 'current') { setAttributeSetId($('#attribute_set_id').val()); - closeDialogAndProcessForm($form); + closeDialogAndProcessForm(_form); return; } else if (affectedAttributeSetId == 'existing') { setAttributeSetId($('select', existingAttributeSetContainer).val()); - closeDialogAndProcessForm($form); + closeDialogAndProcessForm(form); } - $form.find('.messages .message.error').hide(); - if (!$form.find('form').validation().valid()) { - $form.find('input[name=new-attribute-set-name]').focus(); + _form.find('.messages .message.error').hide(); + if (!_form.find('form').validation().valid()) { + _form.find('input[name=new-attribute-set-name]').focus(); return false; } $.ajax({ type: 'POST', - url: '<?= $block->escapeUrl($block->getAttributeSetCreationUrl()) ?>', + url: '{$block->escapeUrl($block->getAttributeSetCreationUrl())}', data: { gotoEdit: 1, - attribute_set_name: $form.find('input[name=new-attribute-set-name]').val(), + attribute_set_name: _form.find('input[name=new-attribute-set-name]').val(), skeleton_set: $('#attribute_set_id').val(), - form_key: '<?= $block->escapeJs($block->getFormKey()) ?>', + form_key: '{$block->escapeJs($block->getFormKey())}', return_session_messages_only: 1 }, dataType: 'json', showLoader: true, - context: $form + context: _form }) .done(function (data) { if (!data.error) { setAttributeSetId(data.id); - closeDialogAndProcessForm($form); + closeDialogAndProcessForm(_form); } else { - $form.find('.messages .message.error').replaceWith($(data.messages).find('.message.error')); + _form.find('.messages .message.error').replaceWith($(data.messages) + .find('.message.error')); } }); return false; } },{ - text: '<?= $block->escapeJs(__('Cancel')) ?>', - id: '<?= $block->escapeJs($block->getJsId('close-button')) ?>', + text: '{$block->escapeJs(__('Cancel'))}', + id: '{$block->escapeJs($block->getJsId('close-button'))}', 'class': 'action-close', click: function() { - $form.modal('closeModal'); + _form.modal('closeModal'); } }] }) @@ -117,7 +120,7 @@ } }); }); - + require([ 'jquery' ], function ($) { @@ -127,17 +130,17 @@ * * @return {Array} */ - var getAttributes = function ($node) { + var getAttributes = function (_node) { return $.map( - $node.find('[data-role=configurable-attributes-container] [data-role=attribute-info]') || [], + _node.find('[data-role=configurable-attributes-container] [data-role=attribute-info]') || [], function (attribute) { - var $attribute = $(attribute); + var _attribute = $(attribute); return { - id: $attribute.find('[name$="[attribute_id]"]').val(), - code: $attribute.find('[name$="[code]"]').val(), - label: $attribute.find('[name$="[label]"]').val(), - position: $attribute.find('[name$="[position]"]').val() + id: _attribute.find('[name$="[attribute_id]"]').val(), + code: _attribute.find('[name$="[code]"]').val(), + label: _attribute.find('[name$="[label]"]').val(), + position: _attribute.find('[name$="[position]"]').val() }; } ); @@ -170,10 +173,13 @@ event.stopImmediatePropagation(); - $form.data('target', event.target).modal('openModal'); + _form.data('target', event.target).modal('openModal'); }); }); })(); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml index e6cf1e9c6870d..59bfabe736c02 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml @@ -3,12 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /** @var $block \Magento\ConfigurableProduct\Block\Product\Configurable\AttributeSelector */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$suggestWidgetOptions = /* @noEscape */ $jsonHelper->jsonEncode($block->getSuggestWidgetOptions()); +$scriptString = <<<script require(["jquery","mage/mage","mage/backend/suggest"], function($){ - var options = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSuggestWidgetOptions()) ?>; + var options = {$suggestWidgetOptions}; $('#configurable-attribute-selector') .mage('suggest', options) .on('suggestselect', function (event, ui) { @@ -29,4 +35,6 @@ require(["jquery","mage/mage","mage/backend/suggest"], function($){ return false; }) }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/stock/disabler.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/stock/disabler.phtml index fe41f07a4434d..c0f3f8617bd16 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/stock/disabler.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/stock/disabler.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require(['jquery'], function($){ $('[data-tab-panel=product-details]').on('stockbeforedisable', function(e) { var variations = $('[data-panel=product-variations]'); @@ -14,4 +17,6 @@ require(['jquery'], function($){ } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> 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/Resolver/ConfigurableVariant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php index f28bf97adf930..0cb0eddf8a246 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php @@ -97,8 +97,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $this->variantCollection->addEavAttributes($fields); $this->optionCollection->addProductId((int)$value[$linkField]); - $result = function () use ($value, $linkField) { - $children = $this->variantCollection->getChildProductsByParentId((int)$value[$linkField]); + $result = function () use ($value, $linkField, $context) { + $children = $this->variantCollection->getChildProductsByParentId((int)$value[$linkField], $context); $options = $this->optionCollection->getAttributesByProductId((int)$value[$linkField]); $variants = []; /** @var Product $child */ diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php index faf666144422c..3a064f3399255 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php @@ -60,7 +60,8 @@ public function resolve( $option['options_map'] ?? [], $code, (int) $optionId, - (int) $model->getData($code) + (int) $model->getData($code), + (int) $option['attribute_id'] ); if (!empty($optionsFromMap)) { $data[] = $optionsFromMap; @@ -77,14 +78,20 @@ public function resolve( * @param string $code * @param int $optionId * @param int $attributeCodeId + * @param int $attributeId * @return array */ - private function getOptionsFromMap(array $optionMap, string $code, int $optionId, int $attributeCodeId): array - { + private function getOptionsFromMap( + array $optionMap, + string $code, + int $optionId, + int $attributeCodeId, + int $attributeId + ): array { $data = []; if (isset($optionMap[$optionId . ':' . $attributeCodeId])) { $optionValue = $optionMap[$optionId . ':' . $attributeCodeId]; - $data = $this->getOptionsArray($optionValue, $code); + $data = $this->getOptionsArray($optionValue, $code, $attributeId); } return $data; } @@ -94,15 +101,17 @@ private function getOptionsFromMap(array $optionMap, string $code, int $optionId * * @param array $optionValue * @param string $code + * @param int $attributeId * @return array */ - private function getOptionsArray(array $optionValue, string $code): array + private function getOptionsArray(array $optionValue, string $code, int $attributeId): array { return [ 'label' => $optionValue['label'] ?? null, 'code' => $code, 'use_default_value' => $optionValue['use_default_value'] ?? null, 'value_index' => $optionValue['value_index'] ?? null, + 'attribute_id' => $attributeId, ]; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php new file mode 100644 index 0000000000000..13f31e7e2ce10 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.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\Resolver\Variant\Attributes; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Format new option uid in base64 encode for super attribute options + */ +class ConfigurableAttributeUid implements ResolverInterface +{ + /** + * Option type name + */ + private const OPTION_TYPE = 'configurable'; + + /** + * Create a option uid for super attribute in "<option-type>/<attribute-id>/<value-index>" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['attribute_id']) || empty($value['attribute_id'])) { + throw new GraphQlInputException(__('"attribute_id" value should be specified.')); + } + + if (!isset($value['value_index']) || empty($value['value_index'])) { + throw new GraphQlInputException(__('"value_index" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['attribute_id'], + $value['value_index'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index 6c4371b23927e..b60a660251f4d 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -13,6 +13,7 @@ use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\GraphQl\Model\Query\ContextInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor; @@ -118,11 +119,12 @@ public function addEavAttributes(array $attributeCodes) : void * Retrieve child products from for passed in parent id. * * @param int $id + * @param ContextInterface|null $context * @return array */ - public function getChildProductsByParentId(int $id) : array + public function getChildProductsByParentId(int $id, ContextInterface $context = null) : array { - $childrenMap = $this->fetch(); + $childrenMap = $this->fetch($context); if (!isset($childrenMap[$id])) { return []; @@ -134,9 +136,10 @@ public function getChildProductsByParentId(int $id) : array /** * Fetch all children products from parent id's. * + * @param ContextInterface|null $context * @return array */ - private function fetch() : array + private function fetch(ContextInterface $context = null) : array { if (empty($this->parentProducts) || !empty($this->childrenMap)) { return $this->childrenMap; @@ -150,7 +153,8 @@ private function fetch() : array $this->collectionProcessor->process( $childCollection, $this->searchCriteriaBuilder->create(), - $attributeData + $attributeData, + $context ); $childCollection->load(); $this->collectionPostProcessor->process($childCollection, $attributeData); 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/composer.json b/app/code/Magento/ConfigurableProductGraphQl/composer.json index 76ec4ad3153e2..295efb65b1978 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/composer.json +++ b/app/code/Magento/ConfigurableProductGraphQl/composer.json @@ -6,6 +6,7 @@ "php": "~7.3.0||~7.4.0", "magento/module-catalog": "*", "magento/module-configurable-product": "*", + "magento/module-graph-ql": "*", "magento/module-catalog-graph-ql": "*", "magento/module-quote": "*", "magento/module-quote-graph-ql": "*", 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 2e9576b35e6e8..257bca11fb5b7 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -18,6 +18,7 @@ type ConfigurableAttributeOption @doc(description: "ConfigurableAttributeOption label: String @doc(description: "A string that describes the configurable attribute option") code: String @doc(description: "The ID assigned to the attribute") value_index: Int @doc(description: "A unique index number assigned to the configurable product option") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes\\ConfigurableAttributeUid") # A Base64 string that encodes option details. } type ConfigurableProductOptions @doc(description: "ConfigurableProductOptions defines configurable attributes for the specified product") { @@ -67,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/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml b/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml index 0c46ed4729d66..2300740f23c7d 100644 --- a/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml +++ b/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml @@ -25,11 +25,15 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <executeJS function="return window.location.host" stepKey="hostname"/> <amOnUrl url="http://{$hostname}/contact" stepKey="goToUnsecureContactURL"/> diff --git a/app/code/Magento/Contact/view/frontend/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml index d218e650657ac..e9d0c065fd8bf 100644 --- a/app/code/Magento/Contact/view/frontend/templates/form.phtml +++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml @@ -4,6 +4,9 @@ * See COPYING.txt for license details. */ +// phpcs:disable Magento2.Templates.ThisInTemplate +// phpcs:disable Generic.Files.LineLength.TooLong + /** @var \Magento\Contact\Block\ContactForm $block */ /** @var \Magento\Contact\ViewModel\UserDataProvider $viewModel */ @@ -23,35 +26,35 @@ $viewModel = $block->getViewModel(); <div class="field name required"> <label class="label" for="name"><span><?= $block->escapeHtml(__('Name')) ?></span></label> <div class="control"> - <input name="name" - id="name" - title="<?= $block->escapeHtmlAttr(__('Name')) ?>" - value="<?= $block->escapeHtmlAttr($viewModel->getUserName()) ?>" - class="input-text" - type="text" + <input name="name" + id="name" + title="<?= $block->escapeHtmlAttr(__('Name')) ?>" + value="<?= $block->escapeHtmlAttr($viewModel->getUserName()) ?>" + class="input-text" + type="text" data-validate="{required:true}"/> </div> </div> <div class="field email required"> <label class="label" for="email"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> - <input name="email" - id="email" - title="<?= $block->escapeHtmlAttr(__('Email')) ?>" - value="<?= $block->escapeHtmlAttr($viewModel->getUserEmail()) ?>" - class="input-text" - type="email" + <input name="email" + id="email" + title="<?= $block->escapeHtmlAttr(__('Email')) ?>" + value="<?= $block->escapeHtmlAttr($viewModel->getUserEmail()) ?>" + class="input-text" + type="email" data-validate="{required:true, 'validate-email':true}"/> </div> </div> <div class="field telephone"> <label class="label" for="telephone"><span><?= $block->escapeHtml(__('Phone Number')) ?></span></label> <div class="control"> - <input name="telephone" - id="telephone" - title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" - value="<?= $block->escapeHtmlAttr($viewModel->getUserTelephone()) ?>" - class="input-text" + <input name="telephone" + id="telephone" + title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" + value="<?= $block->escapeHtmlAttr($viewModel->getUserTelephone()) ?>" + class="input-text" type="tel" /> </div> </div> @@ -60,14 +63,14 @@ $viewModel = $block->getViewModel(); <span><?= $block->escapeHtml(__('What’s on your mind?')) ?></span> </label> <div class="control"> - <textarea name="comment" - id="comment" - title="<?= $block->escapeHtmlAttr(__('What’s on your mind?')) ?>" - class="input-text" - cols="5" - rows="3" - data-validate="{required:true}"><?= $block->escapeHtml($viewModel->getUserComment()) ?> - </textarea> + <textarea name="comment" + id="comment" + title="<?= $block->escapeHtmlAttr(__('What’s on your mind?')) ?>" + class="input-text" + cols="5" + rows="3" + data-validate="{required:true}" + ><?= $block->escapeHtml($viewModel->getUserComment()) ?></textarea> </div> </div> <?= $block->getChildHtml('form.additional.info') ?> @@ -81,3 +84,12 @@ $viewModel = $block->getViewModel(); </div> </div> </form> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "contact-form" + } + } + } +</script> diff --git a/app/code/Magento/Cookie/Block/Html/Notices.php b/app/code/Magento/Cookie/Block/Html/Notices.php index b4dda788a0292..4bc7ffd7e7e16 100644 --- a/app/code/Magento/Cookie/Block/Html/Notices.php +++ b/app/code/Magento/Cookie/Block/Html/Notices.php @@ -9,12 +9,30 @@ */ namespace Magento\Cookie\Block\Html; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template; +use Magento\Cookie\Helper\Cookie as CookieHelper; + /** * @api * @since 100.0.2 */ class Notices extends \Magento\Framework\View\Element\Template { + /** + * @param Template\Context $context + * @param array $data + * @param CookieHelper|null $cookieHelper + */ + public function __construct( + Template\Context $context, + array $data = [], + ?CookieHelper $cookieHelper = null + ) { + $data['cookieHelper'] = $cookieHelper ?? ObjectManager::getInstance()->get(CookieHelper::class); + parent::__construct($context, $data); + } + /** * Get Link to cookie restriction privacy policy page * diff --git a/app/code/Magento/Cookie/Helper/Cookie.php b/app/code/Magento/Cookie/Helper/Cookie.php index 8bab596ab4c13..0e04e7ace2cea 100644 --- a/app/code/Magento/Cookie/Helper/Cookie.php +++ b/app/code/Magento/Cookie/Helper/Cookie.php @@ -80,7 +80,7 @@ public function isUserNotAllowSaveCookie() * Check if cookie restriction mode is enabled for this store * * @return bool - * @since 100.2.0 + * @since 100.1.3 */ public function isCookieRestrictionModeEnabled() { diff --git a/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml new file mode 100644 index 0000000000000..56098cfec90cb --- /dev/null +++ b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureCookieTest"> + <annotations> + <features value="Cookie"/> + <stories value="Storefront Secure Cookie"/> + <title value="Verify Storefront Cookie Secure Config over https"/> + <description value="Verify that cookie are secure on storefront over https"/> + <severity value="MAJOR"/> + <testCaseId value="MC-36900"/> + <useCaseId value="MC-36809"/> + <group value="cookie"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/unsecure/base_url https://{$hostname}/" stepKey="setUnsecureBaseURL"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </before> + <after> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/unsecure/base_url http://{$hostname}/" stepKey="setUnsecureBaseURL"/> + <magentoCLI command="config:set web/secure/base_url http://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="useSecureURLsOnStorefront"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </after> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.cookiesConfig.secure ? 'true' : 'false'" stepKey="isCookieSecure"/> + <assertEquals stepKey="assertCookieIsSecure"> + <actualResult type="variable">isCookieSecure</actualResult> + <expectedResult type="string">true</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml new file mode 100644 index 0000000000000..e601a6b1920b0 --- /dev/null +++ b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyUnsecureCookieTest"> + <annotations> + <features value="Cookie"/> + <stories value="Storefront Secure Cookie"/> + <title value="Verify Storefront Cookie Secure Config over http"/> + <description value="Verify that cookie are not secure on storefront over http"/> + <severity value="MAJOR"/> + <testCaseId value="MC-36899"/> + <useCaseId value="MC-36809"/> + <group value="cookie"/> + <group value="configuration"/> + </annotations> + <before> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </before> + <after> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </after> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.cookiesConfig.secure ? 'true' : 'false'" stepKey="isCookieSecure"/> + <assertEquals stepKey="assertCookieIsUnsecure"> + <actualResult type="variable">isCookieSecure</actualResult> + <expectedResult type="string">false</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php b/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php index 9e370a186d272..6522c3ad1dcaa 100644 --- a/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php +++ b/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php @@ -162,6 +162,6 @@ public function getConfigMethodStub($hashName) return $defaultConfig[$hashName]; } - throw new \InvalidArgumentException('Unknow id = ' . $hashName); + throw new \InvalidArgumentException('Unknown id = ' . $hashName); } } diff --git a/app/code/Magento/Cookie/etc/adminhtml/system.xml b/app/code/Magento/Cookie/etc/adminhtml/system.xml index 3bf9d11e0a462..e43fc5c5c1e2d 100644 --- a/app/code/Magento/Cookie/etc/adminhtml/system.xml +++ b/app/code/Magento/Cookie/etc/adminhtml/system.xml @@ -29,7 +29,7 @@ <field id="cookie_httponly" translate="label comment" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Use HTTP Only</label> <comment> - <![CDATA[<strong style="color:red">Warning</strong>: Do not set to "No". User security could be compromised.]]> + <![CDATA[<strong class="colorRed">Warning</strong>: Do not set to "No". User security could be compromised.]]> </comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> diff --git a/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml b/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml index b05c53db02abf..a604290004588 100644 --- a/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml +++ b/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml @@ -8,10 +8,13 @@ * Cookie settings initialization script * * @var $block \Magento\Framework\View\Element\Js\Cookie + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +$isCookieSecure = $block->getSessionConfig()->getCookieSecure() ? 'true' : 'false'; +$scriptString = " + window.cookiesConfig = window.cookiesConfig || {}; + window.cookiesConfig.secure = $isCookieSecure; +"; ?> -<script> - window.cookiesConfig = window.cookiesConfig || {}; - window.cookiesConfig.secure = <?= /* @noEscape */ $block->getSessionConfig()->getCookieSecure() ? 'true' : 'false' ?>; -</script> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml index 8712f31e71b36..38f0d8655f2d6 100644 --- a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml +++ b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml @@ -5,17 +5,23 @@ */ /** @var \Magento\Cookie\Block\Html\Notices $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($this->helper(\Magento\Cookie\Helper\Cookie::class)->isCookieRestrictionModeEnabled()) : ?> +<?php +/** @var \Magento\Cookie\Helper\Cookie $cookieHelper */ +$cookieHelper = $block->getData('cookieHelper'); +if ($cookieHelper->isCookieRestrictionModeEnabled()): ?> <div role="alertdialog" tabindex="-1" class="message global cookie" - id="notice-cookie-block" - style="display: none;"> + id="notice-cookie-block"> <div role="document" class="content" tabindex="0"> <p> <strong><?= $block->escapeHtml(__('We use cookies to make your experience better.')) ?></strong> - <span><?= $block->escapeHtml(__('To comply with the new e-Privacy directive, we need to ask for your consent to set the cookies.')) ?></span> + <span><?= $block->escapeHtml(__( + 'To comply with the new e-Privacy directive, we need to ask for your consent to set the cookies.' + )) ?> + </span> <?= $block->escapeHtml(__('<a href="%1">Learn more</a>.', $block->getPrivacyPolicyLink()), ['a']) ?> </p> <div class="actions"> @@ -25,15 +31,16 @@ </div> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#notice-cookie-block') ?> <script type="text/x-magento-init"> { "#notice-cookie-block": { "cookieNotices": { "cookieAllowButtonSelector": "#btn-cookie-allow", "cookieName": "<?= /* @noEscape */ \Magento\Cookie\Helper\Cookie::IS_USER_ALLOWED_SAVE_COOKIE ?>", - "cookieValue": <?= /* @noEscape */ $this->helper(\Magento\Cookie\Helper\Cookie::class)->getAcceptedSaveCookiesWebsiteIds() ?>, - "cookieLifetime": <?= /* @noEscape */ $this->helper(\Magento\Cookie\Helper\Cookie::class)->getCookieRestrictionLifetime() ?>, - "noCookiesUrl": "<?= $block->escapeJs($block->escapeUrl($block->getUrl('cookie/index/noCookies'))) ?>" + "cookieValue": <?= /* @noEscape */ $cookieHelper->getAcceptedSaveCookiesWebsiteIds() ?>, + "cookieLifetime": <?= /* @noEscape */ $cookieHelper->getCookieRestrictionLifetime() ?>, + "noCookiesUrl": "<?= $block->escapeJs($block->getUrl('cookie/index/noCookies')) ?>" } } } diff --git a/app/code/Magento/Cron/Model/Schedule.php b/app/code/Magento/Cron/Model/Schedule.php index 3769b8f12cad2..136e2ef191084 100644 --- a/app/code/Magento/Cron/Model/Schedule.php +++ b/app/code/Magento/Cron/Model/Schedule.php @@ -189,6 +189,7 @@ public function matchCronExpression($expr, $num) } // handle all match by modulus + $offset = 0; if ($expr === '*') { $from = 0; $to = 60; @@ -201,6 +202,13 @@ public function matchCronExpression($expr, $num) $from = $this->getNumeric($e[0]); $to = $this->getNumeric($e[1]); + if ($mod !== 1) { + $offset = $from; + } + } elseif ($mod !== 1) { + $offset = $this->getNumeric($expr); + $from = 0; + $to = 60; } else { // handle regular token $from = $this->getNumeric($expr); @@ -211,7 +219,7 @@ public function matchCronExpression($expr, $num) throw new CronException(__('Invalid cron expression: %1', $expr)); } - return $num >= $from && $num <= $to && $num % $mod === 0; + return $num >= $from && $num <= $to && ($num - $offset) % $mod === 0; } /** diff --git a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php index 25e9a8347b2cd..81e96c6ea75b3 100644 --- a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php +++ b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php @@ -128,24 +128,28 @@ public function setCronExprDataProvider(): array [', * * * *', [',', '*', '*', '*', '*']], ['1-2 * * * *', ['1-2', '*', '*', '*', '*']], ['0/5 * * * *', ['0/5', '*', '*', '*', '*']], + ['3/5 * * * *', ['3/5', '*', '*', '*', '*']], ['* 0 * * *', ['*', '0', '*', '*', '*']], ['* 59 * * *', ['*', '59', '*', '*', '*']], ['* , * * *', ['*', ',', '*', '*', '*']], ['* 1-2 * * *', ['*', '1-2', '*', '*', '*']], ['* 0/5 * * *', ['*', '0/5', '*', '*', '*']], + ['* 3/5 * * *', ['*', '3/5', '*', '*', '*']], ['* * 0 * *', ['*', '*', '0', '*', '*']], ['* * 23 * *', ['*', '*', '23', '*', '*']], ['* * , * *', ['*', '*', ',', '*', '*']], ['* * 1-2 * *', ['*', '*', '1-2', '*', '*']], ['* * 0/5 * *', ['*', '*', '0/5', '*', '*']], + ['* * 3/5 * *', ['*', '*', '3/5', '*', '*']], ['* * * 1 *', ['*', '*', '*', '1', '*']], ['* * * 31 *', ['*', '*', '*', '31', '*']], ['* * * , *', ['*', '*', '*', ',', '*']], ['* * * 1-2 *', ['*', '*', '*', '1-2', '*']], ['* * * 0/5 *', ['*', '*', '*', '0/5', '*']], + ['* * * 3/5 *', ['*', '*', '*', '3/5', '*']], ['* * * ? *', ['*', '*', '*', '?', '*']], ['* * * L *', ['*', '*', '*', 'L', '*']], ['* * * W *', ['*', '*', '*', 'W', '*']], @@ -156,6 +160,7 @@ public function setCronExprDataProvider(): array ['* * * * ,', ['*', '*', '*', '*', ',']], ['* * * * 1-2', ['*', '*', '*', '*', '1-2']], ['* * * * 0/5', ['*', '*', '*', '*', '0/5']], + ['* * * * 3/5', ['*', '*', '*', '*', '3/5']], ['* * * * JAN', ['*', '*', '*', '*', 'JAN']], ['* * * * DEC', ['*', '*', '*', '*', 'DEC']], ['* * * * JAN-DEC', ['*', '*', '*', '*', 'JAN-DEC']], @@ -165,6 +170,7 @@ public function setCronExprDataProvider(): array ['* * * * * ,', ['*', '*', '*', '*', '*', ',']], ['* * * * * 1-2', ['*', '*', '*', '*', '*', '1-2']], ['* * * * * 0/5', ['*', '*', '*', '*', '*', '0/5']], + ['* * * * * 3/5', ['*', '*', '*', '*', '*', '3/5']], ['* * * * * ?', ['*', '*', '*', '*', '*', '?']], ['* * * * * L', ['*', '*', '*', '*', '*', 'L']], ['* * * * * 6#3', ['*', '*', '*', '*', '*', '6#3']], @@ -372,9 +378,19 @@ public function matchCronExpressionDataProvider(): array ['0-20/5', 21, false], ['0-20/5', 25, false], + ['3-20/5', 3, true], + ['3-20/5', 8, true], + ['3-20/5', 13, true], + ['3-20/5', 24, false], + ['3-20/5', 28, false], + ['1/5', 5, false], ['5/5', 5, true], ['10/5', 10, true], + + ['4/5', 8, false], + ['8/5', 8, true], + ['13/5', 13, true], ]; } 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/Api/CspAwareActionInterface.php b/app/code/Magento/Csp/Api/CspAwareActionInterface.php new file mode 100644 index 0000000000000..f4d58dd2bf55a --- /dev/null +++ b/app/code/Magento/Csp/Api/CspAwareActionInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Api; + +use Magento\Framework\App\ActionInterface; + +/** + * Interface for controllers that can provide route-specific CSPs. + */ +interface CspAwareActionInterface extends ActionInterface +{ + /** + * Return CSPs that will be applied to current route (page). + * + * The array returned will be used as is so if you need to keep policies that have been already applied they need + * to be included in the resulting array. + * + * @param \Magento\Csp\Api\Data\PolicyInterface[] $appliedPolicies + * @return \Magento\Csp\Api\Data\PolicyInterface[] + */ + public function modifyCsp(array $appliedPolicies): array; +} diff --git a/app/code/Magento/Csp/Api/InlineUtilInterface.php b/app/code/Magento/Csp/Api/InlineUtilInterface.php new file mode 100644 index 0000000000000..dac2adbfd2270 --- /dev/null +++ b/app/code/Magento/Csp/Api/InlineUtilInterface.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Api; + +use Magento\Csp\Api\Data\PolicyInterface; + +/** + * Utility for classes responsible for rendering and templates that allows whitelist inline sources. + */ +interface InlineUtilInterface +{ + /** + * Render HTML tag and whitelist it as trusted source. + * + * Use this method to whitelist remote static resources and inline styles/scripts. + * Do not use user-provided as any of the parameters. + * + * @param string $tagName + * @param string[] $attributes + * @param string|null $content + * @return string + */ + public function renderTag(string $tagName, array $attributes, ?string $content = null): string; + + /** + * Render event listener as an HTML attribute and whitelist it as trusted source. + * + * Do not use user-provided values as any of the parameters. + * + * @param string $eventName Full attribute name like "onclick". + * @param string $javascript + * @return string + */ + public function renderEventListener(string $eventName, string $javascript): string; +} diff --git a/app/code/Magento/Csp/Helper/InlineUtil.php b/app/code/Magento/Csp/Helper/InlineUtil.php new file mode 100644 index 0000000000000..f9dd9aafa459e --- /dev/null +++ b/app/code/Magento/Csp/Helper/InlineUtil.php @@ -0,0 +1,236 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Csp\Helper; + +use Magento\Csp\Api\InlineUtilInterface; +use Magento\Csp\Model\Collector\DynamicCollector; +use Magento\Csp\Model\Policy\FetchPolicy; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRender\EventHandlerData; +use Magento\Framework\View\Helper\SecureHtmlRender\HtmlRenderer; +use Magento\Framework\View\Helper\SecureHtmlRender\SecurityProcessorInterface; +use Magento\Framework\View\Helper\SecureHtmlRender\TagData; + +/** + * Helper for classes responsible for rendering and templates. + * + * Allows to whitelist dynamic sources specific to a certain page. + */ +class InlineUtil implements InlineUtilInterface, SecurityProcessorInterface +{ + /** + * @var DynamicCollector + */ + private $dynamicCollector; + + /** + * @var bool + */ + private $useUnsafeHashes; + + /** + * @var HtmlRenderer + */ + private $htmlRenderer; + + private static $tagMeta = [ + 'script' => ['id' => 'script-src', 'remote' => ['src'], 'hash' => true], + 'style' => ['id' => 'style-src', 'remote' => [], 'hash' => true], + 'img' => ['id' => 'img-src', 'remote' => ['src']], + 'audio' => ['id' => 'media-src', 'remote' => ['src']], + 'video' => ['id' => 'media-src', 'remote' => ['src']], + 'track' => ['id' => 'media-src', 'remote' => ['src']], + 'source' => ['id' => 'media-src', 'remote' => ['src']], + 'object' => ['id' => 'object-src', 'remote' => ['data', 'archive']], + 'embed' => ['id' => 'object-src', 'remote' => ['src']], + 'applet' => ['id' => 'object-src', 'remote' => ['code', 'archive']], + 'link' => ['id' => 'style-src', 'remote' => ['href']], + 'form' => ['id' => 'form-action', 'remote' => ['action']], + 'iframe' => ['id' => 'frame-src', 'remote' => ['src']], + 'frame' => ['id' => 'frame-src', 'remote' => ['src']] + ]; + + /** + * @param DynamicCollector $dynamicCollector + * @param bool $useUnsafeHashes Use 'unsafe-hashes' policy (not supported by CSP v2). + * @param HtmlRenderer|null $htmlRenderer + */ + public function __construct( + DynamicCollector $dynamicCollector, + bool $useUnsafeHashes = false, + ?HtmlRenderer $htmlRenderer = null + ) { + $this->dynamicCollector = $dynamicCollector; + $this->useUnsafeHashes = $useUnsafeHashes; + $this->htmlRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(HtmlRenderer::class); + } + + /** + * Generate fetch policy hash for some content. + * + * @param string $content + * @return array Hash data to insert into a FetchPolicy. + */ + private function generateHashValue(string $content): array + { + return [base64_encode(hash('sha256', $content, true)) => 'sha256']; + } + + /** + * Extract host for a fetch policy from a URL. + * + * @param string $url + * @return string|null Null is returned when URL does not point to a remote host. + */ + private function extractHost(string $url): ?string + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $urlData = parse_url($url); + if (!$urlData + || empty($urlData['scheme']) + || ($urlData['scheme'] !== 'http' && $urlData['scheme'] !== 'https') + ) { + return null; + } + + return $urlData['scheme'] .'://' .$urlData['host']; + } + + /** + * Extract remote hosts used to get fonts. + * + * @param string $styleContent + * @return string[] + */ + private function extractRemoteFonts(string $styleContent): array + { + $urlsFound = [[]]; + preg_match_all('/\@font\-face\s*?\{([^\}]*)[^\}]*?\}/im', $styleContent, $fontFaces); + foreach ($fontFaces[1] as $fontFaceContent) { + preg_match_all('/url\([\'\"]?(http(s)?\:[^\)]+)[\'\"]?\)/i', $fontFaceContent, $urls); + $urlsFound[] = $urls[1]; + } + + return array_map([$this, 'extractHost'], array_merge(...$urlsFound)); + } + + /** + * Extract remote hosts utilized. + * + * @param string $tag + * @param string[] $attributes + * @param string|null $content + * @return string[] + */ + private function extractRemoteHosts(string $tag, array $attributes, ?string $content): array + { + /** @var string[] $remotes */ + $remotes = []; + foreach (self::$tagMeta[$tag]['remote'] as $remoteAttr) { + if (!empty($attributes[$remoteAttr]) && $host = $this->extractHost($attributes[$remoteAttr])) { + $remotes[] = $host; + break; + } + } + if ($tag === 'style' && $content) { + $remotes += $this->extractRemoteFonts($content); + } + + return $remotes; + } + + /** + * @inheritDoc + */ + public function renderTag(string $tagName, array $attributes, ?string $content = null): string + { + if (!array_key_exists($tagName, self::$tagMeta)) { + throw new \InvalidArgumentException('Unknown source type - ' .$tagName); + } + + return $this->htmlRenderer->renderTag($this->processTag(new TagData($tagName, $attributes, $content, false))); + } + + /** + * @inheritDoc + */ + public function renderEventListener(string $eventName, string $javascript): string + { + return $this->htmlRenderer->renderEventHandler( + $this->processEventHandler(new EventHandlerData($eventName, $javascript)) + ); + } + + /** + * @inheritDoc + */ + public function processTag(TagData $tagData): TagData + { + //Processing tag data + if (array_key_exists($tagData->getTag(), self::$tagMeta)) { + /** @var string $policyId */ + $policyId = self::$tagMeta[$tagData->getTag()]['id']; + $remotes = $this->extractRemoteHosts($tagData->getTag(), $tagData->getAttributes(), $tagData->getContent()); + if (empty($remotes) && !$tagData->getContent()) { + throw new \InvalidArgumentException('Either remote URL or hashable content is required to whitelist'); + } + + //Adding required policies. + if ($remotes) { + $this->dynamicCollector->add( + new FetchPolicy($policyId, false, $remotes) + ); + } + if ($tagData->getContent() && !empty(self::$tagMeta[$tagData->getTag()]['hash'])) { + $this->dynamicCollector->add( + new FetchPolicy( + $policyId, + false, + [], + [], + false, + false, + false, + [], + $this->generateHashValue($tagData->getContent()) + ) + ); + } + } + + return $tagData; + } + + /** + * @inheritDoc + */ + public function processEventHandler(EventHandlerData $eventHandlerData): EventHandlerData + { + if ($this->useUnsafeHashes) { + $policy = new FetchPolicy( + 'script-src', + false, + [], + [], + false, + false, + false, + [], + $this->generateHashValue($eventHandlerData->getCode()), + false, + true + ); + } else { + $policy = new FetchPolicy('script-src', false, [], [], false, true); + } + $this->dynamicCollector->add($policy); + + return $eventHandlerData; + } +} diff --git a/app/code/Magento/Csp/Model/BlockCache.php b/app/code/Magento/Csp/Model/BlockCache.php new file mode 100644 index 0000000000000..f0469c3251379 --- /dev/null +++ b/app/code/Magento/Csp/Model/BlockCache.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model; + +use Magento\Csp\Model\Collector\DynamicCollector; +use Magento\Csp\Model\Policy\FetchPolicy; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Serialize\SerializerInterface; + +/** + * CSP aware block cache. + */ +class BlockCache implements CacheInterface +{ + /** + * @var CacheInterface + */ + private $cache; + + /** + * @var DynamicCollector + */ + private $dynamicCollector; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @param CacheInterface $cache + * @param DynamicCollector $dynamicCollector + * @param SerializerInterface $serializer + */ + public function __construct( + CacheInterface $cache, + DynamicCollector $dynamicCollector, + SerializerInterface $serializer + ) { + $this->cache = $cache; + $this->dynamicCollector = $dynamicCollector; + $this->serializer = $serializer; + } + + /** + * @inheritDoc + */ + public function getFrontend() + { + return $this->cache->getFrontend(); + } + + /** + * @inheritDoc + */ + public function load($identifier) + { + /** @var array|null $data */ + $data = null; + $loaded = $this->cache->load($identifier); + try { + $data = $this->serializer->unserialize($loaded); + if (!is_array($data) || !array_key_exists('policies', $data) || !array_key_exists('html', $data)) { + $data = null; + } + } catch (\Throwable $exception) { + //Most likely block HTML was cached without policy data. + $data = null; + } + if ($data) { + foreach ($data['policies'] as $policyData) { + $this->dynamicCollector->add( + new FetchPolicy( + $policyData['id'], + false, + $policyData['hosts'], + [], + false, + false, + false, + [], + $policyData['hashes'] + ) + ); + } + $loaded = $data['html']; + } + + return $loaded; + } + + /** + * @inheritDoc + */ + public function save($data, $identifier, $tags = [], $lifeTime = null) + { + $collected = $this->dynamicCollector->collect(); + if ($collected) { + $policiesData = []; + foreach ($collected as $policy) { + if ($policy instanceof FetchPolicy) { + $policiesData[] = [ + 'id' => $policy->getId(), + 'hosts' => $policy->getHostSources(), + 'hashes' => $policy->getHashes() + ]; + } + } + $data = $this->serializer->serialize(['policies' => $policiesData, 'html' => $data]); + } + + return $this->cache->save($data, $identifier, $tags, $lifeTime); + } + + /** + * @inheritDoc + */ + public function remove($identifier) + { + return $this->cache->remove($identifier); + } + + /** + * @inheritDoc + */ + public function clean($tags = []) + { + return $this->cache->clean($tags); + } +} diff --git a/app/code/Magento/Csp/Model/Collector/ControllerCollector.php b/app/code/Magento/Csp/Model/Collector/ControllerCollector.php new file mode 100644 index 0000000000000..0239601951744 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/ControllerCollector.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\CspAwareActionInterface; +use Magento\Csp\Api\PolicyCollectorInterface; + +/** + * Asks for route-specific policies from a compatible controller. + */ +class ControllerCollector implements PolicyCollectorInterface +{ + /** + * @var CspAwareActionInterface|null + */ + private $controller; + + /** + * Set the action interface that is responsible for processing current HTTP request. + * + * @param CspAwareActionInterface $cspAwareAction + * @return void + */ + public function setCurrentActionInstance(CspAwareActionInterface $cspAwareAction): void + { + $this->controller = $cspAwareAction; + } + + /** + * @inheritDoc + */ + public function collect(array $defaultPolicies = []): array + { + if ($this->controller) { + return $this->controller->modifyCsp($defaultPolicies); + } + + return $defaultPolicies; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php index ab1ee8bb0befe..e0b3af9f9ed81 100644 --- a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php @@ -43,7 +43,6 @@ public function convert($source) } } $policyConfig[$id]['hosts'] = array_unique($policyConfig[$id]['hosts']); - $policyConfig[$id]['hashes'] = array_unique($policyConfig[$id]['hashes']); } return $policyConfig; diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Data.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Data.php new file mode 100644 index 0000000000000..015327df90efb --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Data.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\CspWhitelistXml; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\Config\Data\Scoped; +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Config\CacheInterface; + +/** + * Provides CSP whitelist configuration + */ +class Data extends Scoped +{ + /** + * Scope priority loading scheme + * + * @var array + */ + protected $_scopePriorityScheme = ['global']; + + /** + * Constructor + * + * @param Reader $reader + * @param ScopeInterface $configScope + * @param CacheInterface $cache + * @param SerializerInterface $serializer + */ + public function __construct( + Reader $reader, + ScopeInterface $configScope, + CacheInterface $cache, + SerializerInterface $serializer + ) { + parent::__construct($reader, $configScope, $cache, 'csp_whitelist_config', $serializer); + } +} diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php index 9f19a5299c063..7fa16fda52ab9 100644 --- a/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php @@ -9,7 +9,7 @@ namespace Magento\Csp\Model\Collector; use Magento\Csp\Api\PolicyCollectorInterface; -use Magento\Csp\Model\Collector\CspWhitelistXml\Reader as ConfigReader; +use Magento\Framework\Config\DataInterface as ConfigReader; use Magento\Csp\Model\Policy\FetchPolicy; /** @@ -36,7 +36,7 @@ public function __construct(ConfigReader $configReader) public function collect(array $defaultPolicies = []): array { $policies = $defaultPolicies; - $config = $this->configReader->read(); + $config = $this->configReader->get(null); foreach ($config as $policyId => $values) { $policies[] = new FetchPolicy( $policyId, diff --git a/app/code/Magento/Csp/Model/Collector/DynamicCollector.php b/app/code/Magento/Csp/Model/Collector/DynamicCollector.php new file mode 100644 index 0000000000000..6478e9622f910 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/DynamicCollector.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Api\PolicyCollectorInterface; + +/** + * CSPs dynamically added during the rendering of current page (from .phtml templates for instance). + */ +class DynamicCollector implements PolicyCollectorInterface +{ + /** + * @var PolicyInterface[] + */ + private $added = []; + + /** + * Add a policy for current page. + * + * @param PolicyInterface $policy + * @return void + */ + public function add(PolicyInterface $policy): void + { + $this->added[] = $policy; + } + + /** + * @inheritDoc + */ + public function collect(array $defaultPolicies = []): array + { + return array_merge($defaultPolicies, $this->added); + } +} diff --git a/app/code/Magento/Csp/Model/CompositePolicyCollector.php b/app/code/Magento/Csp/Model/CompositePolicyCollector.php index b775c91b4e1ef..d2c35fe993ed5 100644 --- a/app/code/Magento/Csp/Model/CompositePolicyCollector.php +++ b/app/code/Magento/Csp/Model/CompositePolicyCollector.php @@ -37,22 +37,33 @@ public function __construct(array $collectors, array $mergers) } /** - * Merge 2 policies with the same ID. + * Merge policies with same IDs and return a list of policies with 1 DTO per policy ID. * - * @param PolicyInterface $policy1 - * @param PolicyInterface $policy2 - * @return PolicyInterface + * @param PolicyInterface[] $collected + * @return PolicyInterface[] * @throws \RuntimeException When failed to merge. */ - private function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface + private function merge(array $collected): array { - foreach ($this->mergers as $merger) { - if ($merger->canMerge($policy1, $policy2)) { - return $merger->merge($policy1, $policy2); + /** @var PolicyInterface[] $merged */ + $merged = []; + + foreach ($collected as $policy) { + if (array_key_exists($policy->getId(), $merged)) { + foreach ($this->mergers as $merger) { + if ($merger->canMerge($merged[$policy->getId()], $policy)) { + $merged[$policy->getId()] = $merger->merge($merged[$policy->getId()], $policy); + continue 2; + } + } + + throw new \RuntimeException(sprintf('Merge for policies #%s was not found', $policy->getId())); + } else { + $merged[$policy->getId()] = $policy; } } - throw new \RuntimeException(sprintf('Merge for policies #%s was not found', $policy1->getId())); + return $merged; } /** @@ -62,19 +73,9 @@ public function collect(array $defaultPolicies = []): array { $collected = $defaultPolicies; foreach ($this->collectors as $collector) { - $collected = $collector->collect($collected); - } - //Merging policies. - /** @var PolicyInterface[] $result */ - $result = []; - foreach ($collected as $policy) { - if (array_key_exists($policy->getId(), $result)) { - $result[$policy->getId()] = $this->merge($result[$policy->getId()], $policy); - } else { - $result[$policy->getId()] = $policy; - } + $collected = $this->merge($collector->collect($collected)); } - return array_values($result); + return array_values($collected); } } diff --git a/app/code/Magento/Csp/Model/Policy/FetchPolicy.php b/app/code/Magento/Csp/Model/Policy/FetchPolicy.php index 7350cbe80aecb..d045ee48b0ba2 100644 --- a/app/code/Magento/Csp/Model/Policy/FetchPolicy.php +++ b/app/code/Magento/Csp/Model/Policy/FetchPolicy.php @@ -226,11 +226,13 @@ public function getValue(): string if ($this->areEventHandlersAllowed()) { $sources[] = '\'unsafe-hashes\''; } - foreach ($this->getNonceValues() as $nonce) { - $sources[] = '\'nonce-' .base64_encode($nonce) .'\''; - } - foreach ($this->getHashes() as $hash => $algorithm) { - $sources[]= "'$algorithm-$hash'"; + if (!$this->isInlineAllowed()) { + foreach ($this->getNonceValues() as $nonce) { + $sources[] = '\'nonce-' . base64_encode($nonce) . '\''; + } + foreach ($this->getHashes() as $hash => $algorithm) { + $sources[] = "'$algorithm-$hash'"; + } } return implode(' ', $sources); diff --git a/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php b/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php index 14ae23eb3fe37..d419c25acc4ce 100644 --- a/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php +++ b/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php @@ -45,7 +45,7 @@ public function render(PolicyInterface $policy, HttpResponse $response): void $header = 'Content-Security-Policy'; } $value = $policy->getId() .' ' .$policy->getValue() .';'; - if ($config->getReportUri()) { + if ($config->getReportUri() && !$response->getHeader('Report-To')) { $reportToData = [ 'group' => 'report-endpoint', 'max_age' => 10886400, @@ -57,7 +57,10 @@ public function render(PolicyInterface $policy, HttpResponse $response): void $value .= ' report-to '. $reportToData['group'] .';'; $response->setHeader('Report-To', json_encode($reportToData), true); } - $response->setHeader($header, $value, false); + if ($existing = $response->getHeader($header)) { + $value = $value .' ' .$existing->getFieldValue(); + } + $response->setHeader($header, $value, true); } /** diff --git a/app/code/Magento/Csp/Plugin/CspAwareControllerPlugin.php b/app/code/Magento/Csp/Plugin/CspAwareControllerPlugin.php new file mode 100644 index 0000000000000..09dc7256568bc --- /dev/null +++ b/app/code/Magento/Csp/Plugin/CspAwareControllerPlugin.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Plugin; + +use Magento\Csp\Api\CspAwareActionInterface; +use Magento\Csp\Model\Collector\ControllerCollector; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\RouterInterface; + +/** + * Plugin that registers CSP aware action instance processing current request. + */ +class CspAwareControllerPlugin +{ + /** + * @var ControllerCollector + */ + private $collector; + + /** + * @param ControllerCollector $collector + */ + public function __construct(ControllerCollector $collector) + { + $this->collector = $collector; + } + + /** + * Register matched action instance. + * + * @param RouterInterface $router + * @param ActionInterface|null $matched + * @return ActionInterface|null + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterMatch(RouterInterface $router, $matched) + { + if ($matched && $matched instanceof CspAwareActionInterface) { + $this->collector->setCurrentActionInstance($matched); + } + + return $matched; + } +} diff --git a/app/code/Magento/Csp/etc/config.xml b/app/code/Magento/Csp/etc/config.xml index e45f6b223ed22..6e2235479da93 100644 --- a/app/code/Magento/Csp/etc/config.xml +++ b/app/code/Magento/Csp/etc/config.xml @@ -16,6 +16,218 @@ <report_only>1</report_only> </admin> </mode> + <policies> + <storefront> + <base> + <policy_id>base-uri</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </base> + <default> + <policy_id>default-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>1</eval> + <dynamic>0</dynamic> + </default> + <children> + <policy_id>child-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + <schemes> + <http>http</http> + <https>https</https> + <blob>blob</blob> + </schemes> + </children> + <connections> + <policy_id>connect-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </connections> + <manifests> + <policy_id>manifest-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </manifests> + <media> + <policy_id>media-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </media> + <objects> + <policy_id>object-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </objects> + <styles> + <policy_id>style-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </styles> + <scripts> + <policy_id>script-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>1</eval> + <dynamic>0</dynamic> + </scripts> + <images> + <policy_id>img-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </images> + <frames> + <policy_id>frame-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </frames> + <frame-ancestors> + <policy_id>frame-ancestors</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </frame-ancestors> + <forms> + <policy_id>form-action</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </forms> + <fonts> + <policy_id>font-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </fonts> + </storefront> + <admin> + <base> + <policy_id>base-uri</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </base> + <default> + <policy_id>default-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>1</eval> + <dynamic>0</dynamic> + </default> + <children> + <policy_id>child-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + <schemes> + <http>http</http> + <https>https</https> + <blob>blob</blob> + </schemes> + </children> + <connections> + <policy_id>connect-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </connections> + <manifests> + <policy_id>manifest-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </manifests> + <media> + <policy_id>media-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </media> + <objects> + <policy_id>object-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </objects> + <styles> + <policy_id>style-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </styles> + <scripts> + <policy_id>script-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>1</eval> + <dynamic>0</dynamic> + </scripts> + <images> + <policy_id>img-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </images> + <frames> + <policy_id>frame-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </frames> + <frame-ancestors> + <policy_id>frame-ancestors</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </frame-ancestors> + <forms> + <policy_id>form-action</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </forms> + <fonts> + <policy_id>font-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </fonts> + </admin> + </policies> </csp> </default> </config> 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/Csp/etc/di.xml b/app/code/Magento/Csp/etc/di.xml index 0804f6d579137..7b1129a0e1a41 100644 --- a/app/code/Magento/Csp/etc/di.xml +++ b/app/code/Magento/Csp/etc/di.xml @@ -18,8 +18,10 @@ <type name="Magento\Csp\Model\CompositePolicyCollector"> <arguments> <argument name="collectors" xsi:type="array"> - <item name="config" xsi:type="object">Magento\Csp\Model\Collector\ConfigCollector</item> - <item name="csp_whitelist" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXmlCollector</item> + <item name="config" xsi:type="object" sortOrder="1">Magento\Csp\Model\Collector\ConfigCollector\Proxy</item> + <item name="whitelist" xsi:type="object" sortOrder="2">Magento\Csp\Model\Collector\CspWhitelistXmlCollector\Proxy</item> + <item name="controller" xsi:type="object" sortOrder="100">Magento\Csp\Model\Collector\ControllerCollector\Proxy</item> + <item name="dynamic" xsi:type="object" sortOrder="3">Magento\Csp\Model\Collector\DynamicCollector\Proxy</item> </argument> <argument name="mergers" xsi:type="array"> <item name="fetch" xsi:type="object">Magento\Csp\Model\Collector\FetchPolicyMerger</item> @@ -47,4 +49,55 @@ <argument name="fileName" xsi:type="string">csp_whitelist.xml</argument> </arguments> </type> + <type name="Magento\Csp\Model\Collector\CspWhitelistXmlCollector"> + <arguments> + <argument name="configReader" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\Data</argument> + </arguments> + </type> + <type name="Magento\Csp\Model\Collector\CspWhitelistXml\Data"> + <arguments> + <argument name="reader" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\Reader\Proxy</argument> + </arguments> + </type> + <preference for="Magento\Csp\Api\InlineUtilInterface" type="Magento\Csp\Helper\InlineUtil" /> + <type name="Magento\Csp\Plugin\TemplateRenderingPlugin"> + <arguments> + <argument name="util" xsi:type="object">Magento\Csp\Api\InlineUtilInterface\Proxy</argument> + </arguments> + </type> + <type name="Magento\Framework\View\TemplateEngine\Php"> + <arguments> + <argument name="blockVariables" xsi:type="array"> + <item name="csp" xsi:type="object">Magento\Csp\Api\InlineUtilInterface\Proxy</item> + <item name="secureRenderer" xsi:type="object">Magento\Framework\View\Helper\SecureHtmlRenderer\Proxy</item> + <item name="escaper" xsi:type="object">Magento\Framework\Escaper</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\RouterInterface"> + <plugin name="csp_aware_plugin" type="Magento\Csp\Plugin\CspAwareControllerPlugin" /> + </type> + <type name="Magento\Framework\View\Helper\SecureHtmlRenderer"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="csp" xsi:type="object">Magento\Csp\Helper\InlineUtil\Proxy</item> + </argument> + </arguments> + </type> + <type name="Magento\Csp\Observer\Render"> + <arguments> + <argument name="cspRenderer" xsi:type="object">Magento\Csp\Api\CspRendererInterface</argument> + </arguments> + </type> + + <type name="Magento\Csp\Model\BlockCache"> + <arguments> + <argument name="cache" xsi:type="object">configured_block_cache</argument> + </arguments> + </type> + <type name="Magento\Framework\View\Element\Context"> + <arguments> + <argument name="cache" xsi:type="object">Magento\Csp\Model\BlockCache</argument> + </arguments> + </type> </config> 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/AdminCurrencyConverterAPIConfigurationTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyConverterAPIConfigurationTest.xml index f7e6e05347345..5cfb4b8f0bb2e 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyConverterAPIConfigurationTest.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyConverterAPIConfigurationTest.xml @@ -26,7 +26,9 @@ <!--Set currency allow config--> <magentoCLI command="config:set currency/options/allow RHD,CHW,CHE,AMD,EUR,USD" stepKey="setCurrencyAllow"/> <!--TODO: Add Api key--> - <magentoCLI command="cache:flush" stepKey="clearCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Create product--> <createData entity="SimpleSubCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createProduct"> @@ -68,7 +70,9 @@ <see selector="{{StorefrontCategoryMainSection.productPrice}}" userInput="€" stepKey="seeEURInPrice"/> <!--Set allowed currencies greater then 10--> <magentoCLI command="config:set currency/options/allow RHD,CHW,YER,ZMK,CHE,EUR,USD,AMD,RUB,DZD,ARS,AWG" stepKey="setCurrencyAllow"/> - <magentoCLI command="cache:flush" stepKey="clearCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Import rates from Currency Converter API with currencies greater then 10--> <amOnPage url="{{AdminCurrencyRatesPage.url}}" stepKey="onCurrencyRatePageSecondTime"/> <actionGroup ref="AdminImportCurrencyRatesActionGroup" stepKey="importCurrencyRatesGreaterThen10"> 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/CurrencySymbol/view/adminhtml/templates/grid.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml index 55195698c5dc8..5cce76a791e2d 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml @@ -6,6 +6,7 @@ /** * @var $block \Magento\CurrencySymbol\Block\Adminhtml\System\Currencysymbol + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> @@ -31,9 +32,6 @@ ?> <input id="custom_currency_symbol_inherit<?= $block->escapeHtmlAttr($code) ?>" class="admin__control-checkbox" type="checkbox" - <?php //@codingStandardsIgnoreStart ?> - onclick="toggleUseDefault(<?= '\'' . $escapedCode . '\',\'' . $escapedSymbol . '\'' ?>)" - <?php //@codingStandardsIgnoreEnd ?> <?= $data['inherited'] ? ' checked="checked"' : '' ?> value="1" name="inherit_custom_currency_symbol[<?= $block->escapeHtmlAttr($code) ?>]"> @@ -43,6 +41,11 @@ <?= $block->escapeHtml($block->getInheritText()) ?> </span> </label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleUseDefault('" . $escapedCode . "','" . $escapedSymbol . "')", + '#custom_currency_symbol_inherit' . $block->escapeJs($code) + ) ?> </div> </div> </div> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml index 18b3c7eef746d..bbc2f95825127 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml @@ -6,15 +6,20 @@ /** * @var $block \Magento\CurrencySymbol\Block\Adminhtml\System\Currency\Rate\Matrix + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $_oldRates = $block->getOldRates(); $_newRates = $block->getNewRates(); $_rates = ($_newRates) ? $_newRates : $_oldRates; ?> -<?php if (empty($_rates)) : ?> - <div class="message message-warning warning"><p><?= $block->escapeHtml(__('You must first configure currency options before being able to see currency rates.')) ?></p></div> -<?php else : ?> +<?php if (empty($_rates)): ?> + <div class="message message-warning warning"><p> + <?= $block->escapeHtml( + __('You must first configure currency options before being able to see currency rates.') + ) ?></p> + </div> +<?php else: ?> <form name="rateForm" id="rate-form" method="post" action="<?= $block->escapeUrl($block->getRatesFormAction()) ?>"> <?= $block->getBlockHtml('formkey') ?> <div class="admin__control-table-wrapper"> @@ -22,36 +27,53 @@ $_rates = ($_newRates) ? $_newRates : $_oldRates; <thead> <tr> <th> </th> - <?php $_i = 0; foreach ($block->getAllowedCurrencies() as $_currencyCode) : ?> + <?php $_i = 0; foreach ($block->getAllowedCurrencies() as $_currencyCode): ?> <th><span><?= $block->escapeHtml($_currencyCode) ?></span></th> <?php endforeach; ?> </tr> </thead> - <?php $_j = 0; foreach ($block->getDefaultCurrencies() as $_currencyCode) : ?> + <?php $_j = 0; foreach ($block->getDefaultCurrencies() as $_currencyCode): ?> <tr> - <?php if (isset($_rates[$_currencyCode]) && is_array($_rates[$_currencyCode])) : ?> - <?php foreach ($_rates[$_currencyCode] as $_rate => $_value) : ?> - <?php if (++$_j == 1) : ?> - <td><span class="admin__control-support-text"><?= $block->escapeHtml($_currencyCode) ?></span></td> + <?php if (isset($_rates[$_currencyCode]) && is_array($_rates[$_currencyCode])): ?> + <?php foreach ($_rates[$_currencyCode] as $_rate => $_value): ?> + <?php if (++$_j == 1): ?> + <td><span class="admin__control-support-text"><?= $block->escapeHtml($_currencyCode) ?> + </span></td> <td> <input type="text" - name="rate[<?= $block->escapeHtmlAttr($_currencyCode) ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" - value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $block->escapeHtmlAttr($_value) : (isset($_oldRates[$_currencyCode][$_rate]) ? $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) ?>" + name="rate[<?= $block->escapeHtmlAttr($_currencyCode) + ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" + value="<?= ($_currencyCode == $_rate) ? '1.0000' : + ($_value>0 ? $block->escapeHtmlAttr($_value) : + (isset($_oldRates[$_currencyCode][$_rate]) ? + $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) + ?>" class="admin__control-text" <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> - <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])) : ?> - <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong></div> + <?php if (isset($_newRates) && $_currencyCode != $_rate && + isset($_oldRates[$_currencyCode][$_rate])): ?> + <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> + <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong> + </div> <?php endif; ?> </td> - <?php else : ?> + <?php else: ?> <td> <input type="text" - name="rate[<?= $block->escapeHtmlAttr($_currencyCode) ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" - value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $block->escapeHtmlAttr($_value) : (isset($_oldRates[$_currencyCode][$_rate]) ? $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) ?>" + name="rate[<?= $block->escapeHtmlAttr($_currencyCode) + ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" + value="<?= ($_currencyCode == $_rate) ? '1.0000' : + ($_value>0 ? $block->escapeHtmlAttr($_value) : + (isset($_oldRates[$_currencyCode][$_rate]) ? + $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) + ?>" class="admin__control-text" <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> - <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])) : ?> - <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong></div> + <?php if (isset($_newRates) && $_currencyCode != $_rate && + isset($_oldRates[$_currencyCode][$_rate])): ?> + <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> + <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong> + </div> <?php endif; ?> </td> <?php endif; ?> @@ -64,10 +86,12 @@ $_rates = ($_newRates) ? $_newRates : $_oldRates; </div> </form> <?php endif; ?> -<script> +<?php $scriptString = <<<script require(['jquery', "mage/mage"], function(jQuery){ jQuery('#rate-form').mage('form').mage('validation'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Customer/Api/CustomerGroupConfigInterface.php b/app/code/Magento/Customer/Api/CustomerGroupConfigInterface.php index 6e118b2b40e76..ccbf06206aed7 100644 --- a/app/code/Magento/Customer/Api/CustomerGroupConfigInterface.php +++ b/app/code/Magento/Customer/Api/CustomerGroupConfigInterface.php @@ -9,7 +9,7 @@ * Interface for system configuration operations for customer groups. * * @api - * @since 100.2.0 + * @since 101.0.0 */ interface CustomerGroupConfigInterface { @@ -22,7 +22,7 @@ interface CustomerGroupConfigInterface * @throws \Exception * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 101.0.0 */ public function setDefaultCustomerGroup($id); } diff --git a/app/code/Magento/Customer/Api/SessionCleanerInterface.php b/app/code/Magento/Customer/Api/SessionCleanerInterface.php new file mode 100644 index 0000000000000..eb24712105f96 --- /dev/null +++ b/app/code/Magento/Customer/Api/SessionCleanerInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Api; + +/** + * Interface for cleaning customer session data. + */ +interface SessionCleanerInterface +{ + /** + * Destroy all active customer sessions related to given customer id, including current session. + * + * @param int $customerId + * @return void + */ + public function clearFor(int $customerId): void; +} diff --git a/app/code/Magento/Customer/Block/Account/AuthenticationPopup.php b/app/code/Magento/Customer/Block/Account/AuthenticationPopup.php index 07e0704ee6e43..6695d7fd6d3e8 100644 --- a/app/code/Magento/Customer/Block/Account/AuthenticationPopup.php +++ b/app/code/Magento/Customer/Block/Account/AuthenticationPopup.php @@ -70,7 +70,7 @@ public function getConfig() * Added in scope of https://github.com/magento/magento2/pull/8617 * * @return bool|string - * @since 100.2.0 + * @since 101.0.0 */ public function getSerializedConfig() { diff --git a/app/code/Magento/Customer/Block/Account/AuthorizationLink.php b/app/code/Magento/Customer/Block/Account/AuthorizationLink.php index ff9d56c8fc4cb..16ab9d26450b1 100644 --- a/app/code/Magento/Customer/Block/Account/AuthorizationLink.php +++ b/app/code/Magento/Customer/Block/Account/AuthorizationLink.php @@ -94,7 +94,7 @@ public function isLoggedIn() /** * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function getSortOrder() { diff --git a/app/code/Magento/Customer/Block/Account/Dashboard.php b/app/code/Magento/Customer/Block/Account/Dashboard.php index 92537009175fd..7281c9fc7b78d 100644 --- a/app/code/Magento/Customer/Block/Account/Dashboard.php +++ b/app/code/Magento/Customer/Block/Account/Dashboard.php @@ -120,7 +120,7 @@ public function getAddressEditUrl($address) * Retrieve the Url for customer orders. * * @return string - * @deprecated Action does not exist + * @deprecated 102.0.3 Action does not exist */ public function getOrdersUrl() { diff --git a/app/code/Magento/Customer/Block/Account/Delimiter.php b/app/code/Magento/Customer/Block/Account/Delimiter.php index 056a53e259c49..2bd93668cc438 100644 --- a/app/code/Magento/Customer/Block/Account/Delimiter.php +++ b/app/code/Magento/Customer/Block/Account/Delimiter.php @@ -10,13 +10,13 @@ * Class for delimiter. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class Delimiter extends \Magento\Framework\View\Element\Template implements SortLinkInterface { /** * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function getSortOrder() { diff --git a/app/code/Magento/Customer/Block/Account/Link.php b/app/code/Magento/Customer/Block/Account/Link.php index ed29a10abc8b7..60ade7fe9207c 100644 --- a/app/code/Magento/Customer/Block/Account/Link.php +++ b/app/code/Magento/Customer/Block/Account/Link.php @@ -45,7 +45,7 @@ public function getHref() /** * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function getSortOrder() { diff --git a/app/code/Magento/Customer/Block/Account/Navigation.php b/app/code/Magento/Customer/Block/Account/Navigation.php index 705acbcda4c6a..9a8aa698eaa47 100644 --- a/app/code/Magento/Customer/Block/Account/Navigation.php +++ b/app/code/Magento/Customer/Block/Account/Navigation.php @@ -7,20 +7,20 @@ namespace Magento\Customer\Block\Account; -use \Magento\Framework\View\Element\Html\Links; -use \Magento\Customer\Block\Account\SortLinkInterface; +use Magento\Framework\View\Element\Html\Links; +use Magento\Customer\Block\Account\SortLinkInterface; /** * Class for sorting links in navigation panels. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class Navigation extends Links { /** - * {@inheritdoc} - * @since 100.2.0 + * @inheritdoc + * @since 101.0.0 */ public function getLinks() { @@ -47,6 +47,6 @@ public function getLinks() */ private function compare(SortLinkInterface $firstLink, SortLinkInterface $secondLink): int { - return $secondLink->getSortOrder() <=> $firstLink->getSortOrder(); + return $secondLink->getSortOrder() <=> $firstLink->getSortOrder(); } } diff --git a/app/code/Magento/Customer/Block/Account/SortLinkInterface.php b/app/code/Magento/Customer/Block/Account/SortLinkInterface.php index 114bb02e1444c..5dc59aaf95854 100644 --- a/app/code/Magento/Customer/Block/Account/SortLinkInterface.php +++ b/app/code/Magento/Customer/Block/Account/SortLinkInterface.php @@ -9,7 +9,7 @@ /** * Interface for sortable links. * @api - * @since 100.2.0 + * @since 101.0.0 */ interface SortLinkInterface { @@ -23,7 +23,7 @@ interface SortLinkInterface * Get sort order for block. * * @return int - * @since 100.2.0 + * @since 101.0.0 */ public function getSortOrder(); } diff --git a/app/code/Magento/Customer/Block/Address/Book.php b/app/code/Magento/Customer/Block/Address/Book.php index 04669446ffee9..f37ae21a9b83c 100644 --- a/app/code/Magento/Customer/Block/Address/Book.php +++ b/app/code/Magento/Customer/Block/Address/Book.php @@ -93,7 +93,7 @@ protected function _prepareLayout() * Generate and return "New Address" URL * * @return string - * @deprecated not used in this block + * @deprecated 102.0.1 not used in this block * @see \Magento\Customer\Block\Address\Grid::getAddAddressUrl */ public function getAddAddressUrl() @@ -118,7 +118,7 @@ public function getBackUrl() * Generate and return "Delete" URL * * @return string - * @deprecated not used in this block + * @deprecated 102.0.1 not used in this block * @see \Magento\Customer\Block\Address\Grid::getDeleteUrl */ public function getDeleteUrl() @@ -133,7 +133,7 @@ public function getDeleteUrl() * * @param int $addressId * @return string - * @deprecated not used in this block + * @deprecated 102.0.1 not used in this block * @see \Magento\Customer\Block\Address\Grid::getAddressEditUrl */ public function getAddressEditUrl($addressId) @@ -159,7 +159,7 @@ public function hasPrimaryAddress() * * @return \Magento\Customer\Api\Data\AddressInterface[]|bool * @throws \Magento\Framework\Exception\LocalizedException - * @deprecated not used in this block + * @deprecated 102.0.1 not used in this block * @see \Magento\Customer\Block\Address\Grid::getAdditionalAddresses */ public function getAdditionalAddresses() diff --git a/app/code/Magento/Customer/Block/Address/Edit.php b/app/code/Magento/Customer/Block/Address/Edit.php index ef9937a0cde8b..9ac1870cd17d9 100644 --- a/app/code/Magento/Customer/Block/Address/Edit.php +++ b/app/code/Magento/Customer/Block/Address/Edit.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Block\Address; use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Helper\Address; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; @@ -69,6 +70,7 @@ class Edit extends \Magento\Directory\Block\Data * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper * @param array $data * @param AddressMetadataInterface|null $addressMetadata + * @param Address|null $addressHelper * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -85,7 +87,8 @@ public function __construct( \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, array $data = [], - AddressMetadataInterface $addressMetadata = null + AddressMetadataInterface $addressMetadata = null, + Address $addressHelper = null ) { $this->_customerSession = $customerSession; $this->_addressRepository = $addressRepository; @@ -93,6 +96,8 @@ public function __construct( $this->currentCustomer = $currentCustomer; $this->dataObjectHelper = $dataObjectHelper; $this->addressMetadata = $addressMetadata ?: ObjectManager::getInstance()->get(AddressMetadataInterface::class); + $data['addressHelper'] = $addressHelper ?: ObjectManager::getInstance()->get(Address::class); + $data['directoryHelper'] = $directoryHelper; parent::__construct( $context, $directoryHelper, diff --git a/app/code/Magento/Customer/Block/Address/Grid.php b/app/code/Magento/Customer/Block/Address/Grid.php index 963efc648d94b..9053fd57154bb 100644 --- a/app/code/Magento/Customer/Block/Address/Grid.php +++ b/app/code/Magento/Customer/Block/Address/Grid.php @@ -15,6 +15,7 @@ * Customer address grid * * @api + * @since 102.0.1 */ class Grid extends \Magento\Framework\View\Element\Template { @@ -64,6 +65,7 @@ public function __construct( * * @return void * @throws \Magento\Framework\Exception\LocalizedException + * @since 102.0.1 */ protected function _prepareLayout(): void { @@ -75,6 +77,7 @@ protected function _prepareLayout(): void * Generate and return "New Address" URL * * @return string + * @since 102.0.1 */ public function getAddAddressUrl(): string { @@ -85,6 +88,7 @@ public function getAddAddressUrl(): string * Generate and return "Delete" URL * * @return string + * @since 102.0.1 */ public function getDeleteUrl(): string { @@ -98,6 +102,7 @@ public function getDeleteUrl(): string * * @param int $addressId * @return string + * @since 102.0.1 */ public function getAddressEditUrl($addressId): string { @@ -112,6 +117,7 @@ public function getAddressEditUrl($addressId): string * @return \Magento\Customer\Api\Data\AddressInterface[] * @throws \Magento\Framework\Exception\LocalizedException * @throws NoSuchEntityException + * @since 102.0.1 */ public function getAdditionalAddresses(): array { @@ -132,6 +138,7 @@ public function getAdditionalAddresses(): array * Return stored customer or get it from session * * @return \Magento\Customer\Api\Data\CustomerInterface + * @since 102.0.1 */ public function getCustomer(): \Magento\Customer\Api\Data\CustomerInterface { @@ -148,6 +155,7 @@ public function getCustomer(): \Magento\Customer\Api\Data\CustomerInterface * * @param \Magento\Customer\Api\Data\AddressInterface $address * @return string + * @since 102.0.1 */ public function getStreetAddress(\Magento\Customer\Api\Data\AddressInterface $address): string { @@ -165,6 +173,7 @@ public function getStreetAddress(\Magento\Customer\Api\Data\AddressInterface $ad * * @param string $countryCode * @return string + * @since 102.0.1 */ public function getCountryByCode(string $countryCode): string { diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php index 0aeed1562c51e..ad1e7989239f3 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php @@ -5,6 +5,9 @@ */ namespace Magento\Customer\Block\Adminhtml\Edit\Renderer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Customer address region field renderer */ @@ -16,18 +19,26 @@ class Region extends \Magento\Backend\Block\AbstractBlock implements */ protected $_directoryHelper; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Directory\Helper\Data $directoryHelper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Directory\Helper\Data $directoryHelper, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_directoryHelper = $directoryHelper; parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -60,14 +71,14 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele $selectId . '" name="' . $selectName . - '" class="select required-entry admin__control-select" style="display:none">'; + '" class="select required-entry admin__control-select">'; $html .= '<option value="">' . __('Please select') . '</option>'; $html .= '</select>'; - $html .= '<script>' . "\n"; - $html .= 'require(["prototype", "mage/adminhtml/form"], function(){'; - $html .= '$("' . $selectId . '").setAttribute("defaultValue", "' . $regionId . '");' . "\n"; - $html .= 'new regionUpdater("' . + $scriptString = "\ndocument.querySelector('#$selectId').style.display = 'none';\n"; + $scriptString .= 'require(["prototype", "mage/adminhtml/form"], function(){'; + $scriptString .= '$("' . $selectId . '").setAttribute("defaultValue", "' . $regionId . '");' . "\n"; + $scriptString .= 'new regionUpdater("' . $country->getHtmlId() . '", "' . $element->getHtmlId() . @@ -78,8 +89,9 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele ');' . "\n"; - $html .= '});'; - $html .= '</script>' . "\n"; + $scriptString .= '});'; + $scriptString .= "\n"; + $html .= $this->secureRenderer->renderTag('script', [], $scriptString, false); $html .= '</div></div>' . "\n"; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php index 656a78d1165e3..799d6e3bc1263 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php @@ -316,6 +316,7 @@ private function prepareWebsiteFilter(): void /** * @inheritDoc + * @since 103.0.0 */ public function getMainButtonsHtml() { diff --git a/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php b/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php index bd6e8b69a29ea..726daf69dc587 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php @@ -5,6 +5,10 @@ */ namespace Magento\Customer\Block\Adminhtml\Grid\Renderer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Adminhtml customers wishlist grid item action renderer for few action controls in one cell * @@ -12,6 +16,32 @@ */ class Multiaction extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Action { + + /** + * @var SecureHtmlRenderer + */ + private $secureHtmlRenderer; + + /** + * @var Random + */ + private $random; + + /** + * @inheritDoc + */ + public function __construct( + \Magento\Backend\Block\Context $context, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + array $data = [], + ?SecureHtmlRenderer $secureHtmlRenderer = null, + ?Random $random = null + ) { + parent::__construct($context, $jsonEncoder, $data, $secureHtmlRenderer, $random); + $this->secureHtmlRenderer = $secureHtmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + } + /** * Renders column * @@ -55,9 +85,15 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row) if (isset($action['process']) && $action['process'] == 'configurable') { if ($product->canConfigure()) { - $style = ''; - $onClick = sprintf('onclick="return %s.configureItem(%s)"', $action['control_object'], $row->getId()); - return sprintf('<a href="%s" %s %s>%s</a>', $action['url'], $style, $onClick, $action['caption']); + $id = 'id' .$this->random->getRandomString(10); + $onClick = sprintf('return %s.configureItem(%s)', $action['control_object'], $row->getId()); + return sprintf( + '<a href="%s" id="%s" class="configure-item-link">%s</a>%s', + $action['url'], + $id, + $action['caption'], + $this->secureHtmlRenderer->renderEventListenerAsTag('onclick', $onClick, "#$id") + ); } else { return false; } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php b/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php index 9ee856f6e0af9..ebdf0090fe1c8 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php @@ -5,7 +5,9 @@ */ namespace Magento\Customer\Block\Adminhtml\Sales\Order\Address\Form\Renderer; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * VAT ID element renderer @@ -31,18 +33,26 @@ class Vat extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element */ protected $_jsonEncoder; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -95,11 +105,8 @@ public function getValidateButton() ); $optionsVarName = $this->getJsVariablePrefix() . 'VatParameters'; - $beforeHtml = '<script>var ' . - $optionsVarName . - ' = ' . - $vatValidateOptions . - ';</script>'; + $scriptString = 'var ' . $optionsVarName . ' = ' . $vatValidateOptions . ';'; + $beforeHtml = $this->secureRenderer->renderTag('script', [], $scriptString, false); $this->_validateButton = $this->getLayout()->createBlock( \Magento\Backend\Block\Widget\Button::class )->setData( @@ -110,6 +117,7 @@ public function getValidateButton() ] ); } + return $this->_validateButton; } } diff --git a/app/code/Magento/Customer/Block/CustomerData.php b/app/code/Magento/Customer/Block/CustomerData.php index 98eb2d9f9ea40..bbc54cfa71c09 100644 --- a/app/code/Magento/Customer/Block/CustomerData.php +++ b/app/code/Magento/Customer/Block/CustomerData.php @@ -59,7 +59,7 @@ public function getCustomerDataUrl($route) * Once this period has expired the corresponding section must be invalidated and reloaded. * * @return int section lifetime in minutes - * @since 100.2.0 + * @since 101.0.0 */ public function getExpirableSectionLifetime() { @@ -70,7 +70,7 @@ public function getExpirableSectionLifetime() * Retrieve the list of sections that can expire. * * @return array - * @since 100.2.0 + * @since 101.0.0 */ public function getExpirableSectionNames() { diff --git a/app/code/Magento/Customer/Block/CustomerScopeData.php b/app/code/Magento/Customer/Block/CustomerScopeData.php index f875386695b4b..63ff863f89ce3 100644 --- a/app/code/Magento/Customer/Block/CustomerScopeData.php +++ b/app/code/Magento/Customer/Block/CustomerScopeData.php @@ -15,7 +15,7 @@ * with appropriate value in store front private cache. * * @api - * @since 100.2.0 + * @since 100.1.9 */ class CustomerScopeData extends \Magento\Framework\View\Element\Template { @@ -54,7 +54,7 @@ public function __construct( * Can be used when necessary to obtain website id of the current customer. * * @return integer - * @since 100.2.0 + * @since 100.1.9 */ public function getWebsiteId() { @@ -67,6 +67,7 @@ public function getWebsiteId() * @param array $configuration * @return bool|string * @throws \InvalidArgumentException + * @since 102.0.0 */ public function encodeConfiguration(array $configuration) { diff --git a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php index 2be340c8ccca4..b4c737f6600bf 100644 --- a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php +++ b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php @@ -52,7 +52,7 @@ public function getFrontendLabel(string $attributeCode): string { try { $attribute = $this->addressMetadata->getAttributeMetadata($attributeCode); - $frontendLabel = $attribute->getFrontendLabel(); + $frontendLabel = $attribute->getStoreLabel() ?: $attribute->getFrontendLabel(); } catch (NoSuchEntityException $e) { $frontendLabel = ''; } diff --git a/app/code/Magento/Customer/Block/Form/Register.php b/app/code/Magento/Customer/Block/Form/Register.php index 46d1088e37d0f..d6d0d9c494c11 100644 --- a/app/code/Magento/Customer/Block/Form/Register.php +++ b/app/code/Magento/Customer/Block/Form/Register.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Block\Form; +use Magento\Customer\Helper\Address; use Magento\Customer\Model\AccountManagement; use Magento\Framework\App\ObjectManager; use Magento\Newsletter\Model\Config; @@ -52,6 +53,7 @@ class Register extends \Magento\Directory\Block\Data * @param \Magento\Customer\Model\Url $customerUrl * @param array $data * @param Config $newsLetterConfig + * @param Address|null $addressHelper * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -66,8 +68,11 @@ public function __construct( \Magento\Customer\Model\Session $customerSession, \Magento\Customer\Model\Url $customerUrl, array $data = [], - Config $newsLetterConfig = null + Config $newsLetterConfig = null, + Address $addressHelper = null ) { + $data['addressHelper'] = $addressHelper ?: ObjectManager::getInstance()->get(Address::class); + $data['directoryHelper'] = $directoryHelper; $this->_customerUrl = $customerUrl; $this->_moduleManager = $moduleManager; $this->_customerSession = $customerSession; diff --git a/app/code/Magento/Customer/Controller/AbstractAccount.php b/app/code/Magento/Customer/Controller/AbstractAccount.php index 4f2c80711d292..21357f0505f7d 100644 --- a/app/code/Magento/Customer/Controller/AbstractAccount.php +++ b/app/code/Magento/Customer/Controller/AbstractAccount.php @@ -12,7 +12,7 @@ * AbstractAccount class is deprecated, in favour of Composition approach to build Controllers * * @SuppressWarnings(PHPMD.NumberOfChildren) - * @deprecated + * @deprecated 103.0.0 * @see \Magento\Customer\Controller\AccountInterface */ abstract class AbstractAccount extends Action implements AccountInterface diff --git a/app/code/Magento/Customer/Controller/Account/Confirm.php b/app/code/Magento/Customer/Controller/Account/Confirm.php index a1ec3164a3c31..2fc6ed4d422fb 100644 --- a/app/code/Magento/Customer/Controller/Account/Confirm.php +++ b/app/code/Magento/Customer/Controller/Account/Confirm.php @@ -108,7 +108,7 @@ public function __construct( /** * Retrieve cookie manager * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return \Magento\Framework\Stdlib\Cookie\PhpCookieManager */ private function getCookieManager() @@ -124,7 +124,7 @@ private function getCookieManager() /** * Retrieve cookie metadata factory * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory */ private function getCookieMetadataFactory() diff --git a/app/code/Magento/Customer/Controller/Account/CreatePost.php b/app/code/Magento/Customer/Controller/Account/CreatePost.php index 4c1cfa94c5565..14c2ed43171f6 100644 --- a/app/code/Magento/Customer/Controller/Account/CreatePost.php +++ b/app/code/Magento/Customer/Controller/Account/CreatePost.php @@ -452,7 +452,7 @@ protected function checkPasswordConfirmation($password, $confirmation) /** * Retrieve success message * - * @deprecated + * @deprecated 102.0.4 * @see getMessageManagerSuccessMessage() * @return string */ diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Index.php index ffae1e9f8bf1e..51dc39a2fc658 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index.php @@ -35,7 +35,7 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Framework\Validator - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_validator; @@ -53,13 +53,13 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Customer\Model\CustomerFactory - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_customerFactory = null; /** * @var \Magento\Customer\Model\AddressFactory - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_addressFactory = null; @@ -85,7 +85,7 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Framework\Math\Random - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_random; @@ -96,7 +96,7 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Framework\Api\ExtensibleDataObjectConverter - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_extensibleDataObjectConverter; @@ -132,7 +132,7 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Framework\Reflection\DataObjectProcessor - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $dataObjectProcessor; @@ -143,7 +143,7 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Framework\View\LayoutFactory - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $layoutFactory; @@ -306,7 +306,7 @@ protected function _addSessionErrorMessages($messages) * @param callable $singleAction A single action callable that takes a customer ID as input * @param int[] $customerIds Array of customer Ids to perform the action upon * @return int Number of customers successfully acted upon - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function actUponMultipleCustomers(callable $singleAction, $customerIds) { diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Cart.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Cart.php index 6528ac4c1f211..910f4e94b90b7 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Cart.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Cart.php @@ -42,7 +42,7 @@ * Admin customer shopping cart controller * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ class Cart extends BaseAction implements HttpGetActionInterface, HttpPostActionInterface { diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index 977d3753ded65..192a0f1362ecd 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -243,7 +243,7 @@ protected function _extractData( /** * Saves default_billing and default_shipping flags for customer address * - * @deprecated must be removed because addresses are save separately for now + * @deprecated 102.0.1 must be removed because addresses are save separately for now * @param array $addressIdList * @param array $extractedCustomerData * @return array @@ -286,7 +286,7 @@ protected function saveDefaultFlags(array $addressIdList, array &$extractedCusto /** * Reformat customer addresses data to be compatible with customer service interface * - * @deprecated addresses are saved separately for now + * @deprecated 102.0.1 addresses are saved separately for now * @param array $extractedCustomerData * @return array */ diff --git a/app/code/Magento/Customer/Controller/Section/Load.php b/app/code/Magento/Customer/Controller/Section/Load.php index 6c3aa06b9f022..e735366d0b8b8 100644 --- a/app/code/Magento/Customer/Controller/Section/Load.php +++ b/app/code/Magento/Customer/Controller/Section/Load.php @@ -23,7 +23,7 @@ class Load extends \Magento\Framework\App\Action\Action implements HttpGetAction /** * @var Identifier - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $sectionIdentifier; diff --git a/app/code/Magento/Customer/CustomerData/SectionPool.php b/app/code/Magento/Customer/CustomerData/SectionPool.php index eef2854cf363e..28d79bf42ecd7 100644 --- a/app/code/Magento/Customer/CustomerData/SectionPool.php +++ b/app/code/Magento/Customer/CustomerData/SectionPool.php @@ -66,6 +66,7 @@ public function getSectionsData(array $sectionNames = null, $forceNewTimestamp = * Return array of section names. * * @return array + * @since 102.0.4 */ public function getSectionNames() { diff --git a/app/code/Magento/Customer/Helper/Address.php b/app/code/Magento/Customer/Helper/Address.php index 765c13b287704..74eee759b4abd 100644 --- a/app/code/Magento/Customer/Helper/Address.php +++ b/app/code/Magento/Customer/Helper/Address.php @@ -81,7 +81,7 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper /** * @var CustomerMetadataInterface * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_customerMetadataService; @@ -407,7 +407,7 @@ public function isVatAttributeVisible() * @return bool * @throws NoSuchEntityException * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 101.0.0 */ public function isAttributeVisible($code) { diff --git a/app/code/Magento/Customer/Model/Account/Redirect.php b/app/code/Magento/Customer/Model/Account/Redirect.php index 0389a380b36dc..9824be73f36b5 100644 --- a/app/code/Magento/Customer/Model/Account/Redirect.php +++ b/app/code/Magento/Customer/Model/Account/Redirect.php @@ -58,7 +58,7 @@ class Redirect protected $customerUrl; /** - * @deprecated 100.1.8 + * @deprecated 100.0.2 * @var UrlInterface */ protected $url; diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index 122a062beeff8..d22a10145c7be 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -13,6 +13,7 @@ use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; +use Magento\Customer\Api\SessionCleanerInterface; use Magento\Customer\Helper\View as CustomerViewHelper; use Magento\Customer\Model\Config\Share as ConfigShare; use Magento\Customer\Model\Customer as CustomerModel; @@ -68,55 +69,55 @@ class AccountManagement implements AccountManagementInterface /** * Configuration paths for create account email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE */ const XML_PATH_REGISTER_EMAIL_TEMPLATE = 'customer/create_account/email_template'; /** * Configuration paths for register no password email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE */ const XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE = 'customer/create_account/email_no_password_template'; /** * Configuration paths for remind email identity * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_REGISTER_EMAIL_IDENTITY */ const XML_PATH_REGISTER_EMAIL_IDENTITY = 'customer/create_account/email_identity'; /** * Configuration paths for remind email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_REMIND_EMAIL_TEMPLATE */ const XML_PATH_REMIND_EMAIL_TEMPLATE = 'customer/password/remind_email_template'; /** * Configuration paths for forgot email email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_FORGOT_EMAIL_TEMPLATE */ const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'customer/password/forgot_email_template'; /** * Configuration paths for forgot email identity * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_FORGOT_EMAIL_IDENTITY */ const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity'; /** * Configuration paths for account confirmation required * - * @deprecated Get rid of Helpers in Password Security Management + * @deprecated get rid of Helpers in Password Security Management. * @see AccountConfirmation::XML_PATH_IS_CONFIRM */ const XML_PATH_IS_CONFIRM = 'customer/create_account/confirm'; @@ -124,48 +125,48 @@ class AccountManagement implements AccountManagementInterface /** * Configuration paths for account confirmation email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE */ const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template'; /** * Configuration paths for confirmation confirmed email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_CONFIRMED_EMAIL_TEMPLATE */ const XML_PATH_CONFIRMED_EMAIL_TEMPLATE = 'customer/create_account/email_confirmed_template'; /** * Constants for the type of new account email to be sent * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED */ const NEW_ACCOUNT_EMAIL_REGISTERED = 'registered'; /** * Welcome email, when password setting is required * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD */ const NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD = 'registered_no_password'; /** * Welcome email, when confirmation is enabled * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotificationInterface::NEW_ACCOUNT_EMAIL_CONFIRMATION */ const NEW_ACCOUNT_EMAIL_CONFIRMATION = 'confirmation'; /** * Confirmation email, when account is confirmed * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotificationInterface::NEW_ACCOUNT_EMAIL_CONFIRMED */ const NEW_ACCOUNT_EMAIL_CONFIRMED = 'confirmed'; @@ -191,15 +192,16 @@ class AccountManagement implements AccountManagementInterface /** * Configuration path to customer reset password email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see Magento/Customer/Model/EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_RESET_PASSWORD_TEMPLATE */ const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template'; /** * Minimum password length * - * @deprecated Get rid of Helpers in Password Security Management + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\AccountManagement::XML_PATH_MINIMUM_PASSWORD_LENGTH */ const MIN_PASSWORD_LENGTH = 6; @@ -283,21 +285,6 @@ class AccountManagement implements AccountManagementInterface */ private $transportBuilder; - /** - * @var SessionManagerInterface - */ - private $sessionManager; - - /** - * @var SaveHandlerInterface - */ - private $saveHandler; - - /** - * @var CollectionFactory - */ - private $visitorCollectionFactory; - /** * @var DataObjectProcessor */ @@ -383,6 +370,11 @@ class AccountManagement implements AccountManagementInterface */ private $getByToken; + /** + * @var SessionCleanerInterface + */ + private $sessionCleaner; + /** * @param CustomerFactory $customerFactory * @param ManagerInterface $eventManager @@ -417,10 +409,12 @@ class AccountManagement implements AccountManagementInterface * @param AddressRegistry|null $addressRegistry * @param GetCustomerByToken|null $getByToken * @param AllowedCountries|null $allowedCountriesReader + * @param SessionCleanerInterface|null $sessionCleaner * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.LongVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( CustomerFactory $customerFactory, @@ -455,7 +449,8 @@ public function __construct( SearchCriteriaBuilder $searchCriteriaBuilder = null, AddressRegistry $addressRegistry = null, GetCustomerByToken $getByToken = null, - AllowedCountries $allowedCountriesReader = null + AllowedCountries $allowedCountriesReader = null, + SessionCleanerInterface $sessionCleaner = null ) { $this->customerFactory = $customerFactory; $this->eventManager = $eventManager; @@ -486,12 +481,6 @@ public function __construct( $this->dateTimeFactory = $dateTimeFactory ?: $objectManager->get(DateTimeFactory::class); $this->accountConfirmation = $accountConfirmation ?: $objectManager ->get(AccountConfirmation::class); - $this->sessionManager = $sessionManager - ?: $objectManager->get(SessionManagerInterface::class); - $this->saveHandler = $saveHandler - ?: $objectManager->get(SaveHandlerInterface::class); - $this->visitorCollectionFactory = $visitorCollectionFactory - ?: $objectManager->get(CollectionFactory::class); $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: $objectManager->get(SearchCriteriaBuilder::class); $this->addressRegistry = $addressRegistry @@ -500,6 +489,7 @@ public function __construct( ?: $objectManager->get(GetCustomerByToken::class); $this->allowedCountriesReader = $allowedCountriesReader ?: $objectManager->get(AllowedCountries::class); + $this->sessionCleaner = $sessionCleaner ?? $objectManager->get(SessionCleanerInterface::class); } /** @@ -538,7 +528,10 @@ public function resendConfirmation($email, $websiteId = null, $redirectUrl = '') } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored $this->logger->critical($e); + + return false; } + return true; } @@ -685,16 +678,18 @@ public function initiatePasswordReset($email, $template, $websiteId = null) */ private function handleUnknownTemplate($template) { - $phrase = __( - 'Invalid value of "%value" provided for the %fieldName field. Possible values: %template1 or %template2.', - [ - 'value' => $template, - 'fieldName' => 'template', - 'template1' => AccountManagement::EMAIL_REMINDER, - 'template2' => AccountManagement::EMAIL_RESET - ] + throw new InputException( + __( + 'Invalid value of "%value" provided for the %fieldName field. ' + . 'Possible values: %template1 or %template2.', + [ + 'value' => $template, + 'fieldName' => 'template', + 'template1' => AccountManagement::EMAIL_REMINDER, + 'template2' => AccountManagement::EMAIL_RESET + ] + ) ); - throw new InputException($phrase); } /** @@ -725,7 +720,7 @@ public function resetPassword($email, $resetToken, $newPassword) $customerSecure->setRpToken(null); $customerSecure->setRpTokenCreatedAt(null); $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); - $this->destroyCustomerSessions($customer->getId()); + $this->sessionCleaner->clearFor((int)$customer->getId()); $this->customerRepository->save($customer); return true; @@ -872,6 +867,7 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash if ($customer->getId()) { $customer = $this->customerRepository->get($customer->getEmail()); $websiteId = $customer->getWebsiteId(); + if ($this->isCustomerInStore($websiteId, $customer->getStoreId())) { throw new InputException(__('This customer already exists in this store.')); } @@ -1050,7 +1046,7 @@ private function changePasswordForCustomer($customer, $currentPassword, $newPass $customerSecure->setRpToken(null); $customerSecure->setRpTokenCreatedAt(null); $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); - $this->destroyCustomerSessions($customer->getId()); + $this->sessionCleaner->clearFor((int)$customer->getId()); $this->disableAddressValidation($customer); $this->customerRepository->save($customer); @@ -1379,7 +1375,7 @@ protected function sendEmailTemplate( * * @param CustomerInterface $customer * @return bool - * @deprecated + * @deprecated 101.0.4 * @see AccountConfirmation::isConfirmationRequired */ protected function isConfirmationRequired($customer) @@ -1396,7 +1392,7 @@ protected function isConfirmationRequired($customer) * * @param CustomerInterface $customer * @return bool - * @deprecated + * @deprecated 101.0.4 * @see AccountConfirmation::isConfirmationRequired */ protected function canSkipConfirmation($customer) diff --git a/app/code/Magento/Customer/Model/Address.php b/app/code/Magento/Customer/Model/Address.php index ea9b103f42273..241abbb06f8a1 100644 --- a/app/code/Magento/Customer/Model/Address.php +++ b/app/code/Magento/Customer/Model/Address.php @@ -395,7 +395,7 @@ private function getAttributeList() * Retrieve attribute set id for customer address. * * @return int - * @since 100.2.0 + * @since 101.0.0 */ public function getAttributeSetId() { diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php index fb067decd0b37..8421fc92f8c4a 100644 --- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php +++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php @@ -632,7 +632,7 @@ protected function _createCountryInstance() * Unset Region from address * * @return $this - * @since 100.2.0 + * @since 101.0.0 */ public function unsRegion() { @@ -644,7 +644,7 @@ public function unsRegion() * * @return bool * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 101.0.0 */ protected function isCompanyRequired() { @@ -656,7 +656,7 @@ protected function isCompanyRequired() * * @return bool * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 101.0.0 */ protected function isTelephoneRequired() { @@ -668,7 +668,7 @@ protected function isTelephoneRequired() * * @return bool * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 101.0.0 */ protected function isFaxRequired() { diff --git a/app/code/Magento/Customer/Model/Address/ValidatorInterface.php b/app/code/Magento/Customer/Model/Address/ValidatorInterface.php index 8468f28e70e70..1bdfd77a19311 100644 --- a/app/code/Magento/Customer/Model/Address/ValidatorInterface.php +++ b/app/code/Magento/Customer/Model/Address/ValidatorInterface.php @@ -10,6 +10,7 @@ * Interface for address validator. * * @api + * @since 102.0.0 */ interface ValidatorInterface { @@ -19,6 +20,7 @@ interface ValidatorInterface * * @param AbstractAddress $address * @return array + * @since 102.0.0 */ public function validate(AbstractAddress $address); } diff --git a/app/code/Magento/Customer/Model/AuthenticationInterface.php b/app/code/Magento/Customer/Model/AuthenticationInterface.php index f2d213be2ccfe..3c4cae3089218 100644 --- a/app/code/Magento/Customer/Model/AuthenticationInterface.php +++ b/app/code/Magento/Customer/Model/AuthenticationInterface.php @@ -11,6 +11,7 @@ /** * Interface \Magento\Customer\Model\AuthenticationInterface * @api + * @since 100.1.0 */ interface AuthenticationInterface { @@ -19,6 +20,7 @@ interface AuthenticationInterface * * @param int $customerId * @return void + * @since 100.1.0 */ public function processAuthenticationFailure($customerId); @@ -27,6 +29,7 @@ public function processAuthenticationFailure($customerId); * * @param int $customerId * @return void + * @since 100.1.0 */ public function unlock($customerId); @@ -35,6 +38,7 @@ public function unlock($customerId); * * @param int $customerId * @return boolean + * @since 100.1.0 */ public function isLocked($customerId); @@ -46,6 +50,7 @@ public function isLocked($customerId); * @return boolean * @throws InvalidEmailOrPasswordException * @throws UserLockedException + * @since 100.1.0 */ public function authenticate($customerId, $password); } diff --git a/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php b/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php index ef0b8b7163ad8..53c1b470a5175 100644 --- a/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php +++ b/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php @@ -26,7 +26,7 @@ class ConfigProvider implements ConfigProviderInterface /** * @var UrlInterface - * @deprecated + * @deprecated 101.0.4 */ protected $urlBuilder; diff --git a/app/code/Magento/Customer/Model/Config/Source/Group.php b/app/code/Magento/Customer/Model/Config/Source/Group.php index 7132b8ad4cedf..65b2e14019c40 100644 --- a/app/code/Magento/Customer/Model/Config/Source/Group.php +++ b/app/code/Magento/Customer/Model/Config/Source/Group.php @@ -17,13 +17,13 @@ class Group implements \Magento\Framework\Option\ArrayInterface protected $_options; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @var GroupManagementInterface */ protected $_groupManagement; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @var \Magento\Framework\Convert\DataObject */ protected $_converter; diff --git a/app/code/Magento/Customer/Model/Config/Source/Group/Multiselect.php b/app/code/Magento/Customer/Model/Config/Source/Group/Multiselect.php index bf1fae8d34bed..38f717b82ea35 100644 --- a/app/code/Magento/Customer/Model/Config/Source/Group/Multiselect.php +++ b/app/code/Magento/Customer/Model/Config/Source/Group/Multiselect.php @@ -19,13 +19,13 @@ class Multiselect implements \Magento\Framework\Option\ArrayInterface protected $_options; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @var GroupManagementInterface */ protected $_groupManagement; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @var \Magento\Framework\Convert\DataObject */ protected $_converter; diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php index ea52994735c63..f90b67216254d 100644 --- a/app/code/Magento/Customer/Model/Customer.php +++ b/app/code/Magento/Customer/Model/Customer.php @@ -820,7 +820,7 @@ public function sendNewAccountEmail($type = 'registered', $backUrl = '', $storeI * Check if accounts confirmation is required in config * * @return bool - * @deprecated + * @deprecated 101.0.4 * @see AccountConfirmation::isConfirmationRequired */ public function isConfirmationRequired() @@ -1000,6 +1000,7 @@ public function getSharedWebsiteIds() * Retrieve attribute set id for customer. * * @return int + * @since 102.0.1 */ public function getAttributeSetId() { @@ -1197,7 +1198,7 @@ public function setIsReadonly($value) * Check whether confirmation may be skipped when registering using certain email address * * @return bool - * @deprecated + * @deprecated 101.0.4 * @see AccountConfirmation::isConfirmationRequired */ protected function canSkipConfirmation() diff --git a/app/code/Magento/Customer/Model/Customer/Attribute/Backend/Password.php b/app/code/Magento/Customer/Model/Customer/Attribute/Backend/Password.php index a74838a1a7812..184a9ea8ed7bc 100644 --- a/app/code/Magento/Customer/Model/Customer/Attribute/Backend/Password.php +++ b/app/code/Magento/Customer/Model/Customer/Attribute/Backend/Password.php @@ -9,7 +9,7 @@ use Magento\Framework\Exception\LocalizedException; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * Customer password attribute backend */ class Password extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend @@ -69,7 +69,7 @@ public function beforeSave($object) } /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param \Magento\Framework\DataObject $object * @return bool */ 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/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index 38e597e4e0fe7..ef32ad57886fe 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -33,7 +33,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * - * @deprecated \Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses is used instead + * @deprecated 102.0.1 \Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses is used instead * @api * @since 100.0.2 */ @@ -324,7 +324,7 @@ private function canShowAttribute(AbstractAttribute $customerAttribute): bool * Retrieve Country With Websites Source * * @return CountryWithWebsites - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getCountryWithWebsiteSource() { diff --git a/app/code/Magento/Customer/Model/Customer/Source/GroupSourceInterface.php b/app/code/Magento/Customer/Model/Customer/Source/GroupSourceInterface.php index 26be387a02f9c..e7addc942811d 100644 --- a/app/code/Magento/Customer/Model/Customer/Source/GroupSourceInterface.php +++ b/app/code/Magento/Customer/Model/Customer/Source/GroupSourceInterface.php @@ -9,7 +9,7 @@ /** * @api - * @since 100.2.0 + * @since 101.0.0 */ interface GroupSourceInterface extends OptionSourceInterface { diff --git a/app/code/Magento/Customer/Model/CustomerRegistry.php b/app/code/Magento/Customer/Model/CustomerRegistry.php index d68904f6d1645..f2868132790cf 100644 --- a/app/code/Magento/Customer/Model/CustomerRegistry.php +++ b/app/code/Magento/Customer/Model/CustomerRegistry.php @@ -101,8 +101,10 @@ public function retrieve($customerId) public function retrieveByEmail($customerEmail, $websiteId = null) { if ($websiteId === null) { - $websiteId = $this->storeManager->getStore()->getWebsiteId(); + $websiteId = $this->storeManager->getStore()->getWebsiteId() + ?: $this->storeManager->getDefaultStoreView()->getWebsiteId(); } + $emailKey = $this->getEmailKey($customerEmail, $websiteId); if (isset($this->customerRegistryByEmail[$emailKey])) { return $this->customerRegistryByEmail[$emailKey]; 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/Group/RetrieverInterface.php b/app/code/Magento/Customer/Model/Group/RetrieverInterface.php index 7d2d47cae2f09..1b1e4efdb7c50 100644 --- a/app/code/Magento/Customer/Model/Group/RetrieverInterface.php +++ b/app/code/Magento/Customer/Model/Group/RetrieverInterface.php @@ -9,7 +9,7 @@ * Interface for getting current customer group from session. * * @api - * @since 100.2.0 + * @since 101.0.0 */ interface RetrieverInterface { @@ -17,7 +17,7 @@ interface RetrieverInterface * Retrieve customer group id. * * @return int - * @since 100.2.0 + * @since 101.0.0 */ public function getCustomerGroupId(); } diff --git a/app/code/Magento/Customer/Model/Metadata/Form/File.php b/app/code/Magento/Customer/Model/Metadata/Form/File.php index 227e85ed98f91..1a1c48075fce5 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/File.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/File.php @@ -57,7 +57,7 @@ class File extends AbstractData /** * @var FileProcessorFactory - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $fileProcessorFactory; diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Image.php b/app/code/Magento/Customer/Model/Metadata/Form/Image.php index 33bdf827f80fa..d023db1454906 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/Image.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/Image.php @@ -3,17 +3,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Customer\Model\Metadata\Form; use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Customer\Model\FileProcessor; +use Magento\Customer\Model\FileProcessorFactory; use Magento\Framework\Api\ArrayObjectSearch; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\UploaderFactory; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Io\File as IoFileSystem; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\WriteFactory; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\Url\EncoderInterface; +use Magento\MediaStorage\Model\File\Validator\NotProtectedExtension; +use Psr\Log\LoggerInterface; /** * Metadata for form image field @@ -27,38 +43,55 @@ class Image extends File */ private $imageContentFactory; + /** + * @var IoFileSystem + */ + private $ioFileSystem; + + /** + * @var WriteInterface + */ + private $mediaEntityTmpDirectory; + /** * Constructor * - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver + * @param TimezoneInterface $localeDate + * @param LoggerInterface $logger + * @param AttributeMetadataInterface $attribute + * @param ResolverInterface $localeResolver * @param null|string $value * @param string $entityTypeCode * @param bool $isAjax - * @param \Magento\Framework\Url\EncoderInterface $urlEncoder - * @param \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $fileValidator + * @param EncoderInterface $urlEncoder + * @param NotProtectedExtension $fileValidator * @param Filesystem $fileSystem * @param UploaderFactory $uploaderFactory - * @param \Magento\Customer\Model\FileProcessorFactory|null $fileProcessorFactory - * @param \Magento\Framework\Api\Data\ImageContentInterfaceFactory|null $imageContentInterfaceFactory + * @param FileProcessorFactory|null $fileProcessorFactory + * @param ImageContentInterfaceFactory|null $imageContentInterfaceFactory + * @param IoFileSystem|null $ioFileSystem + * @param DirectoryList|null $directoryList + * @param WriteFactory|null $writeFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @throws FileSystemException */ public function __construct( - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Psr\Log\LoggerInterface $logger, - \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute, - \Magento\Framework\Locale\ResolverInterface $localeResolver, + TimezoneInterface $localeDate, + LoggerInterface $logger, + AttributeMetadataInterface $attribute, + ResolverInterface $localeResolver, $value, $entityTypeCode, $isAjax, - \Magento\Framework\Url\EncoderInterface $urlEncoder, - \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $fileValidator, + EncoderInterface $urlEncoder, + NotProtectedExtension $fileValidator, Filesystem $fileSystem, UploaderFactory $uploaderFactory, - \Magento\Customer\Model\FileProcessorFactory $fileProcessorFactory = null, - \Magento\Framework\Api\Data\ImageContentInterfaceFactory $imageContentInterfaceFactory = null + FileProcessorFactory $fileProcessorFactory = null, + ImageContentInterfaceFactory $imageContentInterfaceFactory = null, + IoFileSystem $ioFileSystem = null, + ?DirectoryList $directoryList = null, + ?WriteFactory $writeFactory = null ) { parent::__construct( $localeDate, @@ -75,7 +108,16 @@ public function __construct( $fileProcessorFactory ); $this->imageContentFactory = $imageContentInterfaceFactory ?: ObjectManager::getInstance() - ->get(\Magento\Framework\Api\Data\ImageContentInterfaceFactory::class); + ->get(ImageContentInterfaceFactory::class); + $this->ioFileSystem = $ioFileSystem ?: ObjectManager::getInstance() + ->get(IoFileSystem::class); + $writeFactory = $writeFactory ?? ObjectManager::getInstance()->get(WriteFactory::class); + $directoryList = $directoryList ?? ObjectManager::getInstance()->get(DirectoryList::class); + $this->mediaEntityTmpDirectory = $writeFactory->create( + $directoryList->getPath($directoryList::MEDIA) + . '/' . $this->_entityTypeCode + . '/' . FileProcessor::TMP_DIR + ); } /** @@ -85,6 +127,7 @@ public function __construct( * * @param array $value * @return string[] + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -93,7 +136,11 @@ protected function _validateByRules($value) $label = $value['name']; $rules = $this->getAttribute()->getValidationRules(); - $imageProp = @getimagesize($value['tmp_name']); + try { + $imageProp = getimagesize($value['tmp_name']); + } catch (\Throwable $e) { + $imageProp = false; + } if (!$this->_isUploadedFile($value['tmp_name']) || !$imageProp) { return [__('"%1" is not a valid file.', $label)]; @@ -106,9 +153,11 @@ protected function _validateByRules($value) } // modify image name - $extension = pathinfo($value['name'], PATHINFO_EXTENSION); + $extension = $this->ioFileSystem->getPathInfo($value['name'])['extension']; if ($extension != $allowImageTypes[$imageProp[2]]) { - $value['name'] = pathinfo($value['name'], PATHINFO_FILENAME) . '.' . $allowImageTypes[$imageProp[2]]; + $value['name'] = $this->ioFileSystem->getPathInfo($value['name'])['filename'] + . '.' + . $allowImageTypes[$imageProp[2]]; } $maxFileSize = ArrayObjectSearch::getArrayElementByName( @@ -153,6 +202,7 @@ protected function _validateByRules($value) * * @param array $value * @return bool|int|ImageContentInterface|string + * @throws LocalizedException */ protected function processUiComponentValue(array $value) { @@ -174,11 +224,23 @@ protected function processUiComponentValue(array $value) * * @param array $value * @return string + * @throws LocalizedException */ protected function processCustomerAddressValue(array $value) { - $result = $this->getFileProcessor()->moveTemporaryFile($value['file']); - return $result; + $fileName = $this->mediaEntityTmpDirectory + ->getDriver() + ->getRealPathSafety( + $this->mediaEntityTmpDirectory->getAbsolutePath( + ltrim( + $value['file'], + '/' + ) + ) + ); + return $this->getFileProcessor()->moveTemporaryFile( + $this->mediaEntityTmpDirectory->getRelativePath($fileName) + ); } /** @@ -186,20 +248,19 @@ protected function processCustomerAddressValue(array $value) * * @param array $value * @return bool|int|ImageContentInterface|string + * @throws LocalizedException */ protected function processCustomerValue(array $value) { - $temporaryFile = FileProcessor::TMP_DIR . '/' . ltrim($value['file'], '/'); - - if ($this->getFileProcessor()->isExist($temporaryFile)) { + $file = ltrim($value['file'], '/'); + if ($this->mediaEntityTmpDirectory->isExist($file)) { + $temporaryFile = FileProcessor::TMP_DIR . '/' . $file; $base64EncodedData = $this->getFileProcessor()->getBase64EncodedData($temporaryFile); - /** @var ImageContentInterface $imageContentDataObject */ $imageContentDataObject = $this->imageContentFactory->create() ->setName($value['name']) ->setBase64EncodedData($base64EncodedData) ->setType($value['type']); - // Remove temporary file $this->getFileProcessor()->removeUploadedFile($temporaryFile); diff --git a/app/code/Magento/Customer/Model/Options.php b/app/code/Magento/Customer/Model/Options.php index 71e70f8e14208..4c9b9f97ad43a 100644 --- a/app/code/Magento/Customer/Model/Options.php +++ b/app/code/Magento/Customer/Model/Options.php @@ -74,7 +74,7 @@ public function getNameSuffixOptions($store = null) * @param bool $isOptional * @return array|bool * - * @deprecated + * @deprecated 101.0.4 * @see prepareNamePrefixSuffixOptions() */ protected function _prepareNamePrefixSuffixOptions($options, $isOptional = false) @@ -97,14 +97,15 @@ private function prepareNamePrefixSuffixOptions($options, $isOptional = false) if (empty($options)) { return false; } + $result = []; - $options = array_filter(explode(';', $options)); + $options = explode(';', $options); foreach ($options as $value) { - $value = $this->escaper->escapeHtml(trim($value)); - $result[$value] = $value; + $result[] = $this->escaper->escapeHtml(trim($value)) ?: ' '; } + if ($isOptional && trim(current($options))) { - $result = array_merge([' ' => ' '], $result); + $result = array_merge([' '], $result); } return $result; diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php index 9eb9ffb806c9f..271d8f795d6f6 100644 --- a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php +++ b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php @@ -6,8 +6,9 @@ namespace Magento\Customer\Model\Plugin; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Integration\Api\AuthorizationServiceInterface as AuthorizationService; +use Closure; +use Magento\Customer\Model\Customer\AuthorizationComposite; +use Magento\Framework\Authorization; /** * Plugin around \Magento\Framework\Authorization::isAllowed @@ -17,45 +18,40 @@ class CustomerAuthorization { /** - * @var UserContextInterface + * @var AuthorizationComposite */ - protected $userContext; + private $authorizationComposite; /** * Inject dependencies. - * - * @param UserContextInterface $userContext + * @param AuthorizationComposite $composite */ - public function __construct(UserContextInterface $userContext) - { - $this->userContext = $userContext; + public function __construct( + AuthorizationComposite $composite + ) { + $this->authorizationComposite = $composite; } /** - * Check if resource for which access is needed has self permissions defined in webapi config. - * - * @param \Magento\Framework\Authorization $subject - * @param callable $proceed - * @param string $resource - * @param string $privilege + * Verify if to allow customer users to access resources with self permission * - * @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 - ) { + if ($this->authorizationComposite->isAllowed($resource, $privilege)) { return true; - } else { - return $proceed($resource, $privilege); } + + return $proceed($resource, $privilege); } } diff --git a/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php b/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php new file mode 100644 index 0000000000000..fdde31e05fb2e --- /dev/null +++ b/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Model\Plugin; + +use Magento\Framework\Webapi\Rest\Request as RestRequest; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; + +/** + * Update customer by id from request param + */ +class UpdateCustomer +{ + /** + * @var RestRequest + */ + private $request; + + /** + * @param RestRequest $request + */ + public function __construct(RestRequest $request) + { + $this->request = $request; + } + + /** + * Update customer by id from request if exist + * + * @param CustomerRepositoryInterface $customerRepository + * @param CustomerInterface $customer + * @param string|null $passwordHash + * @return array + */ + public function beforeSave( + CustomerRepositoryInterface $customerRepository, + CustomerInterface $customer, + ?string $passwordHash = null + ): array { + $customerId = $this->request->getParam('customerId'); + + if ($customerId) { + $customer = $this->getUpdatedCustomer($customerRepository->getById($customerId), $customer); + } + + return [$customer, $passwordHash]; + } + + /** + * Return updated customer + * + * @param CustomerInterface $originCustomer + * @param CustomerInterface $customer + * @return CustomerInterface + */ + private function getUpdatedCustomer( + CustomerInterface $originCustomer, + CustomerInterface $customer + ): CustomerInterface { + $newCustomer = clone $originCustomer; + foreach ($customer->__toArray() as $name => $value) { + if ($name === CustomerInterface::CUSTOM_ATTRIBUTES) { + $value = $customer->getCustomAttributes(); + } elseif ($name === CustomerInterface::EXTENSION_ATTRIBUTES_KEY) { + $value = $customer->getExtensionAttributes(); + } elseif ($name === CustomerInterface::KEY_ADDRESSES) { + $value = $customer->getAddresses(); + } + + $newCustomer->setData($name, $value); + } + + return $newCustomer; + } +} diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address.php b/app/code/Magento/Customer/Model/ResourceModel/Address.php index 200eaabe6517d..8e44638e7aee8 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address.php @@ -126,7 +126,7 @@ public function delete($object) /** * Get instance of DeleteRelation class * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return DeleteRelation */ private function getDeleteRelation() @@ -137,7 +137,7 @@ private function getDeleteRelation() /** * Get instance of CustomerRegistry class * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CustomerRegistry */ private function getCustomerRegistry() diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php index 37b3b1af0a42d..62339cc7b974f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php @@ -67,7 +67,7 @@ protected function _createCountriesCollection() /** * Retrieve Store Manager - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return StoreManagerInterface */ private function getStoreManager() diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php index 0e2eb3e1d8e65..c7b44288bc85f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php @@ -185,7 +185,7 @@ public function addFieldToFilter($field, $condition = null) { if ($field === 'region') { $conditionSql = $this->_getConditionSql( - $this->getRegionNameExpresion(), + $this->getRegionNameExpression(), $condition ); $this->getSelect()->where($conditionSql); @@ -211,7 +211,7 @@ public function addFullTextFilter(string $value) $whereCondition = ''; foreach ($fields as $key => $field) { $field = $field === 'region' - ? $this->getRegionNameExpresion() + ? $this->getRegionNameExpression() : 'main_table.' . $field; $condition = $this->_getConditionSql( $this->getConnection()->quoteIdentifier($field), @@ -246,18 +246,18 @@ private function joinRegionNameTable() )->joinLeft( ['rnt' => $this->getTable('directory_country_region_name')], "rnt.region_id={$regionIdField} AND {$localeCondition}", - ['region' => $this->getRegionNameExpresion()] + ['region' => $this->getRegionNameExpression()] ); return $this; } /** - * Get SQL Expresion to define Region Name field by locale + * Get SQL Expression to define Region Name field by locale * * @return \Zend_Db_Expr */ - private function getRegionNameExpresion(): \Zend_Db_Expr + private function getRegionNameExpression(): \Zend_Db_Expr { $connection = $this->getConnection(); $defaultNameExpr = $connection->getIfNullSql( diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php index cf837e2924161..dd502abcafca9 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php @@ -142,7 +142,7 @@ private function updateCustomerRegistry(Customer $customer, array $changedAddres /** * Checks if address has chosen as default and has had an id * - * @deprecated Is not used anymore due to changes in logic of save of address. + * @deprecated 102.0.1 Is not used anymore due to changes in logic of save of address. * If address was default and becomes not default than default address id for customer must be * set to null * @param AbstractModel $object diff --git a/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php b/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php index 3fe61785de897..48828d58fca04 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php @@ -209,7 +209,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) /** * Helper function that adds a FilterGroup to the collection. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param FilterGroup $filterGroup * @param Collection $collection * @return void @@ -268,7 +268,7 @@ public function deleteById($addressId) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 0611a2df641e7..9a513493ce62f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -10,6 +10,7 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory; +use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Customer\Model\Customer as CustomerModel; use Magento\Customer\Model\Customer\NotificationStorage; use Magento\Customer\Model\CustomerFactory; @@ -27,6 +28,8 @@ use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; /** @@ -119,6 +122,11 @@ class CustomerRepository implements CustomerRepositoryInterface */ private $delegatedStorage; + /** + * @var GroupRepositoryInterface + */ + private $groupRepository; + /** * @param CustomerFactory $customerFactory * @param CustomerSecureFactory $customerSecureFactory @@ -136,6 +144,7 @@ class CustomerRepository implements CustomerRepositoryInterface * @param CollectionProcessorInterface $collectionProcessor * @param NotificationStorage $notificationStorage * @param DelegatedStorage|null $delegatedStorage + * @param GroupRepositoryInterface|null $groupRepository * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -154,7 +163,8 @@ public function __construct( JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor, NotificationStorage $notificationStorage, - DelegatedStorage $delegatedStorage = null + DelegatedStorage $delegatedStorage = null, + ?GroupRepositoryInterface $groupRepository = null ) { $this->customerFactory = $customerFactory; $this->customerSecureFactory = $customerSecureFactory; @@ -172,6 +182,7 @@ public function __construct( $this->collectionProcessor = $collectionProcessor; $this->notificationStorage = $notificationStorage; $this->delegatedStorage = $delegatedStorage ?? ObjectManager::getInstance()->get(DelegatedStorage::class); + $this->groupRepository = $groupRepository ?: ObjectManager::getInstance()->get(GroupRepositoryInterface::class); } /** @@ -216,6 +227,7 @@ public function save(CustomerInterface $customer, $passwordHash = null) $prevCustomerData ? $prevCustomerData->getStoreId() : $this->storeManager->getStore()->getId() ); } + $this->validateGroupId($customer->getGroupId()); $this->setCustomerGroupId($customerModel, $customerArr, $prevCustomerDataArr); // Need to use attribute set or future updates can cause data loss if (!$customerModel->getAttributeSetId()) { @@ -268,10 +280,7 @@ public function save(CustomerInterface $customer, $passwordHash = null) $savedAddressIds[] = $address->getId(); } } - $addressIdsToDelete = array_diff($existingAddressIds, $savedAddressIds); - foreach ($addressIdsToDelete as $addressId) { - $this->addressRepository->deleteById($addressId); - } + $this->deleteAddressesByIds(array_diff($existingAddressIds, $savedAddressIds)); } $this->customerRegistry->remove($customerId); $savedCustomer = $this->get($customer->getEmail(), $customer->getWebsiteId()); @@ -286,6 +295,39 @@ public function save(CustomerInterface $customer, $passwordHash = null) return $savedCustomer; } + /** + * Delete addresses by ids + * + * @param array $addressIds + * @return void + */ + private function deleteAddressesByIds(array $addressIds): void + { + foreach ($addressIds as $id) { + $this->addressRepository->deleteById($id); + } + } + + /** + * Validate customer group id if exist + * + * @param int|null $groupId + * @return bool + * @throws LocalizedException + */ + private function validateGroupId(?int $groupId): bool + { + if ($groupId) { + try { + $this->groupRepository->getById($groupId); + } catch (NoSuchEntityException $e) { + throw new LocalizedException(__('The specified customer group id does not exist.')); + } + } + + return true; + } + /** * Set secure data to customer model * @@ -425,7 +467,7 @@ public function deleteById($customerId) /** * Helper function that adds a FilterGroup to the collection. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param FilterGroup $filterGroup * @param Collection $collection * @return void diff --git a/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php index bf8ef767063bd..0fab27161ce25 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php @@ -74,7 +74,7 @@ public function addFieldToFilter($field, $condition = null) { if ($field === 'billing_region') { $conditionSql = $this->_getConditionSql( - $this->getRegionNameExpresion(), + $this->getRegionNameExpression(), $condition ); $this->getSelect()->where($conditionSql); @@ -100,7 +100,7 @@ public function addFullTextFilter(string $value) $whereCondition = ''; foreach ($fields as $key => $field) { $field = $field === 'billing_region' - ? $this->getRegionNameExpresion() + ? $this->getRegionNameExpression() : 'main_table.' . $field; $condition = $this->_getConditionSql( $this->getConnection()->quoteIdentifier($field), @@ -152,18 +152,18 @@ private function joinRegionNameTable() )->joinLeft( ['rnt' => $this->getTable('directory_country_region_name')], "rnt.region_id={$regionIdField} AND {$localeCondition}", - ['billing_region' => $this->getRegionNameExpresion()] + ['billing_region' => $this->getRegionNameExpression()] ); return $this; } /** - * Get SQL Expresion to define Region Name field by locale + * Get SQL Expression to define Region Name field by locale * * @return \Zend_Db_Expr */ - private function getRegionNameExpresion(): \Zend_Db_Expr + private function getRegionNameExpression(): \Zend_Db_Expr { $connection = $this->getConnection(); $defaultNameExpr = $connection->getIfNullSql( diff --git a/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php b/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php index 664f85f841e3f..10979cd9cc22a 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php @@ -220,7 +220,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) /** * Helper function that adds a FilterGroup to the collection. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param FilterGroup $filterGroup * @param Collection $collection * @return void @@ -243,7 +243,7 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collecti /** * Translates a field name to a DB column name for use in collection queries. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param string $field a field name that should be translated to a DB column name. * @return string */ @@ -343,7 +343,7 @@ protected function _verifyTaxClassModel($taxClassId, $group) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Customer/Model/Session/SessionCleaner.php b/app/code/Magento/Customer/Model/Session/SessionCleaner.php new file mode 100644 index 0000000000000..1423c94782535 --- /dev/null +++ b/app/code/Magento/Customer/Model/Session/SessionCleaner.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Session; + +use Magento\Customer\Api\SessionCleanerInterface; +use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory as VisitorCollectionFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Session\Config; +use Magento\Framework\Session\SaveHandlerInterface; +use Magento\Framework\Session\SessionManagerInterface; +use Magento\Framework\Stdlib\DateTime; +use Magento\Store\Model\ScopeInterface; + +/** + * Deletes all session data which relates to customer, including current session data. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class SessionCleaner implements SessionCleanerInterface +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + + /** + * @var VisitorCollectionFactory + */ + private $visitorCollectionFactory; + + /** + * @var SessionManagerInterface + */ + private $sessionManager; + + /** + * @var SaveHandlerInterface + */ + private $saveHandler; + + /** + * @inheritdoc + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + DateTimeFactory $dateTimeFactory, + VisitorCollectionFactory $visitorCollectionFactory, + SessionManagerInterface $sessionManager, + SaveHandlerInterface $saveHandler + ) { + $this->scopeConfig = $scopeConfig; + $this->dateTimeFactory = $dateTimeFactory; + $this->visitorCollectionFactory = $visitorCollectionFactory; + $this->sessionManager = $sessionManager; + $this->saveHandler = $saveHandler; + } + + /** + * @inheritdoc + */ + public function clearFor(int $customerId): void + { + if ($this->sessionManager->isSessionExists()) { + //delete old session and move data to the new session + //use this instead of $this->sessionManager->regenerateId because last one doesn't delete old session + // phpcs:ignore Magento2.Functions.DiscouragedFunction + session_regenerate_id(true); + } + + $sessionLifetime = $this->scopeConfig->getValue( + Config::XML_PATH_COOKIE_LIFETIME, + ScopeInterface::SCOPE_STORE + ); + $dateTime = $this->dateTimeFactory->create(); + $activeSessionsTime = $dateTime->setTimestamp($dateTime->getTimestamp() - $sessionLifetime) + ->format(DateTime::DATETIME_PHP_FORMAT); + /** @var \Magento\Customer\Model\ResourceModel\Visitor\Collection $visitorCollection */ + $visitorCollection = $this->visitorCollectionFactory->create(); + $visitorCollection->addFieldToFilter('customer_id', $customerId); + $visitorCollection->addFieldToFilter('last_visit_at', ['from' => $activeSessionsTime]); + /** @var \Magento\Customer\Model\Visitor $visitor */ + foreach ($visitorCollection->getItems() as $visitor) { + $sessionId = $visitor->getSessionId(); + $this->sessionManager->start(); + $this->saveHandler->destroy($sessionId); + $this->sessionManager->writeClose(); + } + } +} diff --git a/app/code/Magento/Customer/Model/Visitor.php b/app/code/Magento/Customer/Model/Visitor.php index 53745aa7a30c6..99dec57b89d15 100644 --- a/app/code/Magento/Customer/Model/Visitor.php +++ b/app/code/Magento/Customer/Model/Visitor.php @@ -25,7 +25,7 @@ use Magento\Store\Model\ScopeInterface; /** - * Class Visitor + * Class Visitor responsible for initializing visitor's. * * Used to track sessions of the logged in customers * @@ -206,7 +206,9 @@ public function initByRequest($observer) public function saveByRequest($observer) { // prevent saving Visitor for safe methods, e.g. GET request - if ($this->skipRequestLogging || $this->requestSafety->isSafeMethod() || $this->isModuleIgnored($observer)) { + if (($this->skipRequestLogging || $this->requestSafety->isSafeMethod() || $this->isModuleIgnored($observer)) + && !$this->sessionIdHasChanged() + ) { return $this; } @@ -223,6 +225,23 @@ public function saveByRequest($observer) return $this; } + /** + * Check if visitor session id was changed. + * + * @return bool + */ + private function sessionIdHasChanged(): bool + { + $visitorData = $this->session->getVisitorData(); + $hasChanged = false; + + if (isset($visitorData['session_id'])) { + $hasChanged = $this->session->getSessionId() !== $visitorData['session_id']; + } + + return $hasChanged; + } + /** * Returns true if the module is required * 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/Observer/UpgradeOrderCustomerEmailObserver.php b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php new file mode 100644 index 0000000000000..c2b7189b808a3 --- /dev/null +++ b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Observer; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\Customer\Model\Data\Customer; + +/** + * Class observer UpgradeOrderCustomerEmailObserver + * Update orders customer email after corresponding customer email changed + */ +class UpgradeOrderCustomerEmailObserver implements ObserverInterface +{ + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param OrderRepositoryInterface $orderRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + */ + public function __construct( + OrderRepositoryInterface $orderRepository, + SearchCriteriaBuilder $searchCriteriaBuilder + ) { + $this->orderRepository = $orderRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Upgrade order customer email when customer has changed email + * + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer): void + { + /** @var Customer $originalCustomer */ + $originalCustomer = $observer->getEvent()->getOrigCustomerDataObject(); + if (!$originalCustomer) { + return; + } + + /** @var Customer $customer */ + $customer = $observer->getEvent()->getCustomerDataObject(); + $customerEmail = $customer->getEmail(); + + if ($customerEmail === $originalCustomer->getEmail()) { + return; + } + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId()) + ->create(); + + /** + * @var Collection $orders + */ + $orders = $this->orderRepository->getList($searchCriteria); + $orders->setDataToAll(OrderInterface::CUSTOMER_EMAIL, $customerEmail); + $orders->save(); + } +} diff --git a/app/code/Magento/Customer/Setup/Patch/Data/AddCustomerUpdatedAtAttribute.php b/app/code/Magento/Customer/Setup/Patch/Data/AddCustomerUpdatedAtAttribute.php index bad5735bc3e3a..1a8cdb8987db7 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/AddCustomerUpdatedAtAttribute.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/AddCustomerUpdatedAtAttribute.php @@ -3,19 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Model\Customer; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class AddCustomerUpdatedAtAttribute - * @package Magento\Customer\Setup\Patch + * Class add customer updated attribute to customer */ class AddCustomerUpdatedAtAttribute implements DataPatchInterface, PatchVersionInterface { @@ -30,7 +29,6 @@ class AddCustomerUpdatedAtAttribute implements DataPatchInterface, PatchVersionI private $customerSetupFactory; /** - * AddCustomerUpdatedAtAttribute constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -43,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -61,10 +59,12 @@ public function apply() 'system' => false, ] ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -74,7 +74,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -82,7 +82,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/AddNonSpecifiedGenderAttributeOption.php b/app/code/Magento/Customer/Setup/Patch/Data/AddNonSpecifiedGenderAttributeOption.php index ba50f6e17dd87..36611afc6a2aa 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/AddNonSpecifiedGenderAttributeOption.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/AddNonSpecifiedGenderAttributeOption.php @@ -3,30 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Model\Customer; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Directory\Model\AllowedCountries; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Encryption\Encryptor; -use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\Setup\SetupInterface; -use Magento\Framework\Setup\UpgradeDataInterface; -use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Store\Model\ScopeInterface; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\DB\FieldDataConverterFactory; -use Magento\Framework\DB\DataConverter\SerializedToJson; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class AddNonSpecifiedGenderAttributeOption - * @package Magento\Customer\Setup\Patch + * Class add non specified gender attribute option to customer */ class AddNonSpecifiedGenderAttributeOption implements DataPatchInterface, PatchVersionInterface { @@ -41,7 +29,6 @@ class AddNonSpecifiedGenderAttributeOption implements DataPatchInterface, PatchV private $customerSetupFactory; /** - * AddNonSpecifiedGenderAttributeOption constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -54,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -64,10 +51,12 @@ public function apply() $option = ['attribute_id' => $attributeId, 'values' => [3 => 'Not Specified']]; $customerSetup->addAttributeOption($option); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -77,7 +66,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -85,7 +74,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php b/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php index b066d14a3c63e..09611ac1ccca3 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php @@ -3,19 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Model\Customer; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class AddSecurityTrackingAttributes - * @package Magento\Customer\Setup\Patch + * Class add security tracking attributes to customer */ class AddSecurityTrackingAttributes implements DataPatchInterface, PatchVersionInterface { @@ -30,7 +29,6 @@ class AddSecurityTrackingAttributes implements DataPatchInterface, PatchVersionI private $customerSetupFactory; /** - * AddSecurityTrackingAttributes constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -43,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -94,12 +92,14 @@ public function apply() $this->moduleDataSetup->getConnection()->update( $configTable, ['value' => new \Zend_Db_Expr('value*24')], - ['path = ?' => \Magento\Customer\Model\Customer::XML_PATH_CUSTOMER_RESET_PASSWORD_LINK_EXPIRATION_PERIOD] + ['path = ?' => Customer::XML_PATH_CUSTOMER_RESET_PASSWORD_LINK_EXPIRATION_PERIOD] ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -109,7 +109,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -117,7 +117,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/ConvertValidationRulesFromSerializedToJson.php b/app/code/Magento/Customer/Setup/Patch/Data/ConvertValidationRulesFromSerializedToJson.php index 83c5fe7ae6d1e..e25fe5803e46c 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/ConvertValidationRulesFromSerializedToJson.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/ConvertValidationRulesFromSerializedToJson.php @@ -3,19 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Framework\DB\FieldDataConverterFactory; use Magento\Framework\DB\DataConverter\SerializedToJson; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class ConvertValidationRulesFromSerializedToJson - * @package Magento\Customer\Setup\Patch + * Class convert validation rules from serialized to json for customer */ class ConvertValidationRulesFromSerializedToJson implements DataPatchInterface, PatchVersionInterface { @@ -30,7 +29,6 @@ class ConvertValidationRulesFromSerializedToJson implements DataPatchInterface, private $fieldDataConverterFactory; /** - * ConvertValidationRulesFromSerializedToJson constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param FieldDataConverterFactory $fieldDataConverterFactory */ @@ -43,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -54,10 +52,12 @@ public function apply() 'attribute_id', 'validate_rules' ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -67,7 +67,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -75,7 +75,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/DefaultCustomerGroupsAndAttributes.php b/app/code/Magento/Customer/Setup/Patch/Data/DefaultCustomerGroupsAndAttributes.php index 6e61b66f3c9db..fccda2ee9dac1 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/DefaultCustomerGroupsAndAttributes.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/DefaultCustomerGroupsAndAttributes.php @@ -4,19 +4,20 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Setup\CustomerSetup; use Magento\Customer\Setup\CustomerSetupFactory; +use Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend; use Magento\Framework\Module\Setup\Migration; use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class DefaultCustomerGroupsAndAttributes - * @package Magento\Customer\Setup\Patch + * Class default groups and attributes for customer */ class DefaultCustomerGroupsAndAttributes implements DataPatchInterface, PatchVersionInterface { @@ -31,20 +32,20 @@ class DefaultCustomerGroupsAndAttributes implements DataPatchInterface, PatchVer private $moduleDataSetup; /** - * DefaultCustomerGroupsAndAttributes constructor. * @param CustomerSetupFactory $customerSetupFactory * @param ModuleDataSetupInterface $moduleDataSetup */ public function __construct( CustomerSetupFactory $customerSetupFactory, - \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + ModuleDataSetupInterface $moduleDataSetup ) { $this->customerSetupFactory = $customerSetupFactory; $this->moduleDataSetup = $moduleDataSetup; } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function apply() @@ -133,7 +134,7 @@ public function apply() 'customer_address', 'street', 'backend_model', - \Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend::class + DefaultBackend::class ); $migrationSetup = $this->moduleDataSetup->createMigrationSetup(); @@ -146,10 +147,12 @@ public function apply() ['attribute_id'] ); $migrationSetup->doUpdateClassAliases(); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -157,7 +160,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -165,7 +168,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php b/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php index e4978070f53ad..1f21c7d4e83ba 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; +use Exception; use Magento\Directory\Model\AllowedCountries; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Store\Model\ScopeInterface; @@ -34,15 +36,14 @@ class MigrateStoresAllowedCountriesToWebsite implements DataPatchInterface, Patc private $allowedCountries; /** - * MigrateStoresAllowedCountriesToWebsite constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param StoreManagerInterface $storeManager * @param AllowedCountries $allowedCountries */ public function __construct( ModuleDataSetupInterface $moduleDataSetup, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Directory\Model\AllowedCountries $allowedCountries + StoreManagerInterface $storeManager, + AllowedCountries $allowedCountries ) { $this->moduleDataSetup = $moduleDataSetup; $this->storeManager = $storeManager; @@ -51,6 +52,8 @@ public function __construct( /** * @inheritdoc + * + * @throws Exception */ public function apply() { @@ -60,10 +63,12 @@ public function apply() try { $this->migrateStoresAllowedCountriesToWebsite(); $this->moduleDataSetup->getConnection()->commit(); - } catch (\Exception $e) { + } catch (Exception $e) { $this->moduleDataSetup->getConnection()->rollBack(); throw $e; } + + return $this; } /** diff --git a/app/code/Magento/Customer/Setup/Patch/Data/RemoveCheckoutRegisterAndUpdateAttributes.php b/app/code/Magento/Customer/Setup/Patch/Data/RemoveCheckoutRegisterAndUpdateAttributes.php index 51f54dc4a432c..5dfcf2bf9bf0d 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/RemoveCheckoutRegisterAndUpdateAttributes.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/RemoveCheckoutRegisterAndUpdateAttributes.php @@ -3,30 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Address; +use Magento\Customer\Model\ResourceModel\Address\Attribute\Backend\Region; +use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\Country; +use Magento\Customer\Model\ResourceModel\Attribute\Collection; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Directory\Model\AllowedCountries; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Encryption\Encryptor; -use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\Setup\SetupInterface; -use Magento\Framework\Setup\UpgradeDataInterface; -use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Eav\Model\Entity\Increment\NumericValue; use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Store\Model\ScopeInterface; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\DB\FieldDataConverterFactory; -use Magento\Framework\DB\DataConverter\SerializedToJson; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class RemoveCheckoutRegisterAndUpdateAttributes - * @package Magento\Customer\Setup\Patch + * Remove register and update attributes for checkout */ class RemoveCheckoutRegisterAndUpdateAttributes implements DataPatchInterface, PatchVersionInterface { @@ -41,7 +34,6 @@ class RemoveCheckoutRegisterAndUpdateAttributes implements DataPatchInterface, P private $customerSetupFactory; /** - * RemoveCheckoutRegisterAndUpdateAttributes constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -54,7 +46,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -64,52 +56,54 @@ public function apply() ); $customerSetup = $this->customerSetupFactory->create(['setup' => $this->moduleDataSetup]); $customerSetup->updateEntityType( - \Magento\Customer\Model\Customer::ENTITY, + Customer::ENTITY, 'entity_model', \Magento\Customer\Model\ResourceModel\Customer::class ); $customerSetup->updateEntityType( - \Magento\Customer\Model\Customer::ENTITY, + Customer::ENTITY, 'increment_model', - \Magento\Eav\Model\Entity\Increment\NumericValue::class + NumericValue::class ); $customerSetup->updateEntityType( - \Magento\Customer\Model\Customer::ENTITY, + Customer::ENTITY, 'entity_attribute_collection', - \Magento\Customer\Model\ResourceModel\Attribute\Collection::class + Collection::class ); $customerSetup->updateEntityType( 'customer_address', 'entity_model', - \Magento\Customer\Model\ResourceModel\Address::class + Address::class ); $customerSetup->updateEntityType( 'customer_address', 'entity_attribute_collection', - \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection::class + Address\Attribute\Collection::class ); $customerSetup->updateAttribute( 'customer_address', 'country_id', 'source_model', - \Magento\Customer\Model\ResourceModel\Address\Attribute\Source\Country::class + Country::class ); $customerSetup->updateAttribute( 'customer_address', 'region', 'backend_model', - \Magento\Customer\Model\ResourceModel\Address\Attribute\Backend\Region::class + Region::class ); $customerSetup->updateAttribute( 'customer_address', 'region_id', 'source_model', - \Magento\Customer\Model\ResourceModel\Address\Attribute\Source\Region::class + Address\Attribute\Source\Region::class ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -119,7 +113,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -127,7 +121,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpdateAutocompleteOnStorefrontConfigPath.php b/app/code/Magento/Customer/Setup/Patch/Data/UpdateAutocompleteOnStorefrontConfigPath.php index 30435ace54d46..8b8092cbb22c6 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/UpdateAutocompleteOnStorefrontConfigPath.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpdateAutocompleteOnStorefrontConfigPath.php @@ -3,17 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; -use Magento\Framework\App\ResourceConnection; +use Magento\Customer\Model\Form; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class UpdateAutocompleteOnStorefrontCOnfigPath - * @package Magento\Customer\Setup\Patch + * Update storefront's autocomplete of config path */ class UpdateAutocompleteOnStorefrontConfigPath implements DataPatchInterface, PatchVersionInterface { @@ -23,7 +23,6 @@ class UpdateAutocompleteOnStorefrontConfigPath implements DataPatchInterface, Pa private $moduleDataSetup; /** - * UpdateAutocompleteOnStorefrontCOnfigPath constructor. * @param ModuleDataSetupInterface $moduleDataSetup */ public function __construct( @@ -33,19 +32,21 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { $this->moduleDataSetup->getConnection()->update( $this->moduleDataSetup->getTable('core_config_data'), - ['path' => \Magento\Customer\Model\Form::XML_PATH_ENABLE_AUTOCOMPLETE], + ['path' => Form::XML_PATH_ENABLE_AUTOCOMPLETE], ['path = ?' => 'general/restriction/autocomplete_on_storefront'] ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -55,7 +56,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -63,7 +64,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpdateCustomerAttributeInputFilters.php b/app/code/Magento/Customer/Setup/Patch/Data/UpdateCustomerAttributeInputFilters.php index 938cd3cd52e73..ff6decb1d2123 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/UpdateCustomerAttributeInputFilters.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpdateCustomerAttributeInputFilters.php @@ -3,18 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class UpdateCustomerAttributeInputFilters - * @package Magento\Customer\Setup\Patch + * Update attribute input filters for customer */ class UpdateCustomerAttributeInputFilters implements DataPatchInterface, PatchVersionInterface { @@ -29,7 +28,6 @@ class UpdateCustomerAttributeInputFilters implements DataPatchInterface, PatchVe private $customerSetupFactory; /** - * UpdateCustomerAttributeInputFilters constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -42,7 +40,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -72,10 +70,12 @@ public function apply() ], ]; $customerSetup->upgradeAttributes($entityAttributes); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -85,7 +85,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -93,7 +93,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpdateDefaultCustomerGroupInConfig.php b/app/code/Magento/Customer/Setup/Patch/Data/UpdateDefaultCustomerGroupInConfig.php new file mode 100644 index 0000000000000..c8159adc2ccff --- /dev/null +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpdateDefaultCustomerGroupInConfig.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Setup\Patch\Data; + +use Magento\Customer\Model\GroupManagement; +use Magento\Customer\Model\Vat; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Update default customer group id in customer configuration if it's value is NULL + */ +class UpdateDefaultCustomerGroupInConfig implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var GroupManagement + */ + private $groupManagement; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param GroupManagement $groupManagement + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + GroupManagement $groupManagement + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->groupManagement = $groupManagement; + } + + /** + * @inheritDoc + */ + public function apply() + { + $customerGroups = $this->groupManagement->getLoggedInGroups(); + $commonGroup = array_shift($customerGroups); + + $this->moduleDataSetup->getConnection()->update( + $this->moduleDataSetup->getTable('core_config_data'), + ['value' => $commonGroup->getId()], + [ + 'value is ?' => new \Zend_Db_Expr('NULL'), + 'path = ?' => GroupManagement::XML_PATH_DEFAULT_ID, + ] + ); + + return $this; + } + + /** + * @inheritDoc + */ + public function getAliases() + { + return []; + } + + /** + * @inheritDoc + */ + public static function getDependencies() + { + return [ + DefaultCustomerGroupsAndAttributes::class, + ]; + } +} diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpdateIdentifierCustomerAttributesVisibility.php b/app/code/Magento/Customer/Setup/Patch/Data/UpdateIdentifierCustomerAttributesVisibility.php index 7d0cad768d6b0..8519fab81efc5 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/UpdateIdentifierCustomerAttributesVisibility.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpdateIdentifierCustomerAttributesVisibility.php @@ -3,18 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class UpdateIdentifierCustomerAttributesVisibility - * @package Magento\Customer\Setup\Patch + * Update identifier attributes visibility for customer */ class UpdateIdentifierCustomerAttributesVisibility implements DataPatchInterface, PatchVersionInterface { @@ -29,7 +28,6 @@ class UpdateIdentifierCustomerAttributesVisibility implements DataPatchInterface private $customerSetupFactory; /** - * UpdateIdentifierCustomerAttributesVisibility constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -42,7 +40,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -70,10 +68,12 @@ public function apply() ], ]; $customerSetup->upgradeAttributes($entityAttributes); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -83,7 +83,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -91,7 +91,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpdateVATNumber.php b/app/code/Magento/Customer/Setup/Patch/Data/UpdateVATNumber.php index d31301eedf4b1..ea3207c7ccb85 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/UpdateVATNumber.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpdateVATNumber.php @@ -3,27 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; -use Magento\Customer\Model\Customer; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Directory\Model\AllowedCountries; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Encryption\Encryptor; -use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\Setup\SetupInterface; -use Magento\Framework\Setup\UpgradeDataInterface; -use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Store\Model\ScopeInterface; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\DB\FieldDataConverterFactory; -use Magento\Framework\DB\DataConverter\SerializedToJson; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; +/** + * Upgrade vat number + */ class UpdateVATNumber implements DataPatchInterface, PatchVersionInterface { /** @@ -37,7 +28,6 @@ class UpdateVATNumber implements DataPatchInterface, PatchVersionInterface private $customerSetupFactory; /** - * UpdateVATNumber constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -50,16 +40,23 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { $customerSetup = $this->customerSetupFactory->create(['resourceConnection' => $this->moduleDataSetup]); - $customerSetup->updateAttribute('customer_address', 'vat_id', 'frontend_label', 'VAT Number'); + $customerSetup->updateAttribute( + 'customer_address', + 'vat_id', + 'frontend_label', + 'VAT Number' + ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -69,7 +66,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -77,7 +74,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpgradePasswordHashAndAddress.php b/app/code/Magento/Customer/Setup/Patch/Data/UpgradePasswordHashAndAddress.php index 3b8f96a037343..5d6e490bead22 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/UpgradePasswordHashAndAddress.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpgradePasswordHashAndAddress.php @@ -3,19 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Setup\CustomerSetupFactory; use Magento\Framework\Encryption\Encryptor; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class UpgradePasswordHashAndAddress - * @package Magento\Customer\Setup\Patch + * Update passwordHash and address */ class UpgradePasswordHashAndAddress implements DataPatchInterface, PatchVersionInterface { @@ -30,7 +29,6 @@ class UpgradePasswordHashAndAddress implements DataPatchInterface, PatchVersionI private $customerSetupFactory; /** - * UpgradePasswordHashAndAddress constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -43,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -58,9 +56,13 @@ public function apply() ]; $customerSetup = $this->customerSetupFactory->create(['setup' => $this->moduleDataSetup]); $customerSetup->upgradeAttributes($entityAttributes); + + return $this; } /** + * Password hash upgrade + * * @return void */ private function upgradeHash() @@ -93,7 +95,7 @@ private function upgradeHash() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -103,7 +105,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -111,7 +113,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersNowOnlineGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersNowOnlineGridActionGroup.xml new file mode 100644 index 0000000000000..05ccc6e67c697 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersNowOnlineGridActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Assert company in customers now online grid row --> + <actionGroup name="AdminAssertCustomerInCustomersNowOnlineGridActionGroup"> + <annotations> + <description>Validates that the provided Customer is present and correct in the Customer now online grid page.</description> + </annotations> + <arguments> + <argument name="text" type="string"/> + <argument name="columnName" type="string"/> + </arguments> + <waitForText selector="{{AdminCustomersNowOnlineGridSection.gridCell('1', columnName)}}" userInput="{{text}}" stepKey="waitForCustomesrNowOnline"/> + </actionGroup> +</actionGroups> 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/AdminCustomerDeleteWishlistItemActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml new file mode 100644 index 0000000000000..b827cba8490b8 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.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="AdminCustomerDeleteWishlistItemActionGroup"> + <click selector="{{AdminCustomerWishlistSection.deleteButton}}" stepKey="clickDeleteButton"/> + <waitForPageLoad stepKey="waitForResultsLoading"/> + <click selector="{{AdminCustomerWishlistSection.deleteConfirm}}" stepKey="confirmDeleting"/> + <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 new file mode 100644 index 0000000000000..cd581ed1836dd --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.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="AdminCustomerFindWishlistItemActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <fillField userInput="{{productName}}" selector="{{AdminCustomerWishlistSection.productName}}" stepKey="fillProductNameField"/> + <click selector="{{AdminCustomerWishlistSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForAjaxLoad time="60" stepKey="waitForLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml new file mode 100644 index 0000000000000..66b464006aa0f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.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="AdminNavigateCustomerWishlistTabActionGroup"> + <click selector="{{AdminCustomerInformationSection.wishList}}" stepKey="clickWishlistButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateToCustomerOnlinePageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateToCustomerOnlinePageActionGroup.xml new file mode 100644 index 0000000000000..dd048c2636cd4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateToCustomerOnlinePageActionGroup.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="AdminNavigateToCustomerOnlinePageActionGroup"> + <annotations> + <description>Goes to the Admin Customer Online page.</description> + </annotations> + + <amOnPage url="{{AdminCustomersNowOnlinePage.url}}" stepKey="openCustomersOnlineGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backup/Test/Mftf/ActionGroup/deleteBackupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml similarity index 60% rename from app/code/Magento/Backup/Test/Mftf/ActionGroup/deleteBackupActionGroup.xml rename to app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml index b879a2aa9647a..16688be61171e 100644 --- a/app/code/Magento/Backup/Test/Mftf/ActionGroup/deleteBackupActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml @@ -8,5 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="deleteBackup" extends="AdminBackupDeleteActionGroup" deprecated="Use DeleteBackupActionGroup"/> + <actionGroup name="AssertAdminCustomerNoItemsInWishlistActionGroup"> + <see userInput="No Items Found" selector="{{AdminCustomerWishlistSection.gridTable}}" stepKey="assertNoItems"/> + </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/ActionGroup/CreateCustomerOrderActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml new file mode 100644 index 0000000000000..f287c728bb28d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateCustomerOrderActionGroup"> + <annotations> + <description>Create Order via API assigned to Customer.</description> + </annotations> + + <createData entity="CustomerCart" stepKey="CustomerCart"> + <requiredEntity createDataKey="Customer"/> + </createData> + + <createData entity="CustomerCartItem" stepKey="addCartItem"> + <requiredEntity createDataKey="CustomerCart"/> + <requiredEntity createDataKey="Product"/> + </createData> + + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="CustomerCart"/> + </createData> + + <updateData createDataKey="CustomerCart" entity="CustomerOrderPaymentMethod" stepKey="sendCustomerPaymentInformation"> + <requiredEntity createDataKey="CustomerCart"/> + </updateData> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml new file mode 100644 index 0000000000000..5dafe59bf3c48 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.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="StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup"> + <arguments> + <argument name="itemName" type="string"/> + </arguments> + <dontSee userInput="{{itemName}}" selector="{{StorefrontCustomerSidebarSection.sidebarTab(itemName)}}" stepKey="dontSeeElement"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertOnCustomerLoginPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertOnCustomerLoginPageActionGroup.xml new file mode 100644 index 0000000000000..830d021380b16 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertOnCustomerLoginPageActionGroup.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="StorefrontAssertOnCustomerLoginPageActionGroup"> + <annotations> + <description>Assert on the Storefront Customer Sign-In page.</description> + </annotations> + + <seeInCurrentUrl url="{{StorefrontCustomerSignInPage.url}}" stepKey="seeOnSignInPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontGoToCustomerOrderDetailsPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontGoToCustomerOrderDetailsPageActionGroup.xml new file mode 100644 index 0000000000000..47d6fe1ed204e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontGoToCustomerOrderDetailsPageActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontGoToCustomerOrderDetailsPageActionGroup"> + <annotations> + <description>Navigate to storefront order details page</description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + <argument name="orderNumber" type="string"/> + </arguments> + <amOnPage url="{{StorefrontCustomerOrderViewPage.url(orderId)}}" stepKey="goToOrdersPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForText selector="{{StorefrontCustomerAccountMainSection.pageTitle}}" userInput="{{orderNumber}}" stepKey="verifyOrderNo"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontVerifyCustomerOrderDataActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontVerifyCustomerOrderDataActionGroup.xml new file mode 100644 index 0000000000000..81ce5dbe69980 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontVerifyCustomerOrderDataActionGroup.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="StorefrontVerifyCustomerOrderDataActionGroup"> + <annotations> + <description>Verify a customer's order details on the view order page on the storefront</description> + </annotations> + <arguments> + <argument name="createdDate" type="string"/> + <argument name="productName" type="string"/> + <argument name="grandTotal" type="string"/> + <argument name="orderPlacedBy" type="string"/> + <argument name="paymentMethod" type="string"/> + </arguments> + <waitForText selector="{{StorefrontCustomerOrderViewSection.paymentMethod}}" userInput="{{paymentMethod}}" stepKey="storefrontVerifyPaymentMethod"/> + <waitForText selector="{{StorefrontCustomerOrderViewSection.createdDate}}" userInput="{{createdDate}}" stepKey="storefrontVerifyOrderCreatedDate"/> + <waitForText selector="{{StorefrontCustomerOrderViewSection.orderPlacedBy}}" userInput="{{orderPlacedBy}}" stepKey="storefrontVerifyOrderPlacedBy"/> + <waitForText selector="{{StorefrontCustomerOrderViewSection.productName}}" userInput="{{productName}}" stepKey="storefrontVerifyProductName"/> + <waitForText selector="{{StorefrontCustomerOrderViewSection.grandTotal}}" userInput="{{grandTotal}}" stepKey="storefrontVerifyGrandTotal"/> + + </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/Data/ExtensionAttributeSimple.xml b/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimpleData.xml similarity index 100% rename from app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.xml rename to app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimpleData.xml diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/CustomerExtensionAttributeMeta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/CustomerExtensionAttributeMeta.xml index 06c7b74aef002..d9f738e9d8235 100644 --- a/app/code/Magento/Customer/Test/Mftf/Metadata/CustomerExtensionAttributeMeta.xml +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/CustomerExtensionAttributeMeta.xml @@ -9,10 +9,12 @@ <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> <operation name="CreateCustomerExtensionAttribute" dataType="customer_extension_attribute" type="create"> + <field key="assistance_allowed">integer</field> <field key="is_subscribed">boolean</field> <field key="extension_attribute">customer_nested_extension_attribute</field> </operation> <operation name="UpdateCustomerExtensionAttribute" dataType="customer_extension_attribute" type="update"> + <field key="assistance_allowed">integer</field> <field key="is_subscribed">boolean</field> <field key="extension_attribute">customer_nested_extension_attribute</field> </operation> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomersNowOnlinePage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomersNowOnlinePage.xml new file mode 100644 index 0000000000000..158ffabd7dd24 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomersNowOnlinePage.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="AdminCustomersNowOnlinePage" url="/customer/online/" area="admin" module="Magento_Customer"> + <section name="AdminCustomersNowOnlineGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml index d644b581088bc..8277cdd64928a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml @@ -11,6 +11,7 @@ <section name="AdminCustomerGridMainActionsSection"> <element name="addNewCustomer" type="button" selector="#add" timeout="30"/> <element name="multicheck" type="checkbox" selector="#container>div>div.admin__data-grid-wrap>table>thead>tr>th.data-grid-multicheck-cell>div>label"/> + <element name="multicheckTick" type="checkbox" selector="#container>div>div.admin__data-grid-wrap>table>thead>tr>th.data-grid-multicheck-cell>div>input"/> <element name="delete" type="button" selector="//*[contains(@class, 'admin__data-grid-header')]//span[contains(@class,'action-menu-item') and text()='Delete']"/> <element name="actions" type="text" selector=".action-select"/> <element name="customerCheckbox" type="button" selector="//*[contains(text(),'{{arg}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']//input" parameterized="true"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml new file mode 100644 index 0000000000000..39a67968c66e4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerWishlistSection"> + <element name="productName" type="input" selector="#wishlistGrid_filter_product_name"/> + <element name="searchButton" type="button" selector="#wishlistGrid button[data-action='grid-filter-apply']"/> + <element name="deleteButton" type="text" selector="//*[@id='wishlistGrid_table']//*[@data-column='action']//*[text()='Delete']"/> + <element name="deleteConfirm" type="button" selector=".modal-popup.confirm .action-primary.action-accept"/> + <element name="gridTable" type="text" selector="#wishlistGrid_table"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomersNowOnlineGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomersNowOnlineGridSection.xml new file mode 100644 index 0000000000000..a4b67c950fe7b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomersNowOnlineGridSection.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="AdminCustomersNowOnlineGridSection"> + <element name="columnTitle" type="text" selector=".admin__action-dropdown-menu-content label[title='{{column}}']" parameterized="true"/> + <element name="gridCell" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> + </section> +</sections> + diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml index ec5141d84b1bd..61ce050aa3ef2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml @@ -17,5 +17,9 @@ <element name="viewOrder" type="button" selector="//td[contains(concat(' ',normalize-space(@class),' '),' col actions ')]/a[contains(concat(' ',normalize-space(@class),' '),' action view ')]"/> <element name="tabRefund" type="button" selector="//a[text()='Refunds']"/> <element name="grandTotalRefund" type="text" selector="td[data-th='Grand Total'] > strong > span.price"/> + <element name="currentPage" type="text" selector=".order-products-toolbar .pages .current span:nth-of-type(2)"/> + <element name="pageNumber" type="text" selector="//*[@class='order-products-toolbar toolbar bottom']//a[contains(@class, 'page')]//span[2][contains(text() ,'{{var1}}')]" parameterized="true"/> + <element name="perPage" type="select" selector="//*[@class='order-products-toolbar toolbar bottom']//select[@id='limiter']"/> + <element name="rowsInColumn" type="text" selector="//tbody/tr/td[contains(@class, '{{column}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml index 09b79fe831188..42c6f5cea082f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml @@ -18,5 +18,9 @@ <element name="billingAddress" type="text" selector=".box.box-order-billing-address"/> <element name="orderStatusInGrid" type="text" selector="//td[contains(.,'{{orderId}}')]/../td[contains(.,'{{status}}')]" parameterized="true"/> <element name="pager" type="block" selector=".pager"/> + <element name="createdDate" type="text" selector=".block-order-details-comments .comment-date"/> + <element name="orderPlacedBy" type="text" selector=".block-order-details-comments .comment-content"/> + <element name="productName" type="text" selector="//td[@data-th='Product Name']"/> + <element name="grandTotal" type="text" selector="//tr[@class='grand_total']//td[@data-th='Grand Total']"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml index ab5e332aeed64..6aec5440193a2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml @@ -20,7 +20,9 @@ </annotations> <before> <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 0" stepKey="setConfigDefaultIsNo"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml index 0bf221d49ab74..d48fb90b24ec2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml @@ -20,12 +20,16 @@ </annotations> <before> <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 1" stepKey="setConfigDefaultIsYes"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> </before> <after> <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 0" stepKey="setConfigDefaultIsNo"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml index 3488d2c94dd69..9f6d8d645e5f4 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml @@ -29,8 +29,7 @@ </after> <!--Filter the customer From grid--> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad time="30" stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="navigateToNewCustomerPage"/> <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Retailer" stepKey="fillCustomerGroup"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> @@ -51,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/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index 64e8520323184..cb003ed837294 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -20,7 +20,9 @@ <group value="create"/> </annotations> <before> - <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexCustomerGrid"> + <argument name="indices" value="customer_grid"/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> @@ -35,7 +37,9 @@ <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <reloadPage stepKey="reloadPage"/> <waitForPageLoad stepKey="waitForLoad2"/> <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> 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 52a2483096aaf..7cffd5f304e31 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml @@ -31,15 +31,16 @@ </after> <!--Open New Customer Page --> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="navigateToNewCustomerPage"/> <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="$$customerGroup.code$$" stepKey="fillCustomerGroup"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> - <magentoCLI stepKey="flushMagentoCache" command="cache:flush" /> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <reloadPage stepKey="reloadPage"/> <!--Verify Customer in grid --> @@ -53,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 591cb2dd2845a..eaa3a11edb74e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml @@ -29,8 +29,7 @@ </after> <!--Open New Customer Page and create a customer with Prefix and Suffix--> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="navigateToNewCustomerPage"/> <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Wholesale" stepKey="fillCustomerGroup"/> <fillField selector="{{AdminCustomerAccountInformationSection.namePrefix}}" userInput="{{CustomerEntityOne.prefix}}" stepKey="fillNamePrefix"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> @@ -57,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 081695f7ebe1e..98826b147ad81 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml @@ -29,8 +29,7 @@ </after> <!--Open New Customer Page --> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="waitToCustomerPageLoad"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> @@ -50,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 da25139ee8e60..5edb9d08da46d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml @@ -29,8 +29,7 @@ </after> <!--Open New Customer Page --> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="waitToCustomerPageLoad"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> @@ -43,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/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml index 8494a94f0c122..615a6ebcf24cc 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml @@ -20,7 +20,9 @@ <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml index 340295df04da2..57446a1ee0c72 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml @@ -20,7 +20,9 @@ <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml index 1630743da4922..f08ea83a70da6 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml @@ -20,7 +20,9 @@ <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml new file mode 100644 index 0000000000000..64e9f6d10bdb3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.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="AdminGridSearchSelectAllTest"> + <annotations> + <stories value="Selection should be removed during search."/> + <title value="Selection should be removed during search."/> + <description value="Empty selected before and after search, like it works for filter"/> + <testCaseId value="MC-37659"/> + <severity value="CRITICAL"/> + <group value="uI"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create three customers--> + <createData entity="Simple_US_Customer" stepKey="firstCustomer"/> + <createData entity="Simple_US_Customer" stepKey="secondCustomer"/> + <createData entity="Simple_US_Customer" stepKey="thirdCustomer"/> + </before> + <after> + <!--Remove two created customers, third already deleted--> + <deleteData createDataKey="firstCustomer" stepKey="deleteFirstCustomer"/> + <deleteData createDataKey="secondCustomer" stepKey="deleteSecondCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomerPage"/> + <!-- search Admin Data Grid By Keyword --> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.search}}" userInput="$$secondCustomer.email$$" stepKey="fillKeywordSearchFieldWithSecondCustomerEmail"/> + <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickKeywordSearch"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!-- Select all from dropdown --> + <actionGroup ref="AdminGridSelectAllActionGroup" stepKey="selectAllCustomers"/> + <!-- Clear searching By Keyword--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFiltersAfterSearch"/> + <waitForPageLoad stepKey="waitForPageLoadAfterSearchRemoved"/> + <!-- Check if selection has bee removed --> + <dontSeeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.customerCheckbox($$secondCustomer.email$$)}}" stepKey="checkSecondCustomerCheckboxIsUnchecked"/> + <!-- Check delete action --> + <click selector="{{AdminCustomerGridMainActionsSection.customerCheckbox(($$thirdCustomer.email$$)}}" stepKey="selectThirdCustomer"/> + <seeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.customerCheckbox($$thirdCustomer.email$$)}}" stepKey="checkThirdCustomerIsChecked"/> + <!-- Use delete action for selected --> + <click selector="{{AdminCustomerGridMainActionsSection.actions}}" stepKey="clickActions"/> + <click selector="{{AdminCustomerGridMainActionsSection.delete}}" stepKey="clickDelete"/> + <waitForAjaxLoad stepKey="waitForLoadConfirmation"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> + <!-- Check if only one record record has been deleted --> + <see selector="{{AdminMessagesSection.success}}" userInput="A total of 1 record(s) were deleted" stepKey="seeSuccess"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml new file mode 100644 index 0000000000000..bfc49fd476dd0 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminGridSelectAllOnPageTest"> + <annotations> + <stories value="Toggle select page."/> + <title value="Toggle select page."/> + <description value="Empty selected before and after search, like it works for filter"/> + <testCaseId value="MC-37660"/> + <severity value="CRITICAL"/> + <group value="uI"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create three customers--> + <createData entity="Simple_US_Customer" stepKey="firstCustomer"/> + <createData entity="Simple_US_Customer" stepKey="secondCustomer"/> + <createData entity="Simple_US_Customer" stepKey="thirdCustomer"/> + </before> + <after> + <!--Remove created customers --> + <deleteData createDataKey="firstCustomer" stepKey="deleteFirstCustomer"/> + <deleteData createDataKey="secondCustomer" stepKey="deleteSecondCustomer"/> + <deleteData createDataKey="thirdCustomer" stepKey="deleteThirdCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomerPage"/> + <!-- Select all from dropdown --> + <actionGroup ref="AdminGridSelectAllActionGroup" stepKey="selectAllCustomers"/> + <!-- Deselect third customer --> + <click selector="{{AdminCustomerGridMainActionsSection.customerCheckbox(($$thirdCustomer.email$$)}}" stepKey="selectThirdCustomer"/> + <dontSeeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.customerCheckbox($$thirdCustomer.email$$)}}" stepKey="checkThirdCustomerCheckboxIsUnchecked"/> + <!-- Click select all on page checkbox --> + <actionGroup ref="AdminSelectAllCustomers" stepKey="selectAllCustomersOnPage"/> + <seeElement selector="{{AdminCustomerGridMainActionsSection.multicheckTick}}" stepKey="waitForElement"/> + <seeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.multicheckTick}}" stepKey="checkAllSelectedCheckBoxIsChecked"/> + <!-- Check all created records selected --> + <seeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.customerCheckbox($$firstCustomer.email$$)}}" stepKey="checkFirstCustomerIsCheckedAfterSelectPage"/> + <seeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.customerCheckbox($$secondCustomer.email$$)}}" stepKey="checkSecondCustomerIsCheckedAfterSelectPage"/> + <seeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.customerCheckbox($$thirdCustomer.email$$)}}" stepKey="checkThirdCustomerIsCheckedAfterSelectPage"/> + <!-- Click deselect all on page checkbox --> + <actionGroup ref="AdminSelectAllCustomers" stepKey="deselectAllCustomersCheckbox"/> + <dontSeeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.multicheckTick}}" stepKey="checkAllSelectedCheckBoxUnchecked"/> + <!-- Check all created records unselected --> + <dontSeeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.customerCheckbox($$firstCustomer.email$$)}}" stepKey="checkFirstCustomerIsUncheckedAfterSelectPage"/> + <dontSeeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.customerCheckbox($$secondCustomer.email$$)}}" stepKey="checkSecondCustomerIsUncheckedAfterSelectPage"/> + <dontSeeCheckboxIsChecked selector="{{AdminCustomerGridMainActionsSection.customerCheckbox($$thirdCustomer.email$$)}}" stepKey="checkThirdCustomerIsUncheckedAfterSelectPage"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml index 5721c46d5e4b9..257d4c9b2e3c2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml @@ -25,8 +25,12 @@ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Edit customer info--> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="OpenEditCustomerFrom"> <argument name="customer" value="$$customer$$"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml index 10da9284d45dc..b13a06b9ef858 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml @@ -20,7 +20,9 @@ <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml index c9805ebcc90ed..3f95e55c56132 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml @@ -20,7 +20,9 @@ </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer_Multiple_Addresses"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml index 0d550416167aa..8af07bc2c2d53 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml @@ -20,7 +20,9 @@ </annotations> <before> <createData stepKey="customer" entity="Simple_Customer_Without_Address"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml index 72064617ef33b..78a2fe721453f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml @@ -26,8 +26,7 @@ </after> <!--Open New Customer Page --> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="navigateToNewCustomerPage"/> <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> <waitForPageLoad stepKey="waitForPageToLoad"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml index 60caaf64f05b7..781d721fd5132 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml @@ -26,7 +26,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <!--Create new website,store and store view--> <comment userInput="Create new website,store and store view" stepKey="createWebsite"/> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="goToAdminSystemStorePage"/> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="goToAdminSystemStorePage"/> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="adminCreateNewWebsite"> <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> @@ -42,7 +42,9 @@ <!--Set account sharing option - Default value is 'Per Website'--> <comment userInput="Set account sharing option - Default value is 'Per Website'" stepKey="setAccountSharingOption"/> <createData entity="CustomerAccountSharingDefault" stepKey="setToAccountSharingToDefault"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--delete all created data and set main website country options to default--> @@ -60,8 +62,12 @@ <actionGroup ref="SetWebsiteCountryOptionsToDefaultActionGroup" stepKey="setCountryOptionsToDefault"/> <createData entity="CustomerAccountSharingSystemValue" stepKey="setAccountSharingToSystemValue"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Check that all countries are allowed initially and get amount--> <comment userInput="Check that all countries are allowed initially and get amount" stepKey="checkAllCountriesAreAllowed"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml index 80cdeadb391da..5a75d5d272295 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml @@ -83,9 +83,7 @@ <see userInput="$$createProduct.name$$" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertFirstProductNameTitle"/> <!--Add a product to the cart--> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddProductToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!--Proceed to checkout--> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> <!-- Click next button to open payment section --> @@ -103,8 +101,7 @@ <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="{{TaxRule.name}}"/> @@ -117,8 +114,7 @@ </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml index 317f2c2825ca7..130e1ba6723ae 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml @@ -102,8 +102,12 @@ <requiredEntity createDataKey="createDownloadableProduct1"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Login --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> @@ -182,8 +186,7 @@ <argument name="productVar" value="$$createDownloadableProduct1$$"/> </actionGroup> - <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="amOnMyAccountDashboard1"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="amOnMyAccountDashboard1"/> <actionGroup ref="StorefrontClearCompareActionGroup" stepKey="clearComparedProducts1"/> </test> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml index 8cd35f4147636..47b61b332f571 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml @@ -35,7 +35,9 @@ <argument name="Customer" value="CustomerEntityOne"/> <argument name="dob" value="{{EN_US_DATE.short4DigitYear}}"/> </actionGroup> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml new file mode 100644 index 0000000000000..ba113c739d706 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerAccountOrderListTest"> + <annotations> + <stories value="Frontend Customer Account Orders list"/> + <title value="Verify that the list of Orders is displayed in the grid after changing the number of items on the page"/> + <description value="Verify that the list of Orders is displayed in the grid after changing the number of items on the page."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-34953"/> + <group value="customer"/> + </annotations> + + <before> + + <!--Create Product via API--> + <createData entity="SimpleProduct2" stepKey="Product"/> + + <!--Create Customer via API--> + <createData entity="Simple_US_Customer" stepKey="Customer"/> + + <!--Create Orders via API--> + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder1"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder2"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder3"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder4"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder5"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder6"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder7"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder8"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder9"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder10"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder11"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder12"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder13"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder14"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder15"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + <!--Create Orders via API--> + + </before> + + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <deleteData createDataKey="Product" stepKey="deleteProduct"/> + <deleteData createDataKey="Customer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$Customer$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToSidebarMenu"> + <argument name="menu" value="My Orders"/> + </actionGroup> + + <seeElement selector="{{StorefrontCustomerOrderSection.isMyOrdersSection}}" stepKey="waitOrderHistoryPage"/> + + <scrollTo selector="{{StorefrontCustomerOrderSection.currentPage}}" stepKey="scrollToBottomToolbarSection"/> + + <click selector="{{StorefrontCustomerOrderSection.pageNumber('2')}}" stepKey="clickOnPage2"/> + + <scrollTo selector="{{StorefrontCustomerOrderSection.perPage}}" stepKey="scrollToLimiter"/> + + <selectOption userInput="20" selector="{{StorefrontCustomerOrderSection.perPage}}" stepKey="selectLimitOnPage"/> + + <waitForPageLoad stepKey="waitForLoadPage"/> + + <seeElement selector="{{StorefrontCustomerOrderSection.isMyOrdersSection}}" + stepKey="seeElementOrderHistoryPage"/> + + <dontSee selector="{{StorefrontOrderInformationMainSection.emptyMessage}}" + userInput="You have placed no orders." stepKey="dontSeeEmptyMessage"/> + + <seeNumberOfElements selector="{{StorefrontCustomerOrderSection.rowsInColumn('id')}}" userInput="15" + stepKey="seeRowsCount"/> + + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml index f504af2334e10..410070234b9c0 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml @@ -25,11 +25,15 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <executeJS function="return window.location.host" stepKey="hostname"/> <amOnUrl url="http://{$hostname}/customer" stepKey="goToUnsecureCustomerURL"/> diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index c63395ed501a9..a0a8718319bf5 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -12,6 +12,7 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\SessionCleanerInterface; use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; use Magento\Customer\Helper\View; use Magento\Customer\Model\AccountConfirmation; @@ -203,6 +204,11 @@ class AccountManagementTest extends TestCase */ private $allowedCountriesReader; + /** + * @var SessionCleanerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $sessionCleanerMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -1519,6 +1525,7 @@ private function reInitModel() ->setMethods(['create']) ->getMock(); $dateTimeFactory->expects($this->any())->method('create')->willReturn($dateTimeMock); + $this->sessionCleanerMock = $this->createMock(SessionCleanerInterface::class); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->accountManagement = $this->objectManagerHelper->getObject( @@ -1539,6 +1546,7 @@ private function reInitModel() 'storeManager' => $this->storeManager, 'addressRegistry' => $this->addressRegistryMock, 'transportBuilder' => $this->transportBuilder, + 'sessionCleaner' => $this->sessionCleanerMock, ] ); $this->objectManagerHelper->setBackwardCompatibleProperty( @@ -1617,35 +1625,13 @@ public function testChangePassword() ->with($newPassword) ->willReturn(7); + $this->sessionCleanerMock->expects($this->once())->method('clearFor')->with($customerId)->willReturnSelf(); + $this->customerRepository ->expects($this->once()) ->method('save') ->with($customer); - $this->sessionManager->expects($this->atLeastOnce())->method('getSessionId'); - $this->sessionManager->expects($this->atLeastOnce())->method('regenerateId'); - - $visitor = $this->getMockBuilder(Visitor::class) - ->disableOriginalConstructor() - ->setMethods(['getSessionId']) - ->getMock(); - $visitor->expects($this->atLeastOnce())->method('getSessionId') - ->willReturnOnConsecutiveCalls('session_id_1', 'session_id_2'); - $visitorCollection = $this->getMockBuilder( - CollectionFactory::class - ) - ->disableOriginalConstructor() - ->setMethods(['addFieldToFilter', 'getItems'])->getMock(); - $visitorCollection->expects($this->atLeastOnce())->method('addFieldToFilter')->willReturnSelf(); - $visitorCollection->expects($this->atLeastOnce())->method('getItems')->willReturn([$visitor, $visitor]); - $this->visitorCollectionFactory->expects($this->atLeastOnce())->method('create') - ->willReturn($visitorCollection); - $this->saveHandler->expects($this->atLeastOnce())->method('destroy') - ->withConsecutive( - ['session_id_1'], - ['session_id_2'] - ); - $this->assertTrue($this->accountManagement->changePassword($email, $currentPassword, $newPassword)); } @@ -1702,30 +1688,8 @@ function ($string) { $this->customerSecure->expects($this->once())->method('setRpToken')->with(null); $this->customerSecure->expects($this->once())->method('setRpTokenCreatedAt')->with(null); $this->customerSecure->expects($this->any())->method('setPasswordHash')->willReturn(null); + $this->sessionCleanerMock->expects($this->once())->method('clearFor')->with($customerId)->willReturnSelf(); - $this->sessionManager->method('isSessionExists')->willReturn(false); - $this->sessionManager->expects($this->atLeastOnce())->method('getSessionId'); - $this->sessionManager->expects($this->atLeastOnce())->method('regenerateId'); - $visitor = $this->getMockBuilder(Visitor::class) - ->disableOriginalConstructor() - ->setMethods(['getSessionId']) - ->getMock(); - $visitor->expects($this->atLeastOnce())->method('getSessionId') - ->willReturnOnConsecutiveCalls('session_id_1', 'session_id_2'); - $visitorCollection = $this->getMockBuilder( - CollectionFactory::class - ) - ->disableOriginalConstructor() - ->setMethods(['addFieldToFilter', 'getItems'])->getMock(); - $visitorCollection->expects($this->atLeastOnce())->method('addFieldToFilter')->willReturnSelf(); - $visitorCollection->expects($this->atLeastOnce())->method('getItems')->willReturn([$visitor, $visitor]); - $this->visitorCollectionFactory->expects($this->atLeastOnce())->method('create') - ->willReturn($visitorCollection); - $this->saveHandler->expects($this->atLeastOnce())->method('destroy') - ->withConsecutive( - ['session_id_1'], - ['session_id_2'] - ); $this->assertTrue($this->accountManagement->resetPassword($customerEmail, $resetToken, $newPassword)); } 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/Model/Metadata/Form/ImageTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php index ec154e2c657b1..8017c367c081d 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php @@ -12,18 +12,26 @@ use Magento\Customer\Api\Data\ValidationRuleInterface; use Magento\Customer\Model\FileProcessor; use Magento\Customer\Model\FileProcessorFactory; -use Magento\Customer\Model\Metadata\Form\File; use Magento\Customer\Model\Metadata\Form\Image; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Request\Http; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\UploaderFactory; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteFactory; +use Magento\Framework\Filesystem\Directory\Write; +use Magento\Framework\Filesystem\Driver\File as Driver; +use Magento\Framework\Filesystem\Io\File; use Magento\Framework\Url\EncoderInterface; use Magento\MediaStorage\Model\File\Validator\NotProtectedExtension; use PHPUnit\Framework\MockObject\MockObject; /** + * Tests Metadata/Form/Image class + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ImageTest extends AbstractFormTestCase @@ -68,6 +76,34 @@ class ImageTest extends AbstractFormTestCase */ private $fileProcessorFactoryMock; + /** + * @var File|\PHPUnit\Framework\MockObject\MockObject + */ + private $ioFileSystemMock; + + /** + * @var DirectoryList|\PHPUnit\Framework\MockObject\MockObject + */ + private $directoryListMock; + + /** + * @var WriteFactory|\PHPUnit\Framework\MockObject\MockObject + */ + private $writeFactoryMock; + + /** + * @var Write|\PHPUnit\Framework\MockObject\MockObject + */ + private $mediaEntityTmpDirectoryMock; + + /** + * @var Driver|\PHPUnit\Framework\MockObject\MockObject + */ + private $driverMock; + + /** + * @inheritdoc + */ protected function setUp(): void { parent::setUp(); @@ -101,13 +137,38 @@ protected function setUp(): void $this->fileProcessorFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->fileProcessorMock); + $this->ioFileSystemMock = $this->getMockBuilder(File::class) + ->disableOriginalConstructor() + ->getMock(); + $this->directoryListMock = $this->getMockBuilder(DirectoryList::class) + ->disableOriginalConstructor() + ->getMock(); + $this->writeFactoryMock = $this->getMockBuilder(WriteFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->mediaEntityTmpDirectoryMock = $this->getMockBuilder(Write::class) + ->disableOriginalConstructor() + ->getMock(); + $this->driverMock = $this->getMockBuilder(Driver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->writeFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->mediaEntityTmpDirectoryMock); + $this->mediaEntityTmpDirectoryMock->expects($this->any()) + ->method('getDriver') + ->willReturn($this->driverMock); } /** + * Initializes an image instance + * * @param array $data - * @return File + * @return Image + * @throws FileSystemException */ - private function initialize(array $data) + private function initialize(array $data): Image { return new Image( $this->localeMock, @@ -122,10 +183,17 @@ private function initialize(array $data) $this->fileSystemMock, $this->uploaderFactoryMock, $this->fileProcessorFactoryMock, - $this->imageContentFactory + $this->imageContentFactory, + $this->ioFileSystemMock, + $this->directoryListMock, + $this->writeFactoryMock ); } + /** + * Test for validateValue method for not valid file + * @throws LocalizedException + */ public function testValidateIsNotValidFile() { $value = [ @@ -151,6 +219,10 @@ public function testValidateIsNotValidFile() $this->assertEquals(['"realFileName" is not a valid file.'], $model->validateValue($value)); } + /** + * Test for validateValue method + * @throws LocalizedException + */ public function testValidate() { $value = [ @@ -158,6 +230,14 @@ public function testValidate() 'name' => 'logo.gif', ]; + $this->ioFileSystemMock->expects($this->any()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn([ + 'filename' => 'logo', + 'extension' => 'gif' + ]); + $this->attributeMetadataMock->expects($this->once()) ->method('getStoreLabel') ->willReturn('File Input Field Label'); @@ -167,6 +247,11 @@ public function testValidate() ->with(FileProcessor::TMP_DIR . '/' . $value['name']) ->willReturn(true); + $this->ioFileSystemMock->expects($this->once()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn(['extension' => 'gif']); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, @@ -176,6 +261,10 @@ public function testValidate() $this->assertTrue($model->validateValue($value)); } + /** + * Test for validateValue method for max file size + * @throws LocalizedException + */ public function testValidateMaxFileSize() { $value = [ @@ -196,6 +285,14 @@ public function testValidateMaxFileSize() ->method('getValue') ->willReturn($maxFileSize); + $this->ioFileSystemMock->expects($this->any()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn([ + 'filename' => 'logo', + 'extension' => 'gif' + ]); + $this->attributeMetadataMock->expects($this->once()) ->method('getStoreLabel') ->willReturn('File Input Field Label'); @@ -208,6 +305,11 @@ public function testValidateMaxFileSize() ->with(FileProcessor::TMP_DIR . '/' . $value['name']) ->willReturn(true); + $this->ioFileSystemMock->expects($this->once()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn(['extension' => 'gif']); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, @@ -217,6 +319,10 @@ public function testValidateMaxFileSize() $this->assertEquals(['"logo.gif" exceeds the allowed file size.'], $model->validateValue($value)); } + /** + * Test for validateValue method for max image width + * @throws LocalizedException + */ public function testValidateMaxImageWidth() { $value = [ @@ -236,6 +342,14 @@ public function testValidateMaxImageWidth() ->method('getValue') ->willReturn($maxImageWidth); + $this->ioFileSystemMock->expects($this->any()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn([ + 'filename' => 'logo', + 'extension' => 'gif' + ]); + $this->attributeMetadataMock->expects($this->once()) ->method('getStoreLabel') ->willReturn('File Input Field Label'); @@ -248,6 +362,11 @@ public function testValidateMaxImageWidth() ->with(FileProcessor::TMP_DIR . '/' . $value['name']) ->willReturn(true); + $this->ioFileSystemMock->expects($this->once()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn(['extension' => 'gif']); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, @@ -257,6 +376,10 @@ public function testValidateMaxImageWidth() $this->assertEquals(['"logo.gif" width exceeds allowed value of 1 px.'], $model->validateValue($value)); } + /** + * Test for validateValue method for max image height + * @throws LocalizedException + */ public function testValidateMaxImageHeight() { $value = [ @@ -276,6 +399,14 @@ public function testValidateMaxImageHeight() ->method('getValue') ->willReturn($maxImageHeight); + $this->ioFileSystemMock->expects($this->any()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn([ + 'filename' => 'logo', + 'extension' => 'gif' + ]); + $this->attributeMetadataMock->expects($this->once()) ->method('getStoreLabel') ->willReturn('File Input Field Label'); @@ -288,6 +419,11 @@ public function testValidateMaxImageHeight() ->with(FileProcessor::TMP_DIR . '/' . $value['name']) ->willReturn(true); + $this->ioFileSystemMock->expects($this->once()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn(['extension' => 'gif']); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, @@ -297,6 +433,10 @@ public function testValidateMaxImageHeight() $this->assertEquals(['"logo.gif" height exceeds allowed value of 1 px.'], $model->validateValue($value)); } + /** + * Test for compactValue method + * @throws LocalizedException + */ public function testCompactValueNoChanges() { $originValue = 'filename.ext1'; @@ -314,6 +454,10 @@ public function testCompactValueNoChanges() $this->assertEquals($originValue, $model->compactValue($value)); } + /** + * Test for compactValue method for address image + * @throws LocalizedException + */ public function testCompactValueUiComponentAddress() { $originValue = 'filename.ext1'; @@ -322,20 +466,33 @@ public function testCompactValueUiComponentAddress() 'file' => 'filename.ext2', ]; + $this->driverMock->expects($this->once()) + ->method('getRealPathSafety') + ->with($value['file']) + ->willReturn($value['file']); + $this->mediaEntityTmpDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->willReturn($value['file']); + $this->mediaEntityTmpDirectoryMock->expects($this->once()) + ->method('getRelativePath') + ->willReturn($value['file']); $this->fileProcessorMock->expects($this->once()) ->method('moveTemporaryFile') ->with($value['file']) - ->willReturn(true); - + ->willReturn($value['file']); $model = $this->initialize([ 'value' => $originValue, 'isAjax' => false, 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, ]); - $this->assertTrue($model->compactValue($value)); + $this->assertEquals($value['file'], $model->compactValue($value)); } + /** + * Test for compactValue method for image + * @throws LocalizedException + */ public function testCompactValueUiComponentCustomer() { $originValue = 'filename.ext1'; @@ -348,9 +505,9 @@ public function testCompactValueUiComponentCustomer() $base64EncodedData = 'encoded_data'; - $this->fileProcessorMock->expects($this->once()) + $this->mediaEntityTmpDirectoryMock->expects($this->once()) ->method('isExist') - ->with(FileProcessor::TMP_DIR . '/' . $value['file']) + ->with($value['file']) ->willReturn(true); $this->fileProcessorMock->expects($this->once()) ->method('getBase64EncodedData') @@ -390,6 +547,10 @@ public function testCompactValueUiComponentCustomer() $this->assertEquals($imageContentMock, $model->compactValue($value)); } + /** + * Test for compactValue method for non-existing customer + * @throws LocalizedException + */ public function testCompactValueUiComponentCustomerNotExists() { $originValue = 'filename.ext1'; @@ -400,9 +561,9 @@ public function testCompactValueUiComponentCustomerNotExists() 'type' => 'image', ]; - $this->fileProcessorMock->expects($this->once()) + $this->mediaEntityTmpDirectoryMock->expects($this->once()) ->method('isExist') - ->with(FileProcessor::TMP_DIR . '/' . $value['file']) + ->with($value['file']) ->willReturn(false); $model = $this->initialize([ diff --git a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php index 807c1084e979d..ae48926f12612 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php @@ -17,9 +17,30 @@ use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; class RegionTest extends TestCase { + /** + * Simulate "serialize" method of a form element. + * + * @param string[] $keys + * @param array $data + * @return string + */ + private function mockSerialize(array $keys, array $data): string + { + $attributes = []; + foreach ($keys as $key) { + if (empty($data[$key])) { + continue; + } + $attributes[] = $key .'="' .$data[$key] .'"'; + } + + return implode(' ', $attributes); + } + /** * @param array $regionCollection * @dataProvider renderDataProvider @@ -34,14 +55,25 @@ public function testRender($regionCollection) ['isRegionRequired'] ); $escaperMock = $this->createMock(Escaper::class); + /** @var MockObject|AbstractElement $elementMock */ $elementMock = $this->createPartialMock( AbstractElement::class, - ['getForm', 'getHtmlAttributes'] + ['getForm', 'getHtmlAttributes', 'serialize'] + ); + $elementMock->method('serialize')->willReturnCallback( + function (array $attributes) use ($elementMock): string { + return $this->mockSerialize($attributes, $elementMock->getData()); + } ); $countryMock = $this->getMockBuilder(AbstractElement::class) - ->addMethods(['getValue']) + ->addMethods(['getValue', 'serialize']) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $countryMock->method('serialize')->willReturnCallback( + function (array $attributes) use ($countryMock): string { + return $this->mockSerialize($attributes, $countryMock->getData()); + } + ); $regionMock = $this->createMock( AbstractElement::class ); diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php index 7466505d2cca5..b7ed01ee9da8b 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php @@ -249,7 +249,8 @@ public function testSave() 'getEmail', 'getWebsiteId', 'getAddresses', - 'setAddresses' + 'setAddresses', + 'getGroupId', ] ); $customerSecureData = $this->getMockBuilder(CustomerSecure::class) @@ -433,7 +434,8 @@ public function testSaveWithPasswordHash() 'getEmail', 'getWebsiteId', 'getAddresses', - 'setAddresses' + 'setAddresses', + 'getGroupId' ] ); $customerModel->expects($this->atLeastOnce()) 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/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php new file mode 100644 index 0000000000000..d05c10c00e6c3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Observer; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Observer\UpgradeOrderCustomerEmailObserver; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * For testing upgrade order customer email + */ +class UpgradeOrderCustomerEmailObserverTest extends TestCase +{ + private const NEW_CUSTOMER_EMAIL = "test@test.com"; + private const ORIGINAL_CUSTOMER_EMAIL = "origtest@test.com"; + + /** + * @var UpgradeOrderCustomerEmailObserver + */ + private $orderCustomerEmailObserver; + + /** + * @var Observer|MockObject + */ + private $observerMock; + + /** + * @var OrderRepositoryInterface|MockObject + */ + private $orderRepositoryMock; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private $searchCriteriaBuilderMock; + + /** + * @var Event|MockObject + */ + private $eventMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->getMock(); + + $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerDataObject', 'getOrigCustomerDataObject']) + ->getMock(); + + $this->observerMock = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->observerMock->expects($this->any())->method('getEvent')->willReturn($this->eventMock); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->orderCustomerEmailObserver = $this->objectManagerHelper->getObject( + UpgradeOrderCustomerEmailObserver::class, + [ + 'orderRepository' => $this->orderRepositoryMock, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + ] + ); + } + + /** + * Verifying that the order email is not updated when the customer email is not updated + * + */ + public function testUpgradeOrderCustomerEmailWhenMailIsNotChanged(): void + { + $customer = $this->createCustomerMock(); + $originalCustomer = $this->createCustomerMock(); + + $this->setCustomerToEventMock($customer); + $this->setOriginalCustomerToEventMock($originalCustomer); + + $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL); + $this->setCustomerEmail($customer, self::ORIGINAL_CUSTOMER_EMAIL); + + $this->whenOrderRepositoryGetListIsNotCalled(); + + $this->orderCustomerEmailObserver->execute($this->observerMock); + } + + /** + * Verifying that the order email is updated after the customer updates their email + * + */ + public function testUpgradeOrderCustomerEmail(): void + { + $customer = $this->createCustomerMock(); + $originalCustomer = $this->createCustomerMock(); + $orderCollectionMock = $this->createOrderMock(); + + $this->setCustomerToEventMock($customer); + $this->setOriginalCustomerToEventMock($originalCustomer); + + $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL); + $this->setCustomerEmail($customer, self::NEW_CUSTOMER_EMAIL); + + $this->whenOrderRepositoryGetListIsCalled($orderCollectionMock); + + $this->whenOrderCollectionSetDataToAllIsCalled($orderCollectionMock); + + $this->whenOrderCollectionSaveIsCalled($orderCollectionMock); + + $this->orderCustomerEmailObserver->execute($this->observerMock); + } + + private function createCustomerMock(): MockObject + { + $customer = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + return $customer; + } + + private function createOrderMock(): MockObject + { + $orderCollectionMock = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + + return $orderCollectionMock; + } + + private function setCustomerToEventMock(MockObject $customer): void + { + $this->eventMock->expects($this->once()) + ->method('getCustomerDataObject') + ->willReturn($customer); + } + + private function setOriginalCustomerToEventMock(MockObject $originalCustomer): void + { + $this->eventMock->expects($this->once()) + ->method('getOrigCustomerDataObject') + ->willReturn($originalCustomer); + } + + private function setCustomerEmail(MockObject $originalCustomer, string $email): void + { + $originalCustomer->expects($this->once()) + ->method('getEmail') + ->willReturn($email); + } + + private function whenOrderRepositoryGetListIsCalled(MockObject $orderCollectionMock): void + { + $searchCriteriaMock = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('create') + ->willReturn($searchCriteriaMock); + + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('addFilter') + ->willReturn($this->searchCriteriaBuilderMock); + + $this->orderRepositoryMock->expects($this->once()) + ->method('getList') + ->with($searchCriteriaMock) + ->willReturn($orderCollectionMock); + } + + private function whenOrderCollectionSetDataToAllIsCalled(MockObject $orderCollectionMock): void + { + $orderCollectionMock->expects($this->once()) + ->method('setDataToAll') + ->with(OrderInterface::CUSTOMER_EMAIL, self::NEW_CUSTOMER_EMAIL); + } + + private function whenOrderCollectionSaveIsCalled(MockObject $orderCollectionMock): void + { + $orderCollectionMock->expects($this->once()) + ->method('save'); + } + + private function whenOrderRepositoryGetListIsNotCalled(): void + { + $this->searchCriteriaBuilderMock->expects($this->never()) + ->method('addFilter'); + $this->searchCriteriaBuilderMock->expects($this->never()) + ->method('create'); + + $this->orderRepositoryMock->expects($this->never()) + ->method('getList'); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php index e9bd30940a064..08fd76afb76d3 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php @@ -80,11 +80,16 @@ protected function setUp(): void } /** - * @covers \Magento\Customer\Ui\Component\DataProvider\Document::getCustomAttribute + * @dataProvider getGenderAttributeDataProvider + * @covers \Magento\Customer\Ui\Component\DataProvider\Document::getCustomAttribute + * @param int $genderId + * @param string $attributeValue + * @param string $attributeLabel */ - public function testGetGenderAttribute() + public function testGetGenderAttribute(int $genderId, string $attributeValue, string $attributeLabel): void { - $genderId = 1; + $expectedResult = !empty($attributeValue) ? $attributeLabel : $genderId; + $this->document->setData('gender', $genderId); $this->groupRepository->expects(static::never()) @@ -106,11 +111,37 @@ public function testGetGenderAttribute() ->willReturn([$genderId => $option]); $option->expects(static::once()) + ->method('getValue') + ->willReturn($attributeValue); + + $option->expects(static::any()) ->method('getLabel') - ->willReturn('Male'); + ->willReturn($attributeLabel); $attribute = $this->document->getCustomAttribute('gender'); - static::assertEquals('Male', $attribute->getValue()); + static::assertEquals($expectedResult, $attribute->getValue()); + } + + /** + * Data provider for testGetGenderAttribute + * @return array + */ + public function getGenderAttributeDataProvider() + { + return [ + 'with valid gender label and value' => [ + 1, '1', 'Male' + ], + 'with empty gender label' => [ + 2, '2', '' + ], + 'with empty gender value' => [ + 3, '', 'test' + ], + 'with empty gender label and value' => [ + 4, '', '' + ] + ]; } /** diff --git a/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php b/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php index 468a9e7946f2d..e802505caf9d1 100644 --- a/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php +++ b/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php @@ -5,18 +5,23 @@ */ namespace Magento\Customer\Ui\Component\DataProvider; +use Exception; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\OptionInterface; +use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Customer\Model\AccountManagement; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; /** * Class Document + * + * Set the attribute label and value for UI Component + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Document extends \Magento\Framework\View\Element\UiComponent\DataProvider\Document { @@ -127,7 +132,7 @@ public function getCustomAttribute($attributeCode) private function setGenderValue() { $value = $this->getData(self::$genderAttributeCode); - + if (!$value) { $this->setCustomAttribute(self::$genderAttributeCode, 'N/A'); return; @@ -135,8 +140,15 @@ private function setGenderValue() try { $attributeMetadata = $this->customerMetadata->getAttributeMetadata(self::$genderAttributeCode); - $option = $attributeMetadata->getOptions()[$value]; - $this->setCustomAttribute(self::$genderAttributeCode, $option->getLabel()); + $options = $attributeMetadata->getOptions(); + array_walk( + $options, + function (OptionInterface $option) use ($value) { + if ($option->getValue() == $value) { + $this->setCustomAttribute(self::$genderAttributeCode, $option->getLabel()); + } + } + ); } catch (NoSuchEntityException $e) { $this->setCustomAttribute(self::$genderAttributeCode, 'N/A'); } @@ -199,6 +211,7 @@ private function setConfirmationValue() * Update lock expires value. Method set account lock text value to match what is shown in grid * * @return void + * @throws Exception */ private function setAccountLockValue() { diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml index fca625d847a1d..569f9d09c2087 100644 --- a/app/code/Magento/Customer/etc/adminhtml/system.xml +++ b/app/code/Magento/Customer/etc/adminhtml/system.xml @@ -40,6 +40,7 @@ <field id="default_group" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Default Group</label> <source_model>Magento\Customer\Model\Config\Source\Group</source_model> + <validate>required-entry</validate> </field> <field id="viv_domestic_group" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Group for Valid VAT ID - Domestic</label> diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index ba0bb3bac6a98..437912d29a334 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -61,6 +61,7 @@ type="Magento\Customer\Block\Account\SortLink"/> <preference for="Magento\Customer\Model\Group\RetrieverInterface" type="Magento\Customer\Model\Group\Retriever"/> + <preference for="Magento\Customer\Api\SessionCleanerInterface" type="Magento\Customer\Model\Session\SessionCleaner"/> <type name="Magento\Customer\Model\Session"> <arguments> <argument name="configShare" xsi:type="object">Magento\Customer\Model\Config\Share\Proxy</argument> diff --git a/app/code/Magento/Customer/etc/events.xml b/app/code/Magento/Customer/etc/events.xml index 2a724498a0359..0194f91c591f5 100644 --- a/app/code/Magento/Customer/etc/events.xml +++ b/app/code/Magento/Customer/etc/events.xml @@ -16,6 +16,7 @@ <observer name="customer_visitor" instance="Magento\Customer\Observer\Visitor\BindQuoteCreateObserver" /> </event> <event name="customer_save_after_data_object"> + <observer name="upgrade_order_customer_email" instance="Magento\Customer\Observer\UpgradeOrderCustomerEmailObserver"/> <observer name="upgrade_quote_customer_email" instance="Magento\Customer\Observer\UpgradeQuoteCustomerEmailObserver"/> </event> </config> diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml index f2457963a5f3d..d07d1a61c3d62 100644 --- a/app/code/Magento/Customer/etc/webapi_rest/di.xml +++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml @@ -13,10 +13,22 @@ <arguments> <argument name="userContexts" xsi:type="array"> <item name="customerSessionUserContext" xsi:type="array"> - <item name="type" xsi:type="object">Magento\Customer\Model\Authorization\CustomerSessionUserContext</item> + <item name="type" xsi:type="object">Magento\Customer\Model\Authorization\CustomerSessionUserContext\Proxy</item> <item name="sortOrder" xsi:type="string">20</item> </item> </argument> </arguments> </type> + <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/i18n/en_US.csv b/app/code/Magento/Customer/i18n/en_US.csv index 7c88ffec1170a..0a81e70964b4c 100644 --- a/app/code/Magento/Customer/i18n/en_US.csv +++ b/app/code/Magento/Customer/i18n/en_US.csv @@ -542,3 +542,4 @@ Addresses,Addresses "The store view is not in the associated website.","The store view is not in the associated website." "The Store View selected for sending Welcome email from is not related to the customer's associated website.","The Store View selected for sending Welcome email from is not related to the customer's associated website." "Add/Update Address","Add/Update Address" +"The specified customer group id does not exist.","The specified customer group id does not exist." diff --git a/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml b/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml index 6525e7f29f36b..f4a3d2db6b687 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml @@ -5,8 +5,16 @@ */ /** @var \Magento\Customer\Block\Adminhtml\System\Config\Validatevat $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php + $merchantCountryField = $block->escapeJs($block->getMerchantCountryField()); + $merchantVatNumberField = $block->escapeJs($block->getMerchantVatNumberField()); + $ajaxUrl = $block->escapeJs($block->getAjaxUrl()); + $errorMessage = $block->escapeJs($block->escapeHtml(__('Error during VAT Number verification.'))); + + $scriptString = <<<script require(['prototype'], function(){ //<![CDATA[ @@ -14,21 +22,22 @@ require(['prototype'], function(){ var validationMessage = $('validation_result'); params = { - country: $('<?= $block->escapeJs($block->getMerchantCountryField()) ?>').value, - vat: $('<?= $block->escapeJs($block->getMerchantVatNumberField()) ?>').value + country: $('{$merchantCountryField}').value, + vat: $('{$merchantVatNumberField}').value }; - new Ajax.Request('<?= $block->escapeJs($block->escapeUrl($block->getAjaxUrl())) ?>', { + new Ajax.Request('{$ajaxUrl}', { parameters: params, onSuccess: function(response) { - var result = '<?= $block->escapeJs($block->escapeHtml(__('Error during VAT Number verification.'))) ?>'; + var result = '{$errorMessage}'; try { if (response.responseText.isJSON()) { response = response.responseText.evalJSON(); result = response.message; } if (response.valid == 1) { - validationMessage.removeClassName('hidden').removeClassName('admin__field-error').addClassName('note'); + validationMessage.removeClassName('hidden').removeClassName('admin__field-error'). + addClassName('note'); validationMessage.setStyle({color:'green'}); } else { validationMessage.removeClassName('hidden').addClassName('admin__field-error'); @@ -45,11 +54,19 @@ require(['prototype'], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> + <div class="actions actions-validate-vat"> - <p class="admin__field-error hidden" id="validation_result" style="margin-bottom:10px;"></p> - <button onclick="javascript:validateVat(); return false;" class="action-validate-vat" type="button" id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>"> + <p class="admin__field-error hidden" id="validation_result"></p> + <button class="action-validate-vat" type="button" id="<?= /* @noEscape */ $block->getHtmlId() ?>"> <span><?= $block->escapeHtml($block->getButtonLabel()) ?></span> </button> </div> - +<?= /* @noEscape */ $secureRenderer->renderTag('style', [], '#validation_result {margin-bottom: 10px;}', false); ?> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'validateVat();event.preventDefault();', + '#' . /* @noEscape */ $block->getHtmlId() +); ?> diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml index 434e5606cd032..81ad513351841 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml @@ -5,39 +5,41 @@ */ /* @var \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->getCartHeader()) : ?> +<?php if ($block->getCartHeader()): ?> <div class="content-header skip-header"> <table> <tr> - <td style="width:50%;"><h4><?= $block->escapeHtml($block->getCartHeader()) ?></h4></td> + <td><h4><?= $block->escapeHtml($block->getCartHeader()) ?></h4></td> </tr> </table> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("width:50%;", 'div.content-header.skip-header table tr td') ?> </div> <?php endif ?> <?= $block->getGridParentHtml() ?> -<?php if ($block->canDisplayContainer()) : ?> +<?php if ($block->canDisplayContainer()): ?> <?php $listType = $block->getJsObjectName(); ?> - <script> + <?php $scriptString = <<<script require([ "Magento_Ui/js/modal/alert", "Magento_Ui/js/modal/confirm", "Magento_Catalog/catalog/product/composite/configure" ], function(alert, confirm){ - <?= $block->escapeJs($block->getJsObjectName()) ?>cartControl = { + {$block->escapeJs($block->getJsObjectName())}cartControl = { reload: function (params) { if (!params) { params = {}; } - <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = params; - <?= $block->escapeJs($block->getJsObjectName()) ?>.reload(); - <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = {}; + {$block->escapeJs($block->getJsObjectName())}.reloadParams = params; + {$block->escapeJs($block->getJsObjectName())}.reload(); + {$block->escapeJs($block->getJsObjectName())}.reloadParams = {}; }, configureItem: function (itemId) { - productConfigure.setOnLoadIFrameCallback('<?= $block->escapeJs($listType) ?>', this.cbOnLoadIframe.bind(this)); - productConfigure.showItemConfiguration('<?= $block->escapeJs($listType) ?>', itemId); + productConfigure.setOnLoadIFrameCallback('{$block->escapeJs($listType)}', this.cbOnLoadIframe.bind(this)); + productConfigure.showItemConfiguration('{$block->escapeJs($listType)}', itemId); return false; }, @@ -53,14 +55,14 @@ if (!itemId) { alert({ - content: '<?= $block->escapeJs(__('No item specified.')) ?>' + content: '{$block->escapeJs(__('No item specified.'))}' }); return false; } confirm({ - content: '<?= $block->escapeJs(__('Are you sure you want to remove this item?')) ?>', + content: '{$block->escapeJs(__('Are you sure you want to remove this item?'))}', actions: { confirm: function(){ self.reload({'delete':itemId}); @@ -70,21 +72,24 @@ } }; - <?php +script; + $params = [ 'customer_id' => $block->getCustomerId(), 'website_id' => $block->getWebsiteId(), ]; - ?> + $scriptString .= <<<script productConfigure.addListType( - '<?= $block->escapeJs($listType) ?>', + '{$block->escapeJs($listType)}', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/configure', $params))) ?>', - urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/update', $params))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('customer/cart_product_composite_cart/configure', $params))}', + urlConfirm: '{$block->escapeJs($block->getUrl('customer/cart_product_composite_cart/update', $params))}' } ); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif ?> <br /> diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-listing.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-listing.js index 912d4b32130ec..1578677414b78 100644 --- a/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-listing.js +++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-listing.js @@ -62,7 +62,9 @@ define([ * @param {Object} data - customer address */ deleteMassaction: function (data) { - var ids = _.map(data, function (val) { + var ids = data.selected || this.selections().selected(); + + ids = _.map(ids, function (val) { return parseFloat(val); }); @@ -70,7 +72,7 @@ define([ }, /** - * Delete customer address by ids + * Delete customer address and selections by provided ids. * * @param {Array} ids */ @@ -85,6 +87,10 @@ define([ if (ids.indexOf(defaultBillingId) !== -1) { this.source.set('data.default_billing_address', []); } + + _.each(ids, function (id) { + this.selections().deselect(id.toString(), false); + }, this); } }); }); diff --git a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml index 78bbd612f5b70..065d87792665f 100644 --- a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml +++ b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml @@ -511,7 +511,7 @@ <imports>true</imports> </dataLinks> <externalProvider>customer_address_listing.customer_address_listing_data_source</externalProvider> - <selectionsProvider>customer_address_listing.customer_address_listing.customer_address_listing_columns.ids</selectionsProvider> + <selectionsProvider>customer_address_listing.customer_address_listing.customer_address_columns.ids</selectionsProvider> <autoRender>true</autoRender> <dataScope>customer_address_listing</dataScope> <ns>customer_address_listing</ns> diff --git a/app/code/Magento/Customer/view/frontend/email/change_email.html b/app/code/Magento/Customer/view/frontend/email/change_email.html index bd961ad99ec40..5341a2dc67ad5 100644 --- a/app/code/Magento/Customer/view/frontend/email/change_email.html +++ b/app/code/Magento/Customer/view/frontend/email/change_email.html @@ -18,8 +18,5 @@ {{trans "We have received a request to change the following information associated with your account at %store_name: email." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> -<br> - -<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html b/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html index 4f5c85b2381f3..ed2af7ada669e 100644 --- a/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html +++ b/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html @@ -18,8 +18,5 @@ {{trans "We have received a request to change the following information associated with your account at %store_name: email, password." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> -<br> - -<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/password_reset.html b/app/code/Magento/Customer/view/frontend/email/password_reset.html index cab05a89227b6..e83c484afaec9 100644 --- a/app/code/Magento/Customer/view/frontend/email/password_reset.html +++ b/app/code/Magento/Customer/view/frontend/email/password_reset.html @@ -6,7 +6,6 @@ --> <!--@subject {{trans "Your %store_name password has been changed" store_name=$store.frontend_name}} @--> <!--@vars { -"var customer.name":"Customer Name", "var store.frontend_name":"Store Name", "var store_email":"Store Email", "var store_phone":"Store Phone", @@ -19,8 +18,5 @@ {{trans "We have received a request to change the following information associated with your account at %store_name: password." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> -<br> - -<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml b/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml index 0d4cf3c721d14..8355e229fe452 100644 --- a/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml @@ -5,11 +5,11 @@ */ /** @var \Magento\Customer\Block\Account\AuthenticationPopup $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div id="authenticationPopup" data-bind="scope:'authenticationPopup'" style="display: none;"> - <script> - window.authenticationPopup = <?= /* @noEscape */ $block->getSerializedConfig() ?>; - </script> +<div id="authenticationPopup" data-bind="scope:'authenticationPopup', style: {display: 'none'}"> + <?php $scriptString = 'window.authenticationPopup = ' . /* @noEscape */ $block->getSerializedConfig(); ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <!-- ko template: getTemplate() --><!-- /ko --> <script type="text/x-magento-init"> { @@ -17,7 +17,9 @@ "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?> }, "*": { - "Magento_Ui/js/block-loader": "<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif'))) ?>" + "Magento_Ui/js/block-loader": "<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl( + 'images/loader-1.gif' + ))) ?>" } } </script> diff --git a/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml b/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml index 14827388e3894..e9df83a5913c1 100644 --- a/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml @@ -4,15 +4,16 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Customer\Block\Account\AuthorizationLink $block */ - +/** + * @var \Magento\Customer\Block\Account\AuthorizationLink $block + * @var \Magento\Framework\Escaper $escaper + */ $dataPostParam = ''; if ($block->isLoggedIn()) { $dataPostParam = sprintf(" data-post='%s'", $block->getPostParams()); } ?> -<li class="authorization-link" data-label="<?= $block->escapeHtml(__('or')) ?>"> - <a <?= /* @noEscape */ $block->getLinkAttributes() ?><?= /* @noEscape */ $dataPostParam ?>> - <?= $block->escapeHtml($block->getLabel()) ?> - </a> +<li class="link authorization-link" data-label="<?= $escaper->escapeHtml(__('or')) ?>"> + <a <?= /* @noEscape */ $block->getLinkAttributes() ?> + <?= /* @noEscape */ $dataPostParam ?>><?= $escaper->escapeHtml($block->getLabel()) ?></a> </li> diff --git a/app/code/Magento/Customer/view/frontend/templates/account/link/my-account.phtml b/app/code/Magento/Customer/view/frontend/templates/account/link/my-account.phtml new file mode 100644 index 0000000000000..5649ac7c7d7b4 --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/templates/account/link/my-account.phtml @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @var \Magento\Customer\Block\Account\Link $block + * @var \Magento\Framework\Escaper $escaper + */ +?> +<li class="link my-account-link"> + <a <?= /* @noEscape */ $block->getLinkAttributes() ?>><?= $escaper->escapeHtml($block->getLabel()) ?></a> +</li> diff --git a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml index e7519c7c3320b..7f09361e4d505 100644 --- a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml @@ -7,6 +7,7 @@ /** @var \Magento\Customer\Block\Address\Edit $block */ /** @var \Magento\Customer\ViewModel\Address $viewModel */ /** @var \Magento\Framework\Escaper $escaper */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $viewModel = $block->getViewModel(); ?> <?php $_company = $block->getLayout()->createBlock(\Magento\Customer\Block\Widget\Company::class) ?> @@ -152,9 +153,10 @@ $viewModel = $block->getViewModel(); id="zip" class="input-text validate-zip-international <?= $escaper->escapeHtmlAttr($_postcodeValidationClass) ?>"> - <div role="alert" class="message warning" style="display:none"> + <div role="alert" class="message warning"> <span></span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div.message.warning') ?> </div> </div> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml index 89b86f8af8e55..9821cff73a3dd 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml @@ -7,6 +7,7 @@ use Magento\Customer\Block\Widget\Name; /** @var \Magento\Customer\Block\Form\Edit $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form class="form form-edit-account" action="<?= $block->escapeUrl($block->getUrl('customer/account/editPost')) ?>" @@ -46,6 +47,7 @@ use Magento\Customer\Block\Widget\Name; <span><?= $block->escapeHtml(__('Change Password')) ?></span> </label> </div> + <?= $block->getChildHtml('fieldset_edit_info_additional') ?> </fieldset> <fieldset class="fieldset password" data-container="change-email-password"> @@ -117,16 +119,19 @@ use Magento\Customer\Block\Widget\Name; </div> </div> </form> -<script> +<?php $ignore = /* @noEscape */ $_dob->isEnabled() ? '\'input[id$="full"]\'' : 'null'; +$scriptString = <<<script require([ "jquery", "mage/mage" ], function($){ var dataForm = $('#form-validate'); - var ignore = <?= /* @noEscape */ $_dob->isEnabled() ? '\'input[id$="full"]\'' : 'null' ?>; + var ignore = {$ignore}; dataForm.mage('validation', { - <?php if ($_dob->isEnabled()): ?> +script; +if ($_dob->isEnabled()): + $scriptString .= <<<script errorPlacement: function(error, element) { if (element.prop('id').search('full') !== -1) { var dobElement = $(element).parents('.customer-dob'), @@ -140,13 +145,19 @@ use Magento\Customer\Block\Widget\Name; } }, ignore: ':hidden:not(' + ignore + ')' - <?php else: ?> +script; +else: + $scriptString .= <<<script ignore: ignore ? ':hidden:not(' + ignore + ')' : ':hidden' - <?php endif ?> +script; +endif; +$scriptString .= <<<script }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php $changeEmailAndPasswordTitle = $block->escapeHtml(__('Change Email and Password')) ?> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml index be201afa8f66c..caa501d48da83 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml @@ -6,6 +6,8 @@ * @var $block \Magento\Customer\Block\Account\Forgotpassword */ +// phpcs:disable Generic.Files.LineLength.TooLong + /** @var \Magento\Customer\Block\Account\Forgotpassword $block */ ?> <form class="form password forget" @@ -32,3 +34,12 @@ </div> </div> </form> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "form-validate" + } + } + } +</script> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml index ef74b0062c023..a1d1a0260672a 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +// phpcs:disable Generic.Files.LineLength.TooLong + /** @var \Magento\Customer\Block\Form\Login $block */ ?> <div class="block block-customer-login"> @@ -22,13 +24,22 @@ <div class="field email required"> <label class="label" for="email"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> - <input name="login[username]" value="<?= $block->escapeHtmlAttr($block->getUsername()) ?>" <?php if ($block->isAutocompleteDisabled()) : ?> autocomplete="off"<?php endif; ?> id="email" type="email" class="input-text" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" data-mage-init='{"mage/trim-input":{}}' data-validate="{required:true, 'validate-email':true}"> + <input name="login[username]" value="<?= $block->escapeHtmlAttr($block->getUsername()) ?>" + <?php if ($block->isAutocompleteDisabled()): ?> autocomplete="off"<?php endif; ?> + id="email" type="email" class="input-text" + title="<?= $block->escapeHtmlAttr(__('Email')) ?>" + data-mage-init='{"mage/trim-input":{}}' + data-validate="{required:true, 'validate-email':true}"> </div> </div> <div class="field password required"> <label for="pass" class="label"><span><?= $block->escapeHtml(__('Password')) ?></span></label> <div class="control"> - <input name="login[password]" type="password" <?php if ($block->isAutocompleteDisabled()) : ?> autocomplete="off"<?php endif; ?> class="input-text" id="pass" title="<?= $block->escapeHtmlAttr(__('Password')) ?>" data-validate="{required:true}"> + <input name="login[password]" type="password" + <?php if ($block->isAutocompleteDisabled()): ?> autocomplete="off"<?php endif; ?> + class="input-text" id="pass" + title="<?= $block->escapeHtmlAttr(__('Password')) ?>" + data-validate="{required:true}"> </div> </div> <?= $block->getChildHtml('form_additional_info') ?> @@ -41,3 +52,12 @@ </div> </div> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "login-form" + } + } + } +</script> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml index f7d10f6df1728..99040706e50ac 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis use Magento\Customer\Helper\Address; /** @var \Magento\Customer\Block\Form\Register $block */ /** @var \Magento\Framework\Escaper $escaper */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +/** @var Magento\Customer\Helper\Address $addressHelper */ +$addressHelper = $block->getData('addressHelper'); +/** @var \Magento\Directory\Helper\Data $directoryHelper */ +$directoryHelper = $block->getData('directoryHelper'); $formData = $block->getFormData(); ?> <?php $displayAll = $block->getConfig('general/region/display_all'); ?> @@ -63,11 +67,12 @@ $formData = $block->getFormData(); <?php if ($_gender->isEnabled()): ?> <?= $_gender->setGender($formData->getGender())->toHtml() ?> <?php endif ?> + <?= $block->getChildHtml('fieldset_create_info_additional') ?> </fieldset> <?php if ($block->getShowAddressFields()): ?> - <?php $cityValidationClass = $this->helper(Address::class)->getAttributeValidationClass('city'); ?> - <?php $postcodeValidationClass = $this->helper(Address::class)->getAttributeValidationClass('postcode'); ?> - <?php $regionValidationClass = $this->helper(Address::class)->getAttributeValidationClass('region'); ?> + <?php $cityValidationClass = $addressHelper->getAttributeValidationClass('city'); ?> + <?php $postcodeValidationClass = $addressHelper->getAttributeValidationClass('postcode'); ?> + <?php $regionValidationClass = $addressHelper->getAttributeValidationClass('region'); ?> <fieldset class="fieldset address"> <legend class="legend"><span><?= $escaper->escapeHtml(__('Address Information')) ?></span></legend><br> <input type="hidden" name="create_address" value="1" /> @@ -88,8 +93,7 @@ $formData = $block->getFormData(); <?php endif ?> <?php - $_streetValidationClass = $this->helper(Address::class) - ->getAttributeValidationClass('street'); + $_streetValidationClass = $addressHelper->getAttributeValidationClass('street'); ?> <div class="field street required"> @@ -106,7 +110,7 @@ $formData = $block->getFormData(); <div class="nested"> <?php $_streetValidationClass = trim(str_replace('required-entry', '', $_streetValidationClass)); - $streetLines = $this->helper(Address::class)->getStreetLines(); + $streetLines = $addressHelper->getStreetLines(); ?> <?php for ($_i = 2, $_n = $streetLines; $_i <= $_n; $_i++): ?> <div class="field additional"> @@ -144,19 +148,19 @@ $formData = $block->getFormData(); <select id="region_id" name="region_id" title="<?= /* @noEscape */ $block->getAttributeData()->getFrontendLabel('region') ?>" - class="validate-select region_id" - style="display: none;"> + class="validate-select region_id"> <option value=""> <?= $escaper->escapeHtml(__('Please select a region, state or province.')) ?> </option> </select> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'select#region_id') ?> <input type="text" id="region" name="region" value="<?= $escaper->escapeHtml($block->getRegion()) ?>" title="<?= /* @noEscape */ $block->getAttributeData()->getFrontendLabel('region') ?>" - class="input-text <?= $escaper->escapeHtmlAttr($regionValidationClass) ?>" - style="display:none;"> + class="input-text <?= $escaper->escapeHtmlAttr($regionValidationClass) ?>"> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'input#region') ?> </div> </div> @@ -273,17 +277,20 @@ $formData = $block->getFormData(); </div> </div> </form> -<script> +<?php $ignore = /* @noEscape */ $_dob->isEnabled() ? '\'input[id$="full"]\'' : 'null'; +$scriptString = <<<script require([ 'jquery', 'mage/mage' ], function($){ var dataForm = $('#form-validate'); - var ignore = <?= /* @noEscape */ $_dob->isEnabled() ? '\'input[id$="full"]\'' : 'null' ?>; + var ignore = {$ignore}; dataForm.mage('validation', { - <?php if ($_dob->isEnabled()): ?> +script; +if ($_dob->isEnabled()): + $scriptString .= <<<script errorPlacement: function(error, element) { if (element.prop('id').search('full') !== -1) { var dobElement = $(element).parents('.customer-dob'), @@ -297,20 +304,24 @@ require([ } }, ignore: ':hidden:not(' + ignore + ')' - <?php else: ?> +script; +else: + $scriptString .= <<<script ignore: ignore ? ':hidden:not(' + ignore + ')' : ':hidden' - <?php endif ?> +script; +endif; +$scriptString .= <<<script }).find('input:text').attr('autocomplete', 'off'); - dataForm.submit(function () { - $(this).find(':submit').attr('disabled', 'disabled'); - }); - dataForm.bind("invalid-form.validate", function () { - $(this).find(':submit').prop('disabled', false); - }); - }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php if ($block->getShowAddressFields()): ?> + <?php + $regionJson = /* @noEscape */ $directoryHelper->getRegionJson(); + $regionId = (int) $formData->getRegionId(); + $countriesWithOptionalZip = /* @noEscape */ $directoryHelper->getCountriesWithOptionalZip(true); + ?> <script type="text/x-magento-init"> { "#country": { @@ -320,11 +331,9 @@ require([ "regionInputId": "#region", "postcodeId": "#zip", "form": "#form-validate", - "regionJson": <?= /* @noEscape */ $this->helper(\Magento\Directory\Helper\Data::class) - ->getRegionJson() ?>, - "defaultRegion": "<?= (int) $formData->getRegionId() ?>", - "countriesWithOptionalZip": <?= /* @noEscape */ $this->helper(\Magento\Directory\Helper\Data::class) - ->getCountriesWithOptionalZip(true) ?> + "regionJson": {$regionJson}, + "defaultRegion": "{$regionId}", + "countriesWithOptionalZip": {$countriesWithOptionalZip} } } } @@ -337,6 +346,11 @@ require([ "passwordStrengthIndicator": { "formSelector": "form.form-create-account" } + }, + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "form-validate" + } } } </script> diff --git a/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml b/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml index a1a784076bac3..eb50ea6454788 100644 --- a/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml @@ -5,16 +5,21 @@ */ /** @var \Magento\Customer\Block\CustomerData $block */ + +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper ?> <script type="text/x-magento-init"> { "*": { "Magento_Customer/js/customer-data": { - "sectionLoadUrl": "<?= $block->escapeJs($block->escapeUrl($block->getCustomerDataUrl('customer/section/load'))) ?>", + "sectionLoadUrl": "<?= $block->escapeJs($block->getCustomerDataUrl('customer/section/load')) ?>", "expirableSectionLifetime": <?= (int)$block->getExpirableSectionLifetime() ?>, - "expirableSectionNames": <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getExpirableSectionNames()) ?>, + "expirableSectionNames": <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class) + ->jsonEncode($block->getExpirableSectionNames()) ?>, "cookieLifeTime": "<?= $block->escapeJs($block->getCookieLifeTime()) ?>", - "updateSessionUrl": "<?= $block->escapeJs($block->escapeUrl($block->getCustomerDataUrl('customer/account/updateSession'))) ?>" + "updateSessionUrl": "<?= $block->escapeJs( + $block->getCustomerDataUrl('customer/account/updateSession') + ) ?>" } } } diff --git a/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js new file mode 100644 index 0000000000000..b941ec7a254d8 --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js @@ -0,0 +1,22 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/mage' +], function ($) { + 'use strict'; + + return function (config) { + var dataForm = $('#' + config.formId); + + dataForm.submit(function () { + $(this).find(':submit').attr('disabled', 'disabled'); + }); + dataForm.bind('invalid-form.validate', function () { + $(this).find(':submit').prop('disabled', false); + }); + }; +}); 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 770ea47d754d3..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 @@ -24,20 +24,12 @@ define([ invalidateCacheByCloseCookieSession, dataProvider, buffer, - customerData; + customerData, + deferred = $.Deferred(); url.setBaseUrl(window.BASE_URL); options.sectionLoadUrl = url.build('customer/section/load'); - //TODO: remove global change, in this case made for initNamespaceStorage - $.cookieStorage.setConf({ - path: '/', - expires: 1 - }); - - storage = $.initNamespaceStorage('mage-cache-storage').localStorage; - storageInvalidation = $.initNamespaceStorage('mage-cache-storage-section-invalidation').localStorage; - /** * @param {Object} invalidateOptions */ @@ -221,6 +213,18 @@ define([ } }, + /** + * Storage init + */ + initStorage: function () { + $.cookieStorage.setConf({ + path: '/', + expires: new Date(Date.now() + parseInt(options.cookieLifeTime, 10) * 1000) + }); + storage = $.initNamespaceStorage('mage-cache-storage').localStorage; + storageInvalidation = $.initNamespaceStorage('mage-cache-storage-section-invalidation').localStorage; + }, + /** * Retrieve the list of sections that has expired since last page reload. * @@ -257,6 +261,9 @@ define([ } }); + //remove expired section names of previously installed/enable modules + expiredSectionNames = _.intersection(expiredSectionNames, sectionConfig.getSectionNames()); + return _.uniq(expiredSectionNames); }, @@ -341,15 +348,26 @@ define([ $.cookieStorage.set('section_data_ids', sectionDataIds); }, + /** + * Checks if customer data is initialized. + * + * @returns {jQuery.Deferred} + */ + getInitCustomerData: function () { + return deferred.promise(); + }, + /** * @param {Object} settings * @constructor */ 'Magento_Customer/js/customer-data': function (settings) { options = settings; + customerData.initStorage(); invalidateCacheBySessionTimeOut(settings); invalidateCacheByCloseCookieSession(); customerData.init(); + deferred.resolve(); } }; diff --git a/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js b/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js index a6d1de5fde255..eba9a8c3ea7ae 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js +++ b/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js @@ -6,7 +6,7 @@ /** * @api */ -define([], function () { +define(['underscore'], function (_) { 'use strict'; /** @@ -44,7 +44,7 @@ define([], function () { vatId: addressData['vat_id'], sameAsBilling: addressData['same_as_billing'], saveInAddressBook: addressData['save_in_address_book'], - customAttributes: addressData['custom_attributes'], + customAttributes: _.toArray(addressData['custom_attributes']).reverse(), /** * @return {*} diff --git a/app/code/Magento/Customer/view/frontend/web/js/validation.js b/app/code/Magento/Customer/view/frontend/web/js/validation.js index 1f7f24d5ac031..6b9983c0af873 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/validation.js +++ b/app/code/Magento/Customer/view/frontend/web/js/validation.js @@ -8,6 +8,20 @@ define([ ], function ($, moment, utils) { 'use strict'; + $.validator.addMethod( + 'validate-date', + function (value, element, params) { + var dateFormat = utils.normalizeDate(params.dateFormat); + + if (value === '') { + return true; + } + + return moment(value, dateFormat, true).isValid(); + }, + $.mage.__('Invalid date') + ); + $.validator.addMethod( 'validate-dob', function (value, element, params) { diff --git a/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php new file mode 100644 index 0000000000000..ef3e86788c43f --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Api; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Interface for customer data validator + */ +interface ValidateCustomerDataInterface +{ + /** + * Validate customer data + * + * @param array $customerData + * @throws GraphQlInputException + */ + public function execute(array $customerData): void; +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php index 3861ce324ea7d..95d68d69d71e7 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php @@ -7,6 +7,9 @@ namespace Magento\CustomerGraphQl\Model\Customer; +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; @@ -27,53 +30,41 @@ class ValidateCustomerData */ private $emailAddressValidator; + /** + * @var ValidateCustomerDataInterface[] + */ + private $validators = []; + /** * ValidateCustomerData constructor. * * @param GetAllowedCustomerAttributes $getAllowedCustomerAttributes * @param EmailAddressValidator $emailAddressValidator + * @param array $validators */ public function __construct( GetAllowedCustomerAttributes $getAllowedCustomerAttributes, - EmailAddressValidator $emailAddressValidator + EmailAddressValidator $emailAddressValidator, + $validators = [] ) { $this->getAllowedCustomerAttributes = $getAllowedCustomerAttributes; $this->emailAddressValidator = $emailAddressValidator; + $this->validators = $validators; } /** * Validate customer data * * @param array $customerData - * - * @return void - * * @throws GraphQlInputException + * @throws LocalizedException + * @throws NoSuchEntityException */ - public function execute(array $customerData): void + public function execute(array $customerData) { - $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData)); - $errorInput = []; - - foreach ($attributes as $attributeInfo) { - if ($attributeInfo->getIsRequired() - && (!isset($customerData[$attributeInfo->getAttributeCode()]) - || $customerData[$attributeInfo->getAttributeCode()] == '') - ) { - $errorInput[] = $attributeInfo->getDefaultFrontendLabel(); - } - } - - if ($errorInput) { - throw new GraphQlInputException( - __('Required parameters are missing: %1', [implode(', ', $errorInput)]) - ); - } - - if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) { - throw new GraphQlInputException( - __('"%1" is not a valid email address.', $customerData['email']) - ); + /** @var ValidateCustomerDataInterface $validator */ + foreach ($this->validators as $validator) { + $validator->execute($customerData); } } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php new file mode 100644 index 0000000000000..87f8831550f04 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; + +/** + * Validates an email + */ +class ValidateEmail implements ValidateCustomerDataInterface +{ + /** + * @var EmailAddressValidator + */ + private $emailAddressValidator; + + /** + * ValidateEmail constructor. + * + * @param EmailAddressValidator $emailAddressValidator + */ + public function __construct(EmailAddressValidator $emailAddressValidator) + { + $this->emailAddressValidator = $emailAddressValidator; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) { + throw new GraphQlInputException( + __('"%1" is not a valid email address.', $customerData['email']) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php new file mode 100644 index 0000000000000..463372a63d8d5 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Model\Data\AttributeMetadata; +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Validates gender value + */ +class ValidateGender implements ValidateCustomerDataInterface +{ + /** + * @var CustomerMetadataInterface + */ + private $customerMetadata; + + /** + * ValidateGender constructor. + * + * @param CustomerMetadataInterface $customerMetadata + */ + public function __construct(CustomerMetadataInterface $customerMetadata) + { + $this->customerMetadata = $customerMetadata; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + if (isset($customerData['gender']) && $customerData['gender']) { + /** @var AttributeMetadata $genderData */ + $options = $this->customerMetadata->getAttributeMetadata('gender')->getOptions(); + + $isValid = false; + foreach ($options as $optionData) { + if ($optionData->getValue() && $optionData->getValue() == $customerData['gender']) { + $isValid = true; + } + } + + if (!$isValid) { + throw new GraphQlInputException( + __('"%1" is not a valid gender value.', $customerData['gender']) + ); + } + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php new file mode 100644 index 0000000000000..fdf4fa1c824f2 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\CustomerGraphQl\Model\Customer\GetAllowedCustomerAttributes; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Validates required attributes + */ +class ValidateRequiredArguments implements ValidateCustomerDataInterface +{ + /** + * Get allowed/required customer attributes + * + * @var GetAllowedCustomerAttributes + */ + private $getAllowedCustomerAttributes; + + /** + * ValidateRequiredArguments constructor. + * + * @param GetAllowedCustomerAttributes $getAllowedCustomerAttributes + */ + public function __construct(GetAllowedCustomerAttributes $getAllowedCustomerAttributes) + { + $this->getAllowedCustomerAttributes = $getAllowedCustomerAttributes; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData)); + $errorInput = []; + + foreach ($attributes as $attributeInfo) { + if ($attributeInfo->getIsRequired() + && (!isset($customerData[$attributeInfo->getAttributeCode()]) + || $customerData[$attributeInfo->getAttributeCode()] == '') + ) { + $errorInput[] = $attributeInfo->getDefaultFrontendLabel(); + } + } + + if ($errorInput) { + throw new GraphQlInputException( + __('Required parameters are missing: %1', [implode(', ', $errorInput)]) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerEmail.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerEmail.php new file mode 100644 index 0000000000000..e77cea69a3f9d --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerEmail.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver; + +use Magento\CustomerGraphQl\Model\Customer\ExtractCustomerData; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\CustomerGraphQl\Model\Customer\UpdateCustomerAccount; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Customer email update, used for GraphQL request processing + */ +class UpdateCustomerEmail implements ResolverInterface +{ + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @var UpdateCustomerAccount + */ + private $updateCustomerAccount; + + /** + * @var ExtractCustomerData + */ + private $extractCustomerData; + + /** + * @param GetCustomer $getCustomer + * @param UpdateCustomerAccount $updateCustomerAccount + * @param ExtractCustomerData $extractCustomerData + */ + public function __construct( + GetCustomer $getCustomer, + UpdateCustomerAccount $updateCustomerAccount, + ExtractCustomerData $extractCustomerData + ) { + $this->getCustomer = $getCustomer; + $this->updateCustomerAccount = $updateCustomerAccount; + $this->extractCustomerData = $extractCustomerData; + } + + /** + * Resolve customer email update mutation + * + * @param \Magento\Framework\GraphQl\Config\Element\Field $field + * @param \Magento\Framework\GraphQl\Query\Resolver\ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return array|Value + * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + /** @var ContextInterface $context */ + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + + $customer = $this->getCustomer->execute($context); + $this->updateCustomerAccount->execute( + $customer, + [ + 'email' => $args['email'] ?? null, + 'password' => $args['password'] ?? null + ], + $context->getExtensionAttributes()->getStore() + ); + + $data = $this->extractCustomerData->execute($customer); + + return ['customer' => $data]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/etc/extension_attributes.xml b/app/code/Magento/CustomerGraphQl/etc/extension_attributes.xml index 26840551eaeb8..b8bdb5a46ca81 100644 --- a/app/code/Magento/CustomerGraphQl/etc/extension_attributes.xml +++ b/app/code/Magento/CustomerGraphQl/etc/extension_attributes.xml @@ -8,5 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd"> <extension_attributes for="Magento\GraphQl\Model\Query\ContextInterface"> <attribute code="is_customer" type="boolean"/> + <attribute code="customer_group_id" type="integer"/> </extension_attributes> -</config> \ No newline at end of file +</config> diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml index 1ba0e457430e0..3ed77a2ad563c 100644 --- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml @@ -29,4 +29,14 @@ </argument> </arguments> </type> + <!-- Validate input customer data --> + <type name="Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData"> + <arguments> + <argument name="validators" xsi:type="array"> + <item name="validateRequiredArguments" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateRequiredArguments</item> + <item name="validateEmail" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateEmail</item> + <item name="validateGender" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateGender</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CustomerGraphQl/etc/module.xml b/app/code/Magento/CustomerGraphQl/etc/module.xml index eeed4862bbbfd..b15df7fc0be6b 100644 --- a/app/code/Magento/CustomerGraphQl/etc/module.xml +++ b/app/code/Magento/CustomerGraphQl/etc/module.xml @@ -6,5 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_CustomerGraphQl"/> + <module name="Magento_CustomerGraphQl" > + <sequence> + <module name="Magento_Customer"/> + </sequence> + </module> </config> diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index 42a52bd5a99a7..5eed9a38a0350 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -18,13 +18,16 @@ type Mutation { generateCustomerToken(email: String!, password: String!): CustomerToken @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\GenerateCustomerToken") @doc(description:"Retrieve the customer token") changeCustomerPassword(currentPassword: String!, newPassword: String!): Customer @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ChangePassword") @doc(description:"Changes the password for the logged-in customer") createCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") - updateCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Update the customer's personal information") + createCustomerV2 (input: CustomerCreateInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") + updateCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Deprecated. Use UpdateCustomerV2 instead.") + updateCustomerV2 (input: CustomerUpdateInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Update the customer's personal information") revokeCustomerToken: RevokeCustomerTokenOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RevokeCustomerToken") @doc(description:"Revoke the customer token") createCustomerAddress(input: CustomerAddressInput!): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomerAddress") @doc(description: "Create customer address") updateCustomerAddress(id: Int!, input: CustomerAddressInput): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerAddress") @doc(description: "Update customer address") deleteCustomerAddress(id: Int!): Boolean @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\DeleteCustomerAddress") @doc(description: "Delete customer address") requestPasswordResetEmail(email: String!): Boolean @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RequestPasswordResetEmail") @doc(description: "Request an email with a reset password token for the registered customer identified by the specified email.") resetPassword(email: String!, resetPasswordToken: String!, newPassword: String!): Boolean @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ResetPassword") @doc(description: "Reset a customer's password using the reset password token that the customer received in an email after requesting it using requestPasswordResetEmail.") + updateCustomerEmail(email: String!, password: String!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerEmail") @doc(description: "") } input CustomerAddressInput { @@ -78,6 +81,34 @@ input CustomerInput { is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") } +input CustomerCreateInput { + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String! @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String! @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String! @doc(description: "The customer's email address. Required for customer creation") + dob: String @doc(description: "Deprecated: Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") + password: String @doc(description: "The customer's password") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") +} + +input CustomerUpdateInput { + date_of_birth: String @doc(description: "The customer's date of birth") + dob: String @doc(description: "Deprecated: Use `date_of_birth` instead") + firstname: String @doc(description: "The customer's first name") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") + lastname: String @doc(description: "The customer's family name") + middlename: String @doc(description: "The customer's middle name") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") +} + type CustomerOutput { customer: Customer! } diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index a5947f48bea5f..f15f920fe95f4 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -169,7 +169,7 @@ class Address extends AbstractCustomer * Array of region parameters * * @var array - * @deprecated field not in use + * @deprecated 100.3.4 field not in use */ protected $_regionParameters; @@ -199,19 +199,19 @@ class Address extends AbstractCustomer /** * @var \Magento\Eav\Model\Config - * @deprecated field not-in use + * @deprecated 100.3.4 field not-in use */ protected $_eavConfig; /** * @var \Magento\Customer\Model\AddressFactory - * @deprecated not utilized anymore + * @deprecated 100.3.4 not utilized anymore */ protected $_addressFactory; /** * @var \Magento\Framework\Stdlib\DateTime - * @deprecated the property isn't used + * @deprecated 100.3.4 the property isn't used */ protected $dateTime; @@ -667,8 +667,6 @@ protected function _prepareDataForUpdate(array $rowData): array $defaults[$table][$customerId][$attributeCode] = $addressId; } } - // let's try to find region ID - $entityRow[self::COLUMN_REGION_ID] = null; if (!empty($entityRow[self::COLUMN_REGION]) && !empty($entityRow[self::COLUMN_COUNTRY_ID])) { $entityRow[self::COLUMN_REGION_ID] = $this->getCountryRegionId( @@ -679,6 +677,8 @@ protected function _prepareDataForUpdate(array $rowData): array $entityRow[self::COLUMN_REGION] = $entityRow[self::COLUMN_REGION_ID] !== null ? $this->_regions[$entityRow[self::COLUMN_REGION_ID]] : $entityRow[self::COLUMN_REGION]; + } elseif ($newAddress) { + $entityRow[self::COLUMN_REGION_ID] = null; } if ($newAddress) { $entityRowNew = $entityRow; @@ -908,7 +908,7 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) } } - if (isset($rowData[self::COLUMN_COUNTRY_ID])) { + if (!empty($rowData[self::COLUMN_COUNTRY_ID])) { if (isset($rowData[self::COLUMN_POSTCODE]) && !$this->postcodeValidator->isValid( $rowData[self::COLUMN_COUNTRY_ID], @@ -918,8 +918,7 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, self::COLUMN_POSTCODE); } - if (isset($rowData[self::COLUMN_REGION]) - && !empty($rowData[self::COLUMN_REGION]) + if (!empty($rowData[self::COLUMN_REGION]) && count($this->getCountryRegions($rowData[self::COLUMN_COUNTRY_ID])) > 0 && null === $this->getCountryRegionId( $rowData[self::COLUMN_COUNTRY_ID], diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 8f5bb951ce737..5ebf242bd6ac4 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -367,6 +367,7 @@ protected function _getNextEntityId() * @param array|AbstractSource $rows * * @return void + * @since 100.2.3 */ public function prepareCustomerData($rows): void { @@ -387,6 +388,7 @@ public function prepareCustomerData($rows): void /** * @inheritDoc + * @since 100.2.3 */ public function validateData() { diff --git a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php index 11f326e6dfc8f..f08278af864e7 100644 --- a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php +++ b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php @@ -130,7 +130,7 @@ public function addCustomerByArray(array $customer): Storage /** * Add customer to array * - * @deprecated @see addCustomerByArray + * @deprecated 100.3.0 @see addCustomerByArray * @param DataObject $customer * @return $this */ diff --git a/app/code/Magento/Deploy/Package/LocaleResolver.php b/app/code/Magento/Deploy/Package/LocaleResolver.php new file mode 100644 index 0000000000000..d9909edf31284 --- /dev/null +++ b/app/code/Magento/Deploy/Package/LocaleResolver.php @@ -0,0 +1,208 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Deploy\Package; + +use InvalidArgumentException; +use Magento\Framework\App\Area; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\AppInterface; +use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Validator\Locale; +use Magento\Store\Model\Config\StoreView; +use Magento\User\Api\Data\UserInterface; +use Magento\User\Model\ResourceModel\User\Collection as UserCollection; +use Magento\User\Model\ResourceModel\User\CollectionFactory as UserCollectionFactory; +use Psr\Log\LoggerInterface; + +/** + * Deployment Package Locale Resolver class + */ +class LocaleResolver +{ + /** + * Parameter to force deploying certain languages for the admin, without any users having configured them yet. + */ + const ADMIN_LOCALES_FOR_DEPLOY = 'admin_locales_for_deploy'; + + /** + * @var StoreView + */ + private $storeView; + + /** + * @var UserCollectionFactory + */ + private $userCollFactory; + + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * @var Locale + */ + private $locale; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var array|null + */ + private $usedStoreLocales; + + /** + * @var array|null + */ + private $usedAdminLocales; + + /** + * LocaleResolver constructor. + * + * @param StoreView $storeView + * @param UserCollectionFactory $userCollectionFactory + * @param DeploymentConfig $deploymentConfig + * @param Locale $locale + * @param LoggerInterface $logger + */ + public function __construct( + StoreView $storeView, + UserCollectionFactory $userCollectionFactory, + DeploymentConfig $deploymentConfig, + Locale $locale, + LoggerInterface $logger + ) { + $this->storeView = $storeView; + $this->userCollFactory = $userCollectionFactory; + $this->deploymentConfig = $deploymentConfig; + $this->locale = $locale; + $this->logger = $logger; + } + + /** + * Get locales that are used for a given theme. + * If it is a frontend theme, return supported frontend languages. + * If it is an adminhtml theme, return languages that admin users have configured together with deployment config. + * + * @param Package $package + * + * @return array + */ + public function getUsedPackageLocales(Package $package): array + { + switch ($package->getArea()) { + case Area::AREA_ADMINHTML: + $locales = $this->getUsedAdminLocales(); + break; + case Area::AREA_FRONTEND: + $locales = $this->getUsedStoreLocales(); + break; + default: + $locales = array_merge($this->getUsedAdminLocales(), $this->getUsedStoreLocales()); + } + return $this->validateLocales($locales); + } + + /** + * Get used admin user locales, en_US is always included by default. + * + * @return array + */ + private function getUsedAdminLocales(): array + { + if ($this->usedAdminLocales === null) { + $deploymentConfig = $this->getDeploymentAdminLocales(); + $this->usedAdminLocales = array_merge([AppInterface::DISTRO_LOCALE_CODE], $deploymentConfig); + + if (!$this->isDbConnectionAvailable()) { + return $this->usedAdminLocales; + } + + /** @var UserCollection $userCollection */ + $userCollection = $this->userCollFactory->create(); + /** @var UserInterface $adminUser */ + foreach ($userCollection as $adminUser) { + $this->usedAdminLocales[] = $adminUser->getInterfaceLocale(); + } + } + return $this->usedAdminLocales; + } + + /** + * Get used store locales. + * + * @return array + */ + private function getUsedStoreLocales(): array + { + if ($this->usedStoreLocales === null) { + $this->usedStoreLocales = $this->isDbConnectionAvailable() + ? $this->storeView->retrieveLocales() + : [AppInterface::DISTRO_LOCALE_CODE]; + } + return $this->usedStoreLocales; + } + + /** + * Strip out duplicates and break on invalid locale codes. + * + * @param array $usedLocales + * + * @return array + * @throws InvalidArgumentException if unknown locale is provided by the store configuration + */ + private function validateLocales(array $usedLocales): array + { + return array_map( + function ($locale) { + if (!$this->locale->isValid($locale)) { + throw new InvalidArgumentException( + $locale . ' argument has invalid value, run info:language:list for list of available locales' + ); + } + + return $locale; + }, + array_unique($usedLocales) + ); + } + + /** + * Check if a database connection is already set up. + * + * @return bool + */ + private function isDbConnectionAvailable(): bool + { + try { + $connections = $this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS, []); + } catch (LocalizedException $exception) { + $this->logger->critical($exception); + } + return !empty($connections); + } + + /** + * Retrieve deployment configuration for admin locales that have to be deployed. + * + * @return array|mixed|string|null + */ + private function getDeploymentAdminLocales(): array + { + try { + return $this->deploymentConfig + ->get(self::ADMIN_LOCALES_FOR_DEPLOY, []); + } catch (LocalizedException $exception) { + return []; + } + } +} diff --git a/app/code/Magento/Deploy/Package/PackagePool.php b/app/code/Magento/Deploy/Package/PackagePool.php index 9057f50fb3c91..f31af8f9e081f 100644 --- a/app/code/Magento/Deploy/Package/PackagePool.php +++ b/app/code/Magento/Deploy/Package/PackagePool.php @@ -7,7 +7,7 @@ use Magento\Deploy\Collector\Collector; use Magento\Deploy\Console\DeployStaticOptions as Options; -use Magento\Framework\AppInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Design\ThemeInterface; use Magento\Framework\View\Design\Theme\ListInterface; @@ -41,22 +41,30 @@ class PackagePool */ private $collected = false; + /** + * @var LocaleResolver|null + */ + private $localeResolver; + /** * PackagePool constructor * * @param Collector $collector * @param ListInterface $themeCollection * @param PackageFactory $packageFactory + * @param LocaleResolver|null $localeResolver */ public function __construct( Collector $collector, ListInterface $themeCollection, - PackageFactory $packageFactory + PackageFactory $packageFactory, + ?LocaleResolver $localeResolver = null ) { $this->collector = $collector; $themeCollection->clear()->resetConstraints(); $this->themes = $themeCollection->getItems(); $this->packageFactory = $packageFactory; + $this->localeResolver = $localeResolver ?: ObjectManager::getInstance()->get(LocaleResolver::class); } /** @@ -221,17 +229,18 @@ private function ensurePackagesForRequiredLocales(array $options) private function ensureRequiredLocales(array $options) { if (empty($options[Options::LANGUAGE]) || $options[Options::LANGUAGE][0] === 'all') { - $forcedLocales = [AppInterface::DISTRO_LOCALE_CODE]; + $forcedLocales = []; } else { $forcedLocales = $options[Options::LANGUAGE]; } - $resultPackages = $this->packages; - foreach ($resultPackages as $package) { + foreach ($this->packages as $package) { if ($package->getTheme() === Package::BASE_THEME) { continue; } - foreach ($forcedLocales as $locale) { + + $locales = $forcedLocales ?: $this->localeResolver->getUsedPackageLocales($package); + foreach ($locales as $locale) { $this->ensurePackage([ 'area' => $package->getArea(), 'theme' => $package->getTheme(), diff --git a/app/code/Magento/Deploy/Process/Queue.php b/app/code/Magento/Deploy/Process/Queue.php index 6c8db345187cc..35d85c390b9c4 100644 --- a/app/code/Magento/Deploy/Process/Queue.php +++ b/app/code/Magento/Deploy/Process/Queue.php @@ -29,7 +29,7 @@ class Queue /** * Default max execution time */ - const DEFAULT_MAX_EXEC_TIME = 400; + const DEFAULT_MAX_EXEC_TIME = 900; /** * @var array @@ -96,6 +96,11 @@ class Queue */ private $lastJobStarted = 0; + /** + * @var int + */ + private $logDelay; + /** * @param AppState $appState * @param LocaleResolver $localeResolver @@ -157,11 +162,12 @@ public function getPackages() * Process jobs * * @return int + * @throws TimeoutException */ public function process() { $returnStatus = 0; - $logDelay = 10; + $this->logDelay = 10; $this->start = $this->lastJobStarted = time(); $packages = $this->packages; while (count($packages) && $this->checkTimeout()) { @@ -170,13 +176,7 @@ public function process() $this->assertAndExecute($name, $packages, $packageJob); } - // refresh current status in console once in 10 iterations (once in 5 sec) - if ($logDelay >= 10) { - $this->logger->info('.'); - $logDelay = 0; - } else { - $logDelay++; - } + $this->refreshStatus(); if ($this->isCanBeParalleled()) { // in parallel mode sleep before trying to check status and run new jobs @@ -193,9 +193,28 @@ public function process() $this->awaitForAllProcesses(); + if (!empty($packages)) { + throw new TimeoutException('Not all packages are deployed.'); + } + return $returnStatus; } + /** + * Refresh current status in console once in 10 iterations (once in 5 sec) + * + * @return void + */ + private function refreshStatus(): void + { + if ($this->logDelay >= 10) { + $this->logger->info('.'); + $this->logDelay = 0; + } else { + $this->logDelay++; + } + } + /** * Check that all depended packages deployed and execute * @@ -204,7 +223,7 @@ public function process() * @param array $packageJob * @return void */ - private function assertAndExecute($name, array & $packages, array $packageJob) + private function assertAndExecute($name, array &$packages, array $packageJob) { /** @var Package $package */ $package = $packageJob['package']; @@ -256,7 +275,6 @@ private function executePackage(Package $package, string $name, array &$packages */ private function awaitForAllProcesses() { - $logDelay = 10; while ($this->inProgress && $this->checkTimeout()) { foreach ($this->inProgress as $name => $package) { if ($this->isDeployed($package)) { @@ -264,13 +282,7 @@ private function awaitForAllProcesses() } } - // refresh current status in console once in 10 iterations (once in 5 sec) - if ($logDelay >= 10) { - $this->logger->info('.'); - $logDelay = 0; - } else { - $logDelay++; - } + $this->refreshStatus(); // sleep before checking parallel jobs status // phpcs:ignore Magento2.Functions.DiscouragedFunction diff --git a/app/code/Magento/Deploy/Process/TimeoutException.php b/app/code/Magento/Deploy/Process/TimeoutException.php new file mode 100644 index 0000000000000..2d8eb3ab2aad2 --- /dev/null +++ b/app/code/Magento/Deploy/Process/TimeoutException.php @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Deploy\Process; + +/** + * Exception is thrown if deploy process is finished due to timeout. + */ +class TimeoutException extends \RuntimeException +{ +} diff --git a/app/code/Magento/Deploy/Test/Unit/Package/LocaleResolverTest.php b/app/code/Magento/Deploy/Test/Unit/Package/LocaleResolverTest.php new file mode 100644 index 0000000000000..3c911d4c0c533 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Package/LocaleResolverTest.php @@ -0,0 +1,227 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Deploy\Test\Unit\Package; + +use Magento\Deploy\Package\LocaleResolver; +use Magento\Deploy\Package\Package; +use Magento\Framework\App\Area; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\AppInterface; +use Magento\Framework\Validator\Locale; +use Magento\Store\Model\Config\StoreView; +use Magento\User\Api\Data\UserInterface; +use Magento\User\Model\ResourceModel\User\Collection; +use Magento\User\Model\ResourceModel\User\CollectionFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * Deployment Package LocaleResolver class unit tests + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class LocaleResolverTest extends TestCase +{ + /** + * @var LocaleResolver + */ + private $localeResolver; + + /** + * @var Package|MockObject + */ + private $package; + + /** + * @var StoreView|MockObject + */ + private $storeView; + + /** + * @var CollectionFactory|MockObject + */ + private $userCollectionFactory; + + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfig; + + /** + * @var Locale|MockObject + */ + private $locale; + + /** + * @var MockObject|LoggerInterface + */ + private $logger; + + /** + * @var Collection|MockObject + */ + private $userCollection; + + /** + * @var UserInterface|MockObject + */ + private $adminUser; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->package = $this->createMock(Package::class); + $this->storeView = $this->createMock(StoreView::class); + $this->adminUser = $this->getMockForAbstractClass(UserInterface::class); + $this->userCollection = $this->createMock(Collection::class); + $this->userCollectionFactory = $this->createMock(CollectionFactory::class); + $this->userCollectionFactory->method('create')->willReturn($this->userCollection); + $this->deploymentConfig = $this->createMock(DeploymentConfig::class); + $this->locale = $this->createMock(Locale::class); + $this->logger = $this->getMockForAbstractClass( + LoggerInterface::class, + ['critical'], + '', + false + ); + $this->localeResolver = new LocaleResolver( + $this->storeView, + $this->userCollectionFactory, + $this->deploymentConfig, + $this->locale, + $this->logger + ); + } + + /** + * Test Get Used Package Locales when there is no DB connection set up yet + * Should only return en_US by default + */ + public function testGetUsedPackageLocalesNoDb() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_FRONTEND); + $this->deploymentConfig->expects(static::exactly(1))->method('get')->willReturn([]); + $this->storeView->expects(static::exactly(0))->method('retrieveLocales'); + $this->userCollectionFactory->expects(static::exactly(0))->method('create'); + $this->locale->method('isValid')->willReturn(true); + + $locales = $this->localeResolver->getUsedPackageLocales($this->package); + static::assertEquals( + [AppInterface::DISTRO_LOCALE_CODE], + $locales + ); + } + + /** + * Test Get Used Package Locales when there is no DB connection set up yet + * Should only return en_US by default + */ + public function testGetUsedPackageLocalesNoDbWithDeployment() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_ADMINHTML); + $this->deploymentConfig->expects(static::exactly(2))->method('get')->willReturn(['zh_SG'], []); + $this->storeView->expects(static::exactly(0))->method('retrieveLocales'); + $this->userCollectionFactory->expects(static::exactly(0))->method('create'); + $this->locale->method('isValid')->willReturn(true); + + $locales = $this->localeResolver->getUsedPackageLocales($this->package); + static::assertEquals( + [AppInterface::DISTRO_LOCALE_CODE, 'zh_SG'], + $locales + ); + } + + /** + * Test Get Used Package Locales when there is no DB connection set up yet + * Should only return en_US by default + */ + public function testGetUsedPackageLocalesIllegalLocale() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_ADMINHTML); + $this->deploymentConfig->expects(static::exactly(2))->method('get')->willReturn(['en_DE'], []); + $this->storeView->expects(static::exactly(0))->method('retrieveLocales'); + $this->userCollectionFactory->expects(static::exactly(0))->method('create'); + $this->locale->method('isValid')->willReturn(true, false); + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage( + 'en_DE argument has invalid value, run info:language:list for list of available locales' + ); + $this->localeResolver->getUsedPackageLocales($this->package); + } + + /** + * Test Get Used Package Locales for a frontend theme + * Should return used frontend languages + */ + public function testGetUsedPackageLocalesFrontend() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_FRONTEND); + $this->deploymentConfig->expects(static::exactly(1))->method('get')->willReturn(['default' => []]); + $this->storeView->expects(static::exactly(1))->method('retrieveLocales')->willReturn(['de_DE', 'en_GB']); + $this->userCollectionFactory->expects(static::exactly(0))->method('create'); + $this->locale->method('isValid')->willReturn(true); + + $locales = $this->localeResolver->getUsedPackageLocales($this->package); + static::assertEquals( + ['de_DE', 'en_GB'], + $locales + ); + } + + /** + * Test Get Used Package Locales for an admin theme + * Should return used admin languages, admin deployment configuration languages and en_US by default + */ + public function testGetUsedPackageLocalesAdmin() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_ADMINHTML); + $this->deploymentConfig->expects(static::exactly(2))->method('get')->willReturn(['de_AT'], ['default' => []]); + $this->storeView->expects(static::exactly(0))->method('retrieveLocales'); + $this->userCollectionFactory->expects(static::exactly(1))->method('create'); + $this->locale->method('isValid')->willReturn(true); + $this->adminUser->expects(static::exactly(2))->method('getInterfaceLocale')->willReturn('nl_NL', 'fr_FR'); + $this->userCollection->method('getIterator')->willReturn(new \ArrayIterator([ + $this->adminUser, + $this->adminUser, + ])); + + $locales = $this->localeResolver->getUsedPackageLocales($this->package); + static::assertEquals( + [AppInterface::DISTRO_LOCALE_CODE, 'de_AT', 'nl_NL', 'fr_FR'], + $locales + ); + } + + /** + * Test Get Used Package Locales for a theme that is neither frontend nor admin (hypothetical) + * Should return both used admin and used frontend languages, plus en_US by default + */ + public function testGetUsedPackageLocalesDefault() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_GLOBAL); + $this->deploymentConfig->expects(static::exactly(3))->method('get') + ->willReturn(['de_AT'], ['default' => []], ['default' => []]); + $this->storeView->expects(static::exactly(1))->method('retrieveLocales')->willReturn(['en_IE', 'fr_LU']); + $this->userCollectionFactory->expects(static::exactly(1))->method('create'); + $this->locale->method('isValid')->willReturn(true); + $this->adminUser->expects(static::exactly(2))->method('getInterfaceLocale')->willReturn('nl_NL', 'fr_FR'); + $this->userCollection->method('getIterator')->willReturn(new \ArrayIterator([ + $this->adminUser, + $this->adminUser, + ])); + + $locales = $this->localeResolver->getUsedPackageLocales($this->package); + static::assertEquals( + [AppInterface::DISTRO_LOCALE_CODE, 'de_AT', 'nl_NL', 'fr_FR', 'en_IE', 'fr_LU'], + $locales + ); + } +} 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/Console/Command/patch_template.php.dist b/app/code/Magento/Developer/Console/Command/patch_template.php.dist index f4fc25abcb29a..8e14b24bdc933 100644 --- a/app/code/Magento/Developer/Console/Command/patch_template.php.dist +++ b/app/code/Magento/Developer/Console/Command/patch_template.php.dist @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace %moduleName%\Setup\Patch\%patchType%; @@ -36,7 +37,7 @@ class %class% implements %implementsInterfaces% } %revertFunction% /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { @@ -44,12 +45,10 @@ class %class% implements %implementsInterfaces% } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { - return [ - - ]; + return []; } } diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php index ba98524bb665e..fc659c773c0af 100644 --- a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php +++ b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php @@ -21,11 +21,6 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug */ private $state; - /** - * @var ScopeConfigInterface - */ - private $scopeConfig; - /** * @var DeploymentConfig */ @@ -34,7 +29,6 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug /** * @param DriverInterface $filesystem * @param State $state - * @param ScopeConfigInterface $scopeConfig * @param DeploymentConfig $deploymentConfig * @param string $filePath * @throws \Exception @@ -42,14 +36,12 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug public function __construct( DriverInterface $filesystem, State $state, - ScopeConfigInterface $scopeConfig, DeploymentConfig $deploymentConfig, $filePath = null ) { parent::__construct($filesystem, $filePath); $this->state = $state; - $this->scopeConfig = $scopeConfig; $this->deploymentConfig = $deploymentConfig; } diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php index 3f5ff58640313..c6ee70fb9ce40 100644 --- a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php +++ b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php @@ -29,13 +29,10 @@ class Syslog extends \Magento\Framework\Logger\Handler\Syslog private $deploymentConfig; /** - * @param ScopeConfigInterface $scopeConfig Scope config * @param DeploymentConfig $deploymentConfig Deployment config * @param string $ident The string ident to be added to each message - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - ScopeConfigInterface $scopeConfig, DeploymentConfig $deploymentConfig, string $ident ) { diff --git a/app/code/Magento/Developer/Model/TemplateEngine/Decorator/DebugHints.php b/app/code/Magento/Developer/Model/TemplateEngine/Decorator/DebugHints.php index c689bc7aee80a..f331923f4b696 100644 --- a/app/code/Magento/Developer/Model/TemplateEngine/Decorator/DebugHints.php +++ b/app/code/Magento/Developer/Model/TemplateEngine/Decorator/DebugHints.php @@ -8,6 +8,10 @@ namespace Magento\Developer\Model\TemplateEngine\Decorator; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Decorates block with block and template hints * @@ -26,20 +30,39 @@ class DebugHints implements \Magento\Framework\View\TemplateEngineInterface */ private $_showBlockHints; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @var Random + */ + private $random; + /** * @param \Magento\Framework\View\TemplateEngineInterface $subject * @param bool $showBlockHints Whether to include block into the debugging information or not + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random */ - public function __construct(\Magento\Framework\View\TemplateEngineInterface $subject, $showBlockHints) - { + public function __construct( + \Magento\Framework\View\TemplateEngineInterface $subject, + $showBlockHints, + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { $this->_subject = $subject; $this->_showBlockHints = $showBlockHints; + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** * Insert debugging hints into the rendered block contents * - * {@inheritdoc} + * Insert debugging hints into the rendered block contents + * @inheritdoc */ public function render(\Magento\Framework\View\Element\BlockInterface $block, $templateFile, array $dictionary = []) { @@ -60,14 +83,33 @@ public function render(\Magento\Framework\View\Element\BlockInterface $block, $t */ protected function _renderTemplateHints($blockHtml, $templateFile) { - // @codingStandardsIgnoreStart - return <<<HTML -<div class="debugging-hints" style="position: relative; border: 1px dotted red; margin: 6px 2px; padding: 18px 2px 2px 2px;"> -<div class="debugging-hint-template-file" style="position: absolute; top: 0; padding: 2px 5px; font: normal 11px Arial; background: red; left: 0; color: white; white-space: nowrap;" onmouseover="this.style.zIndex = 999;" onmouseout="this.style.zIndex = 'auto';" title="{$templateFile}">{$templateFile}</div> + $hintsId = 'hintsId_' .$this->random->getRandomString(32); + $hintsTemplateFileId = 'hintsTemplateFileId_' .$this->random->getRandomString(32); + + $scriptString = <<<HTML +<div class="debugging-hints" id="{$hintsId}"> +<div class="debugging-hint-template-file" id="{$hintsTemplateFileId}" title="{$templateFile}">{$templateFile}</div> {$blockHtml} </div> HTML; - // @codingStandardsIgnoreEnd + + return $scriptString . + $this->secureRenderer->renderStyleAsTag( + "position: relative; border: 1px dotted red; margin: 6px 2px; padding: 18px 2px 2px 2px;", + '#' . $hintsId + ) . $this->secureRenderer->renderStyleAsTag( + "position: absolute; top: 0; padding: 2px 5px; font: normal 11px Arial; background: red; left: 0;" . + " color: white; white-space: nowrap;", + '#' . $hintsTemplateFileId + ) . $this->secureRenderer->renderEventListenerAsTag( + 'onmouseover', + "this.style.zIndex = 999;", + '#' . $hintsTemplateFileId + ) . $this->secureRenderer->renderEventListenerAsTag( + 'onmouseout', + "this.style.zIndex = 'auto';", + '#' . $hintsTemplateFileId + ); } /** @@ -80,11 +122,25 @@ protected function _renderTemplateHints($blockHtml, $templateFile) protected function _renderBlockHints($blockHtml, \Magento\Framework\View\Element\BlockInterface $block) { $blockClass = get_class($block); - // @codingStandardsIgnoreStart - return <<<HTML -<div class="debugging-hint-block-class" style="position: absolute; top: 0; padding: 2px 5px; font: normal 11px Arial; background: red; right: 0; color: blue; white-space: nowrap;" onmouseover="this.style.zIndex = 999;" onmouseout="this.style.zIndex = 'auto';" title="{$blockClass}">{$blockClass}</div> + $hintsId = 'hintsBlockId_' .$this->random->getRandomString(32); + $scriptString = <<<HTML +<div class="debugging-hint-block-class" id="{$hintsId}" title="{$blockClass}">{$blockClass}</div> {$blockHtml} HTML; - // @codingStandardsIgnoreEnd + + return $scriptString . + $this->secureRenderer->renderStyleAsTag( + "position: absolute; top: 0; padding: 2px 5px; font: normal 11px Arial; background: red; right: 0;" . + " color: blue; white-space: nowrap;", + '#' . $hintsId + ) . $this->secureRenderer->renderEventListenerAsTag( + 'onmouseover', + "this.style.zIndex = 999;", + '#' . $hintsId + ) . $this->secureRenderer->renderEventListenerAsTag( + 'onmouseout', + "this.style.zIndex = 'auto';", + '#' . $hintsId + ); } } 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/Developer/Test/Unit/Console/Command/XmlCatalogGenerateCommandTest.php b/app/code/Magento/Developer/Test/Unit/Console/Command/XmlCatalogGenerateCommandTest.php index 919ee0e060468..152bdfef376fb 100644 --- a/app/code/Magento/Developer/Test/Unit/Console/Command/XmlCatalogGenerateCommandTest.php +++ b/app/code/Magento/Developer/Test/Unit/Console/Command/XmlCatalogGenerateCommandTest.php @@ -46,7 +46,7 @@ public function testExecuteBadType() ->with( $this->equalTo(['urn:magento:framework:Module/etc/module.xsd' => $fixtureXmlFile]), $this->equalTo('test') - )->willReturn(null); + ); $formats = ['phpstorm' => $phpstormFormatMock]; $readFactory = $this->createMock(ReadFactory::class); @@ -97,7 +97,7 @@ public function testExecuteVsCodeFormat() ->with( $this->equalTo(['urn:magento:framework:Module/etc/module.xsd' => $fixtureXmlFile]), $this->equalTo('test') - )->willReturn(null); + ); $formats = ['vscode' => $vscodeFormatMock]; $readFactory = $this->createMock(ReadFactory::class); diff --git a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php index 8bb0b1f176313..5e824e43764de 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php @@ -44,7 +44,6 @@ protected function setUp(): void $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); $this->model = new Syslog( - $this->scopeConfigMock, $this->deploymentConfigMock, 'Magento' ); diff --git a/app/code/Magento/Developer/Test/Unit/Model/TemplateEngine/Decorator/DebugHintsTest.php b/app/code/Magento/Developer/Test/Unit/Model/TemplateEngine/Decorator/DebugHintsTest.php index c1ba20191d0c8..e4e60f95146df 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/TemplateEngine/Decorator/DebugHintsTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/TemplateEngine/Decorator/DebugHintsTest.php @@ -1,14 +1,20 @@ -<?php declare(strict_types=1); +<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Developer\Test\Unit\Model\TemplateEngine\Decorator; use Magento\Developer\Model\TemplateEngine\Decorator\DebugHints; use Magento\Framework\View\Element\BlockInterface; use Magento\Framework\View\TemplateEngineInterface; use PHPUnit\Framework\TestCase; +use Magento\Framework\DataObject; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class DebugHintsTest extends TestCase { @@ -33,7 +39,30 @@ public function testRender($showBlockHints) )->willReturn( '<div id="fixture"/>' ); - $model = new DebugHints($subject, $showBlockHints); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('random'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); + $model = new DebugHints($subject, $showBlockHints, $secureRendererMock, $randomMock); $actualResult = $model->render($block, 'template.phtml', ['var' => 'val']); $this->assertNotNull($actualResult); } diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 6587b462099be..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'), ]; @@ -1084,7 +1085,7 @@ function () use ($deferredResponses, $responseBodies) { * * @param string $request * @return string - * @deprecated Use asynchronous client. + * @deprecated 100.3.3 Use asynchronous client. * @see _getQuotes() */ protected function _getQuotesFromServer($request) @@ -1396,7 +1397,7 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) * * @param \Magento\Framework\DataObject $request * @return $this|\Magento\Framework\DataObject|boolean - * @deprecated + * @deprecated 100.2.3 */ public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) { @@ -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/Dhl/view/adminhtml/templates/unitofmeasure.phtml b/app/code/Magento/Dhl/view/adminhtml/templates/unitofmeasure.phtml index 36ff8138f3955..2d9c8c91e1b76 100644 --- a/app/code/Magento/Dhl/view/adminhtml/templates/unitofmeasure.phtml +++ b/app/code/Magento/Dhl/view/adminhtml/templates/unitofmeasure.phtml @@ -5,24 +5,26 @@ */ /** * @var \Magento\Dhl\Block\Adminhtml\Unitofmeasure $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> - +script; +?> +renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Directory/Block/Data.php b/app/code/Magento/Directory/Block/Data.php index 66c962d6656a6..0424b824e4454 100644 --- a/app/code/Magento/Directory/Block/Data.php +++ b/app/code/Magento/Directory/Block/Data.php @@ -142,7 +142,7 @@ public function getCountryHtmlSelect($defValue = null, $name = 'country_id', $id )->setId( $id )->setTitle( - __($title) + $this->escapeHtmlAttr(__($title)) )->setValue( $defValue )->setOptions( @@ -175,7 +175,7 @@ public function getRegionCollection() * Returns region html select * * @return string - * @deprecated + * @deprecated 100.3.3 * @see getRegionSelect() method for more configurations */ public function getRegionHtmlSelect() diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php index 5db880c00343a..aec2da291f1b3 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php @@ -165,7 +165,7 @@ public function saveRates($rates) * @param string $path * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @deprecated because doesn't take into consideration scopes and system config values. + * @deprecated 100.2.3 because doesn't take into consideration scopes and system config values. * @see \Magento\Directory\Model\CurrencyConfig::getConfigCurrencies() */ public function getConfigCurrencies($model, $path) diff --git a/app/code/Magento/Directory/Test/Unit/Block/DataTest.php b/app/code/Magento/Directory/Test/Unit/Block/DataTest.php index bf71419744e7c..af64d7119f077 100644 --- a/app/code/Magento/Directory/Test/Unit/Block/DataTest.php +++ b/app/code/Magento/Directory/Test/Unit/Block/DataTest.php @@ -21,6 +21,7 @@ use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\Escaper; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -62,9 +63,16 @@ class DataTest extends TestCase /** @var SerializerInterface|MockObject */ private $serializerMock; + /** @var \Magento\Framework\Escaper */ + private $escaper; + protected function setUp(): void { $objectManagerHelper = new ObjectManager($this); + $this->escaper = $this->getMockBuilder(Escaper::class) + ->disableOriginalConstructor() + ->setMethods(['escapeHtmlAttr']) + ->getMock(); $this->prepareContext(); $this->helperDataMock = $this->getMockBuilder(\Magento\Directory\Helper\Data::class) @@ -129,6 +137,10 @@ protected function prepareContext() $this->contextMock->expects($this->any()) ->method('getLayout') ->willReturn($this->layoutMock); + + $this->contextMock->expects($this->once()) + ->method('getEscaper') + ->willReturn($this->escaper); } protected function prepareCountryCollection() @@ -142,9 +154,11 @@ protected function prepareCountryCollection() \Magento\Directory\Model\ResourceModel\Country\CollectionFactory::class ) ->disableOriginalConstructor() - ->setMethods([ - 'create' - ]) + ->setMethods( + [ + 'create' + ] + ) ->getMock(); $this->countryCollectionFactoryMock->expects($this->any()) @@ -292,15 +306,17 @@ protected function mockElementHtmlSelect($defaultCountry, $options, $resultHtml) $elementHtmlSelect = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() - ->setMethods([ - 'setName', - 'setId', - 'setTitle', - 'setValue', - 'setOptions', - 'setExtraParams', - 'getHtml', - ]) + ->setMethods( + [ + 'setName', + 'setId', + 'setTitle', + 'setValue', + 'setOptions', + 'setExtraParams', + 'getHtml', + ] + ) ->getMock(); $elementHtmlSelect->expects($this->once()) @@ -330,6 +346,10 @@ protected function mockElementHtmlSelect($defaultCountry, $options, $resultHtml) $elementHtmlSelect->expects($this->once()) ->method('getHtml') ->willReturn($resultHtml); + $this->escaper->expects($this->once()) + ->method('escapeHtmlAttr') + ->with(__($title)) + ->willReturn(__($title)); return $elementHtmlSelect; } diff --git a/app/code/Magento/Directory/view/adminhtml/templates/js/optional_zip_countries.phtml b/app/code/Magento/Directory/view/adminhtml/templates/js/optional_zip_countries.phtml index 524c86fbaf604..4037dc19bde33 100644 --- a/app/code/Magento/Directory/view/adminhtml/templates/js/optional_zip_countries.phtml +++ b/app/code/Magento/Directory/view/adminhtml/templates/js/optional_zip_countries.phtml @@ -10,15 +10,24 @@ * @see \Magento\Backend\Block\Template */ +/** + * @var \Magento\Backend\Block\Template $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> - +script; +?> +renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Downloadable.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Downloadable.php index 973d52e865dc9..b3c906c1bb9ad 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Downloadable.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Downloadable.php @@ -11,7 +11,7 @@ * * @api * @since 100.0.2 - * @deprecated + * @deprecated 100.3.1 * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite */ class Downloadable extends \Magento\Downloadable\Block\Catalog\Product\Links diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php index 8fdf1d395308e..707e9788141c4 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php @@ -15,7 +15,7 @@ * Adminhtml catalog product downloadable items tab and form * * @author Magento Core Team - * @deprecated + * @deprecated 100.3.1 * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite */ class Downloadable extends Widget implements TabInterface diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php index e0026765f269b..5895f3a92c54c 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php @@ -11,7 +11,7 @@ * @author Magento Core Team * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * - * @deprecated in favor of new class which adds grid links + * @deprecated 100.3.1 in favor of new class which adds grid links * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Links */ class Links extends \Magento\Backend\Block\Template diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php index 83a5a93405158..04210d54f38aa 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php @@ -10,7 +10,7 @@ * * @author Magento Core Team * - * @deprecated because of new class which adds grids samples + * @deprecated 100.3.1 because of new class which adds grids samples * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Samples */ class Samples extends \Magento\Backend\Block\Widget diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Sales/Items/Column/Downloadable/Name.php b/app/code/Magento/Downloadable/Block/Adminhtml/Sales/Items/Column/Downloadable/Name.php index fced70593704c..40599efef4f05 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Sales/Items/Column/Downloadable/Name.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Sales/Items/Column/Downloadable/Name.php @@ -8,7 +8,9 @@ use Magento\Downloadable\Model\Link; use Magento\Downloadable\Model\Link\Purchased; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\ScopeInterface; +use Magento\Catalog\Helper\Data as CatalogHelper; /** * Sales Order downloadable items name column renderer @@ -42,6 +44,7 @@ class Name extends \Magento\Sales\Block\Adminhtml\Items\Column\Name * @param \Magento\Downloadable\Model\Link\PurchasedFactory $purchasedFactory * @param \Magento\Downloadable\Model\ResourceModel\Link\Purchased\Item\CollectionFactory $itemsFactory * @param array $data + * @param CatalogHelper|null $catalogHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -51,14 +54,18 @@ public function __construct( \Magento\Catalog\Model\Product\OptionFactory $optionFactory, \Magento\Downloadable\Model\Link\PurchasedFactory $purchasedFactory, \Magento\Downloadable\Model\ResourceModel\Link\Purchased\Item\CollectionFactory $itemsFactory, - array $data = [] + array $data = [], + ?CatalogHelper $catalogHelper = null ) { $this->_purchasedFactory = $purchasedFactory; $this->_itemsFactory = $itemsFactory; + $data['catalogHelper'] = $catalogHelper ?? ObjectManager::getInstance()->get(CatalogHelper::class); parent::__construct($context, $stockRegistry, $stockConfiguration, $registry, $optionFactory, $data); } /** + * Return purchased links. + * * @return Purchased */ public function getLinks() @@ -73,6 +80,8 @@ public function getLinks() } /** + * Retunrn links title. + * * @return null|string */ public function getLinksTitle() diff --git a/app/code/Magento/Downloadable/Block/Sales/Order/Email/Items/Downloadable.php b/app/code/Magento/Downloadable/Block/Sales/Order/Email/Items/Downloadable.php index dbf8eacbd7848..5a54a274485fe 100644 --- a/app/code/Magento/Downloadable/Block/Sales/Order/Email/Items/Downloadable.php +++ b/app/code/Magento/Downloadable/Block/Sales/Order/Email/Items/Downloadable.php @@ -12,7 +12,7 @@ use Magento\Store\Model\ScopeInterface; /** - * Downlaodable Sales Order Email items renderer + * Downloadable Sales Order Email items renderer * * @api * @since 100.0.2 @@ -81,6 +81,8 @@ public function getLinks() } /** + * Returns links title + * * @return null|string */ public function getLinksTitle() @@ -92,6 +94,8 @@ public function getLinksTitle() } /** + * Returns purchased link url + * * @param Item $item * @return string */ diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php index fe430566d63ce..dbd236d5e8827 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php @@ -9,7 +9,7 @@ /** * Class Form * - * @deprecated since downloadable information rendering moved to UI components. + * @deprecated 100.3.1 since downloadable information rendering moved to UI components. * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite * @package Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit */ 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 c0bc825a8285b..1be97435fff84 100644 --- a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php +++ b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php @@ -7,8 +7,9 @@ namespace Magento\Downloadable\Controller\Download; -use Magento\Catalog\Model\Product\SalabilityChecker; use Magento\Downloadable\Helper\Download as DownloadHelper; +use Magento\Downloadable\Model\Link as LinkModel; +use Magento\Downloadable\Model\RelatedProductRetriever; use Magento\Framework\App\Action\Context; use Magento\Framework\App\ResponseInterface; @@ -20,20 +21,21 @@ class LinkSample extends \Magento\Downloadable\Controller\Download { /** - * @var SalabilityChecker + * @var RelatedProductRetriever */ - private $salabilityChecker; + private $relatedProductRetriever; /** * @param Context $context - * @param SalabilityChecker|null $salabilityChecker + * @param RelatedProductRetriever $relatedProductRetriever */ public function __construct( Context $context, - SalabilityChecker $salabilityChecker = null + RelatedProductRetriever $relatedProductRetriever ) { parent::__construct($context); - $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class); + + $this->relatedProductRetriever = $relatedProductRetriever; } /** @@ -44,9 +46,10 @@ public function __construct( public function execute() { $linkId = $this->getRequest()->getParam('link_id', 0); - /** @var \Magento\Downloadable\Model\Link $link */ - $link = $this->_objectManager->create(\Magento\Downloadable\Model\Link::class)->load($linkId); - if ($link->getId() && $this->salabilityChecker->isSalable($link->getProductId())) { + /** @var LinkModel $link */ + $link = $this->_objectManager->create(LinkModel::class); + $link->load($linkId); + if ($link->getId() && $this->isProductSalable($link)) { $resource = ''; $resourceType = ''; if ($link->getSampleType() == DownloadHelper::LINK_TYPE_URL) { @@ -66,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.') ); } @@ -74,4 +77,16 @@ public function execute() return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); } + + /** + * Check is related product salable. + * + * @param LinkModel $link + * @return bool + */ + private function isProductSalable(LinkModel $link): bool + { + $product = $this->relatedProductRetriever->getProduct((int) $link->getProductId()); + return $product ? $product->isSalable() : false; + } } diff --git a/app/code/Magento/Downloadable/Controller/Download/Sample.php b/app/code/Magento/Downloadable/Controller/Download/Sample.php index b95ec510fdd9b..839083b320878 100644 --- a/app/code/Magento/Downloadable/Controller/Download/Sample.php +++ b/app/code/Magento/Downloadable/Controller/Download/Sample.php @@ -3,13 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Downloadable\Controller\Download; -use Magento\Catalog\Model\Product\SalabilityChecker; +use Magento\Downloadable\Controller\Download; use Magento\Downloadable\Helper\Download as DownloadHelper; +use Magento\Downloadable\Helper\File; +use Magento\Downloadable\Model\RelatedProductRetriever; +use Magento\Downloadable\Model\Sample as SampleModel; +use Magento\Downloadable\Model\SampleFactory; use Magento\Framework\App\Action\Context; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResponseInterface; /** @@ -17,23 +25,49 @@ * * @SuppressWarnings(PHPMD.AllPurposeAction) */ -class Sample extends \Magento\Downloadable\Controller\Download +class Sample extends Download { /** - * @var SalabilityChecker + * @var RelatedProductRetriever + */ + private $relatedProductRetriever; + + /** + * @var File */ - private $salabilityChecker; + private $file; + + /** + * @var SampleFactory + */ + private $sampleFactory; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; /** * @param Context $context - * @param SalabilityChecker|null $salabilityChecker + * @param RelatedProductRetriever $relatedProductRetriever + * @param File|null $file + * @param SampleFactory|null $sampleFactory + * @param StockConfigurationInterface|null $stockConfiguration */ public function __construct( Context $context, - SalabilityChecker $salabilityChecker = null + RelatedProductRetriever $relatedProductRetriever, + ?File $file = null, + ?SampleFactory $sampleFactory = null, + ?StockConfigurationInterface $stockConfiguration = null ) { parent::__construct($context); - $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class); + + $this->relatedProductRetriever = $relatedProductRetriever; + $this->file = $file ?: ObjectManager::getInstance()->get(File::class); + $this->sampleFactory = $sampleFactory ?: ObjectManager::getInstance()->get(SampleFactory::class); + $this->stockConfiguration = $stockConfiguration + ?: ObjectManager::getInstance()->get(StockConfigurationInterface::class); } /** @@ -44,31 +78,61 @@ public function __construct( public function execute() { $sampleId = $this->getRequest()->getParam('sample_id', 0); - /** @var \Magento\Downloadable\Model\Sample $sample */ - $sample = $this->_objectManager->create(\Magento\Downloadable\Model\Sample::class)->load($sampleId); - if ($sample->getId() && $this->salabilityChecker->isSalable($sample->getProductId())) { - $resource = ''; - $resourceType = ''; - if ($sample->getSampleType() == DownloadHelper::LINK_TYPE_URL) { - $resource = $sample->getSampleUrl(); - $resourceType = DownloadHelper::LINK_TYPE_URL; - } elseif ($sample->getSampleType() == DownloadHelper::LINK_TYPE_FILE) { - /** @var \Magento\Downloadable\Helper\File $helper */ - $helper = $this->_objectManager->get(\Magento\Downloadable\Helper\File::class); - $resource = $helper->getFilePath($sample->getBasePath(), $sample->getSampleFile()); - $resourceType = DownloadHelper::LINK_TYPE_FILE; - } - try { - $this->_processDownload($resource, $resourceType); - // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage - exit(0); - } catch (\Exception $e) { - $this->messageManager->addError( - __('Sorry, there was an error getting requested content. Please contact the store owner.') - ); - } + /** @var SampleModel $sample */ + $sample = $this->sampleFactory->create(); + $sample->load($sampleId); + if ($this->isCanDownload($sample)) { + $this->download($sample); } return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); } + + /** + * Is sample can be downloaded + * + * @param SampleModel $sample + * @return bool + */ + private function isCanDownload(SampleModel $sample): bool + { + $product = $this->relatedProductRetriever->getProduct((int) $sample->getProductId()); + if ($product && $sample->getId()) { + $isProductEnabled = (int) $product->getStatus() === Status::STATUS_ENABLED; + + return $product->isSalable() || $this->stockConfiguration->isShowOutOfStock() && $isProductEnabled; + } + + return false; + } + + /** + * Download process + * + * @param SampleModel $sample + * @return void + */ + private function download(SampleModel $sample): void + { + $resource = ''; + $resourceType = ''; + + if ($sample->getSampleType() === DownloadHelper::LINK_TYPE_URL) { + $resource = $sample->getSampleUrl(); + $resourceType = DownloadHelper::LINK_TYPE_URL; + } elseif ($sample->getSampleType() === DownloadHelper::LINK_TYPE_FILE) { + $resource = $this->file->getFilePath($sample->getBasePath(), $sample->getSampleFile()); + $resourceType = DownloadHelper::LINK_TYPE_FILE; + } + + try { + $this->_processDownload($resource, $resourceType); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage + exit(0); + } catch (\Exception $e) { + $this->messageManager->addErrorMessage( + __('Sorry, there was an error getting requested content. Please contact the store owner.') + ); + } + } } diff --git a/app/code/Magento/Downloadable/Model/Link/UpdateHandler.php b/app/code/Magento/Downloadable/Model/Link/UpdateHandler.php index 8e351b3dfb0a5..21bc0a121f5e2 100644 --- a/app/code/Magento/Downloadable/Model/Link/UpdateHandler.php +++ b/app/code/Magento/Downloadable/Model/Link/UpdateHandler.php @@ -3,17 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Downloadable\Model\Link; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Downloadable\Api\LinkRepositoryInterface as LinkRepository; use Magento\Downloadable\Model\Product\Type; use Magento\Framework\EntityManager\Operation\ExtensionInterface; /** - * Class UpdateHandler + * UpdateHandler for downloadable product links */ class UpdateHandler implements ExtensionInterface { + private const GLOBAL_SCOPE_ID = 0; + /** * @var LinkRepository */ @@ -28,35 +34,48 @@ public function __construct(LinkRepository $linkRepository) } /** - * @param object $entity + * Update links for downloadable product if exist + * + * @param ProductInterface $entity * @param array $arguments - * @return \Magento\Catalog\Api\Data\ProductInterface|object + * @return ProductInterface * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($entity, $arguments = []) + public function execute($entity, $arguments = []): ProductInterface { - /** @var $entity \Magento\Catalog\Api\Data\ProductInterface */ - if ($entity->getTypeId() != Type::TYPE_DOWNLOADABLE) { - return $entity; + $links = $entity->getExtensionAttributes()->getDownloadableProductLinks(); + + if ($links && $entity->getTypeId() === Type::TYPE_DOWNLOADABLE) { + $this->updateLinks($entity, $links); } - /** @var \Magento\Downloadable\Api\Data\LinkInterface[] $links */ - $links = $entity->getExtensionAttributes()->getDownloadableProductLinks() ?: []; - $updatedLinks = []; + return $entity; + } + + /** + * Update product links + * + * @param ProductInterface $entity + * @param array $links + * @return void + */ + private function updateLinks(ProductInterface $entity, array $links): void + { + $isGlobalScope = (int) $entity->getStoreId() === self::GLOBAL_SCOPE_ID; $oldLinks = $this->linkRepository->getList($entity->getSku()); + + $updatedLinks = []; foreach ($links as $link) { if ($link->getId()) { $updatedLinks[$link->getId()] = true; } - $this->linkRepository->save($entity->getSku(), $link, !(bool)$entity->getStoreId()); + $this->linkRepository->save($entity->getSku(), $link, $isGlobalScope); } - /** @var \Magento\Catalog\Api\Data\ProductInterface $entity */ + foreach ($oldLinks as $link) { if (!isset($updatedLinks[$link->getId()])) { $this->linkRepository->delete($link->getId()); } } - - return $entity; } } diff --git a/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php new file mode 100644 index 0000000000000..f701f96b910e7 --- /dev/null +++ b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php @@ -0,0 +1,68 @@ +productRepository = $productRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->metadataPool = $metadataPool; + } + + /** + * Get related product. + * + * @param int $productId + * @return ProductInterface|null + */ + public function getProduct(int $productId): ?ProductInterface + { + $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $searchCriteria = $this->searchCriteriaBuilder->addFilter($productMetadata->getLinkField(), $productId) + ->create(); + $items = $this->productRepository->getList($searchCriteria) + ->getItems(); + $product = $items ? array_shift($items) : null; + + return $product; + } +} diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php index 90b458ff6348e..e50213f139ba1 100644 --- a/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php @@ -100,10 +100,7 @@ public function __construct( } /** - * {@inheritdoc} - * @param array $dimensions - * @param \Traversable $entityIds - * @throws \Exception + * @inheritDoc */ public function executeByDimensions(array $dimensions, \Traversable $entityIds) { @@ -205,8 +202,7 @@ private function fillTemporaryTable(string $temporaryDownloadableTableName, arra 'max_price' => new \Zend_Db_Expr('SUM(' . $ifPrice . ')'), ] ); - $query = $select->insertFromSelect($temporaryDownloadableTableName); - $this->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $temporaryDownloadableTableName, []); } /** @@ -259,14 +255,13 @@ private function fillFinalPrice( IndexTableStructure $temporaryPriceTable ) { $select = $this->baseFinalPrice->getQuery($dimensions, Type::TYPE_DOWNLOADABLE, iterator_to_array($entityIds)); - $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false); - $this->tableMaintainer->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []); } /** * Get connection * - * return \Magento\Framework\DB\Adapter\AdapterInterface + * @return \Magento\Framework\DB\Adapter\AdapterInterface * @throws \DomainException */ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php index 8d30322745b8d..b7b079d208d97 100644 --- a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php +++ b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php @@ -24,7 +24,7 @@ class Sample extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool - * @param null $connectionName + * @param string|null $connectionName */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -126,7 +126,7 @@ public function getSearchableData($productId, $storeId) )->join( ['cpe' => $this->getTable('catalog_product_entity')], sprintf( - 'cpe.entity_id = m.product_id', + 'cpe.%s = m.product_id', $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() ), [] diff --git a/app/code/Magento/Downloadable/Model/Sample/Builder.php b/app/code/Magento/Downloadable/Model/Sample/Builder.php index 368d190319766..163e641935db0 100644 --- a/app/code/Magento/Downloadable/Model/Sample/Builder.php +++ b/app/code/Magento/Downloadable/Model/Sample/Builder.php @@ -76,7 +76,8 @@ public function __construct( * Init data for builder * * @param array $data - * @return $this; + * @return $this + * @since 100.1.0 * @since 100.1.0 */ public function setData(array $data) diff --git a/app/code/Magento/Downloadable/Model/Sample/UpdateHandler.php b/app/code/Magento/Downloadable/Model/Sample/UpdateHandler.php index 80294032aea1b..cb7ff725a21d3 100644 --- a/app/code/Magento/Downloadable/Model/Sample/UpdateHandler.php +++ b/app/code/Magento/Downloadable/Model/Sample/UpdateHandler.php @@ -3,17 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Downloadable\Model\Sample; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Downloadable\Api\SampleRepositoryInterface as SampleRepository; use Magento\Downloadable\Model\Product\Type; use Magento\Framework\EntityManager\Operation\ExtensionInterface; /** - * Class UpdateHandler + * UpdateHandler for downloadable product samples */ class UpdateHandler implements ExtensionInterface { + private const GLOBAL_SCOPE_ID = 0; + /** * @var SampleRepository */ @@ -28,35 +34,48 @@ public function __construct(SampleRepository $sampleRepository) } /** - * @param object $entity + * Update samples for downloadable product if exist + * + * @param ProductInterface $entity * @param array $arguments - * @return \Magento\Catalog\Api\Data\ProductInterface|object + * @return ProductInterface * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($entity, $arguments = []) + public function execute($entity, $arguments = []): ProductInterface { - /** @var $entity \Magento\Catalog\Api\Data\ProductInterface */ - if ($entity->getTypeId() != Type::TYPE_DOWNLOADABLE) { - return $entity; + $samples = $entity->getExtensionAttributes()->getDownloadableProductSamples(); + + if ($samples && $entity->getTypeId() === Type::TYPE_DOWNLOADABLE) { + $this->updateSamples($entity, $samples); } - /** @var \Magento\Downloadable\Api\Data\SampleInterface[] $samples */ - $samples = $entity->getExtensionAttributes()->getDownloadableProductSamples() ?: []; - $updatedSamples = []; + return $entity; + } + + /** + * Update product samples + * + * @param ProductInterface $entity + * @param array $samples + * @return void + */ + private function updateSamples(ProductInterface $entity, array $samples): void + { + $isGlobalScope = (int) $entity->getStoreId() === self::GLOBAL_SCOPE_ID; $oldSamples = $this->sampleRepository->getList($entity->getSku()); + + $updatedSamples = []; foreach ($samples as $sample) { if ($sample->getId()) { $updatedSamples[$sample->getId()] = true; } - $this->sampleRepository->save($entity->getSku(), $sample, !(bool)$entity->getStoreId()); + $this->sampleRepository->save($entity->getSku(), $sample, $isGlobalScope); } - /** @var \Magento\Catalog\Api\Data\ProductInterface $entity */ + foreach ($oldSamples as $sample) { if (!isset($updatedSamples[$sample->getId()])) { $this->sampleRepository->delete($sample->getId()); } } - - return $entity; } } diff --git a/app/code/Magento/Downloadable/Observer/IsAllowedGuestCheckoutObserver.php b/app/code/Magento/Downloadable/Observer/IsAllowedGuestCheckoutObserver.php index 7f77c8b5f10bf..ea8df05e6a79a 100644 --- a/app/code/Magento/Downloadable/Observer/IsAllowedGuestCheckoutObserver.php +++ b/app/code/Magento/Downloadable/Observer/IsAllowedGuestCheckoutObserver.php @@ -3,65 +3,140 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Downloadable\Observer; +use Magento\Downloadable\Model\Link; +use Magento\Downloadable\Model\Product\Type; +use Magento\Downloadable\Model\ResourceModel\Link\CollectionFactory as LinkCollectionFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Quote\Model\Quote; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +/** + * Checks if guest checkout is allowed then quote contains downloadable products. + */ class IsAllowedGuestCheckoutObserver implements ObserverInterface { + private const XML_PATH_DISABLE_GUEST_CHECKOUT = 'catalog/downloadable/disable_guest_checkout'; + + private const XML_PATH_DOWNLOADABLE_SHAREABLE = 'catalog/downloadable/shareable'; + /** - * Xml path to disable checkout + * @var ScopeConfigInterface */ - const XML_PATH_DISABLE_GUEST_CHECKOUT = 'catalog/downloadable/disable_guest_checkout'; + private $scopeConfig; /** - * Core store config + * Downloadable link collection factory * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var LinkCollectionFactory */ - protected $_scopeConfig; + private $linkCollectionFactory; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param LinkCollectionFactory $linkCollectionFactory + * @param StoreManagerInterface $storeManager */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + ScopeConfigInterface $scopeConfig, + LinkCollectionFactory $linkCollectionFactory, + StoreManagerInterface $storeManager ) { - $this->_scopeConfig = $scopeConfig; + $this->scopeConfig = $scopeConfig; + $this->linkCollectionFactory = $linkCollectionFactory; + $this->storeManager = $storeManager; } /** * Check is allowed guest checkout if quote contain downloadable product(s) * - * @param \Magento\Framework\Event\Observer $observer + * @param Observer $observer * @return $this */ - public function execute(\Magento\Framework\Event\Observer $observer) + public function execute(Observer $observer) { - $store = $observer->getEvent()->getStore(); + $storeId = (int)$this->storeManager->getStore($observer->getEvent()->getStore())->getId(); $result = $observer->getEvent()->getResult(); - if (!$this->_scopeConfig->isSetFlag( + /* @var $quote Quote */ + $quote = $observer->getEvent()->getQuote(); + $isGuestCheckoutDisabled = $this->scopeConfig->isSetFlag( self::XML_PATH_DISABLE_GUEST_CHECKOUT, ScopeInterface::SCOPE_STORE, - $store - )) { - return $this; - } - - /* @var $quote \Magento\Quote\Model\Quote */ - $quote = $observer->getEvent()->getQuote(); + $storeId + ); foreach ($quote->getAllItems() as $item) { - if (($product = $item->getProduct()) - && $product->getTypeId() == \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE - ) { - $result->setIsAllowed(false); - break; + $product = $item->getProduct(); + + if ((string)$product->getTypeId() === Type::TYPE_DOWNLOADABLE) { + if ($isGuestCheckoutDisabled || !$this->checkForShareableLinks($item, $storeId)) { + $result->setIsAllowed(false); + break; + } } } return $this; } + + /** + * Check for shareable link + * + * @param CartItemInterface $item + * @param int $storeId + * @return boolean + */ + private function checkForShareableLinks(CartItemInterface $item, int $storeId): bool + { + $isSharable = true; + $option = $item->getOptionByCode('downloadable_link_ids'); + + if (!empty($option)) { + $downloadableLinkIds = explode(',', $option->getValue()); + + $linkCollection = $this->linkCollectionFactory->create(); + $linkCollection->addFieldToFilter('link_id', ['in' => $downloadableLinkIds]); + $linkCollection->addFieldToFilter('is_shareable', ['in' => $this->getNotSharableValues($storeId)]); + + // We don't have not sharable links + $isSharable = $linkCollection->getSize() === 0; + } + + return $isSharable; + } + + /** + * Returns not sharable values depending on configuration + * + * @param int $storeId + * @return array + */ + private function getNotSharableValues(int $storeId): array + { + $configIsSharable = $this->scopeConfig->isSetFlag( + self::XML_PATH_DOWNLOADABLE_SHAREABLE, + ScopeInterface::SCOPE_STORE, + $storeId + ); + + $notShareableValues = [Link::LINK_SHAREABLE_NO]; + + if (!$configIsSharable) { + $notShareableValues[] = Link::LINK_SHAREABLE_CONFIG; + } + + return $notShareableValues; + } } diff --git a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php index 4f7939da478fa..9351568c5a757 100644 --- a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php +++ b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Downloadable\Observer; use Magento\Framework\Event\ObserverInterface; @@ -81,12 +83,14 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + /** @var \Magento\Sales\Model\Order\Item $orderItem */ $orderItem = $observer->getEvent()->getItem(); if (!$orderItem->getId()) { //order not saved in the database return $this; } - if ($orderItem->getProductType() != \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) { + $productType = $orderItem->getRealProductType() ?: $orderItem->getProductType(); + if ($productType !== \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) { return $this; } $product = $orderItem->getProduct(); @@ -112,13 +116,13 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($linkIds = $orderItem->getProductOptionByCode('links')) { $linkPurchased = $this->_createPurchasedModel(); $this->_objectCopyService->copyFieldsetToTarget( - \downloadable_sales_copy_order::class, + 'downloadable_sales_copy_order', 'to_downloadable', $orderItem->getOrder(), $linkPurchased ); $this->_objectCopyService->copyFieldsetToTarget( - \downloadable_sales_copy_order_item::class, + 'downloadable_sales_copy_order_item', 'to_downloadable', $orderItem, $linkPurchased @@ -131,14 +135,12 @@ public function execute(\Magento\Framework\Event\Observer $observer) ScopeInterface::SCOPE_STORE ); $linkPurchased->setLinkSectionTitle($linkSectionTitle)->save(); - $linkStatus = \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_PENDING; if ($orderStatusToEnableItem == \Magento\Sales\Model\Order\Item::STATUS_PENDING || $orderItem->getOrder()->getState() == \Magento\Sales\Model\Order::STATE_COMPLETE ) { $linkStatus = \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_AVAILABLE; } - foreach ($linkIds as $linkId) { if (isset($links[$linkId])) { $linkPurchasedItem = $this->_createPurchasedItemModel()->setPurchasedId( @@ -148,7 +150,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) ); $this->_objectCopyService->copyFieldsetToTarget( - \downloadable_sales_copy_link::class, + 'downloadable_sales_copy_link', 'to_purchased', $links[$linkId], $linkPurchasedItem 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/Data/CatalogConfigData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/CatalogConfigData.xml index 8bb81f9c7579d..d8fb9a0497332 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/CatalogConfigData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/CatalogConfigData.xml @@ -18,4 +18,9 @@ 0 1 + + catalog/downloadable/shareable + 0 + 1 + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml index eb3ad674a0fdf..4c0382e0d444d 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml @@ -31,6 +31,17 @@ magento-logo.png https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg + + link-1 + 2.43 + 2 + url + http://example.com + url + http://example.com + 1 + 1 + link-1 2.43 diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml index 2986532ef1138..9dca730dfd5c5 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml @@ -105,4 +105,15 @@ downloadableLink1 downloadableLink2 + + downloadableproduct + downloadable + 4 + DownloadableProduct + 99.99 + 50 + 1 + 0 + downloadableproduct + 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/AdminAddDefaultImageDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml index c634a8426eac0..44cc15272ff64 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml @@ -27,12 +27,11 @@
- + - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml index 2f43c6f8278cc..e53c05cfb92cf 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml @@ -48,8 +48,7 @@ - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml similarity index 83% rename from app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml rename to app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml index d3933ae4fae7d..bfa0c77280f42 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml @@ -26,6 +26,10 @@ - + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml index f1ea344d4e45c..7685017adc426 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml @@ -42,8 +42,7 @@ - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml index 850a73cd354a5..e711add69b8c8 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml @@ -24,8 +24,12 @@ - - + + + + + + @@ -44,8 +48,7 @@ - - + @@ -76,17 +79,19 @@ - - + + + + + + - - + - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml index ba2e5e89005cf..ea4c58a17fdd6 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml @@ -42,8 +42,7 @@ - - + @@ -82,13 +81,11 @@ - - + - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml index 9ad20385519d1..34b9701f2dca5 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml @@ -37,7 +37,7 @@ - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml index 317f2abdf2f23..e7e00d2fb81ef 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml @@ -42,8 +42,7 @@ - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml index 0ff7c9bab26ca..1c573ddef79a7 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml @@ -42,8 +42,7 @@ - - + @@ -89,13 +88,11 @@ - - + - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml index 5615c66762c52..779864aafea55 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml @@ -42,8 +42,7 @@ - - + @@ -81,13 +80,11 @@ - - + - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml index f1d00d83b6666..48a7283cececd 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml @@ -42,8 +42,7 @@ - - + @@ -81,13 +80,11 @@ - - + - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml index fb0532d9d1fbe..5c5e59bd99689 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml @@ -42,8 +42,7 @@ - - + @@ -78,13 +77,11 @@ - - + - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml index 50a2215d441ad..71da70ec823d9 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml @@ -42,8 +42,7 @@ - - + @@ -79,13 +78,11 @@ - - + - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml index 7062b15aeedbf..e04b53ff208af 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml @@ -44,10 +44,9 @@ - - - - + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToDownloadableProductTest.xml index aa94de681de1d..0237eca61b784 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToDownloadableProductTest.xml @@ -49,12 +49,20 @@ - + - - + + + + + + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml index 27d3d3d10a0b7..a2b418e510482 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml @@ -24,12 +24,11 @@ - + - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml index 0ac2dc9b04825..6e981794a9b9c 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml @@ -28,8 +28,7 @@ - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml index b9773415059ec..37db99fc2f55b 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml @@ -20,11 +20,12 @@ + - + @@ -35,7 +36,7 @@ - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml index 4a7f1dde227da..45c8cc71486f3 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml @@ -28,8 +28,7 @@ - - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml index 55c673146021d..0caa23f0f2a7f 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml @@ -31,8 +31,7 @@ - - + @@ -49,7 +48,7 @@ - + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml index 0ed826e944a4f..64449b9436e11 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml @@ -28,8 +28,7 @@ - - + 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/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml index d7e0ce3b2ca22..5f89db581a7c1 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml @@ -28,11 +28,15 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyOutOfStockDownloadableProductSamplesAreAccessibleTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyOutOfStockDownloadableProductSamplesAreAccessibleTest.xml new file mode 100644 index 0000000000000..337d4c7dd38b5 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyOutOfStockDownloadableProductSamplesAreAccessibleTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="VerifyOutOfStockDownloadableProductSamplesAreAccessibleTest"> + <annotations> + <features value="Downloadable"/> + <stories value="Downloadable product"/> + <title value="Samples of Downloadable Products are accessible, if product is out of stock"/> + <description value="Samples of Downloadable Products are accessible, if product is out of stock"/> + <severity value="MAJOR"/> + <testCaseId value="MC-35639"/> + <group value="downloadable"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Enable show out of stock product --> + <magentoCLI stepKey="enableShowOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 1"/> + + <!-- Add downloadable domains --> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + + <!-- Create category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create downloadable product --> + <createData entity="DownloadableProductWithoutLinksOutOfStock" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Add downloadable link --> + <createData entity="downloadableLink1" stepKey="addDownloadableLink"> + <requiredEntity createDataKey="createProduct"/> + </createData> + + <!-- Add downloadable sample --> + <createData entity="DownloadableSample" stepKey="addDownloadableSample"> + <requiredEntity createDataKey="createProduct"/> + </createData> + </before> + <after> + <!-- Disable show out of stock product --> + <magentoCLI stepKey="enableShowOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 0"/> + + <!-- Remove downloadable domains --> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + + <!-- Delete product --> + <deleteData createDataKey="createProduct" stepKey="deleteDownloadableProduct"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Admin logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Open Downloadable product from precondition on Storefront --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openStorefrontProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Sample url is accessible --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableSample"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableSampleLabel(DownloadableSample.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableSampleLabel(DownloadableSample.title)}}" stepKey="clickDownloadableSample"/> + <switchToNextTab stepKey="switchToSampleTab"/> + <wait time="2" stepKey="waitToMakeSureThereWillBeNoRedirectToHomePage"/> + <seeInCurrentUrl url="downloadable/download/sample/sample_id/" stepKey="amOnSampleDownloadPage"/> + <closeTab stepKey="closeSampleTab"/> + </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/LinkSampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php deleted file mode 100644 index 725c06004f117..0000000000000 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php +++ /dev/null @@ -1,237 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Downloadable\Test\Unit\Controller\Download; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\SalabilityChecker; -use Magento\Downloadable\Controller\Download\LinkSample; -use Magento\Downloadable\Helper\Data; -use Magento\Downloadable\Helper\Download; -use Magento\Downloadable\Helper\File; -use Magento\Downloadable\Model\Link; -use Magento\Framework\App\Request\Http; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\App\Response\RedirectInterface; -use Magento\Framework\App\ResponseInterface; -use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Framework\UrlInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Unit tests for \Magento\Downloadable\Controller\Download\LinkSample. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class LinkSampleTest extends TestCase -{ - /** @var LinkSample */ - protected $linkSample; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** - * @var MockObject|Http - */ - protected $request; - - /** - * @var MockObject|ResponseInterface - */ - protected $response; - - /** - * @var MockObject|\Magento\Framework\ObjectManager\ObjectManager - */ - protected $objectManager; - - /** - * @var MockObject|ManagerInterface - */ - protected $messageManager; - - /** - * @var MockObject|RedirectInterface - */ - protected $redirect; - - /** - * @var MockObject|Data - */ - protected $helperData; - - /** - * @var MockObject|\Magento\Downloadable\Helper\Download - */ - protected $downloadHelper; - - /** - * @var MockObject|Product - */ - protected $product; - - /** - * @var MockObject|UrlInterface - */ - protected $urlInterface; - - /** - * @var SalabilityChecker|MockObject - */ - private $salabilityCheckerMock; - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function setUp(): void - { - $this->objectManagerHelper = new ObjectManagerHelper($this); - - $this->request = $this->getMockForAbstractClass(RequestInterface::class); - $this->response = $this->getMockBuilder(ResponseInterface::class) - ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect']) - ->onlyMethods(['sendResponse']) - ->getMockForAbstractClass(); - - $this->helperData = $this->createPartialMock( - Data::class, - ['getIsShareable'] - ); - $this->downloadHelper = $this->createPartialMock( - Download::class, - [ - 'setResource', - 'getFilename', - 'getContentType', - 'getFileSize', - 'getContentDisposition', - 'output' - ] - ); - $this->product = $this->getMockBuilder(Product::class) - ->addMethods(['_wakeup']) - ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class); - $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class); - $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class); - $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class); - $this->objectManager = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create', 'get'] - ); - $this->linkSample = $this->objectManagerHelper->getObject( - LinkSample::class, - [ - 'objectManager' => $this->objectManager, - 'request' => $this->request, - 'response' => $this->response, - 'messageManager' => $this->messageManager, - 'redirect' => $this->redirect, - 'salabilityChecker' => $this->salabilityCheckerMock, - ] - ); - } - - /** - * Execute Download link's sample action with Url link. - * - * @return void - */ - public function testExecuteLinkTypeUrl() - { - $linkMock = $this->getMockBuilder(Link::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id'); - $this->objectManager->expects($this->once()) - ->method('create') - ->with(Link::class) - ->willReturn($linkMock); - $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf(); - $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id'); - $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); - $linkMock->expects($this->once())->method('getSampleType')->willReturn( - Download::LINK_TYPE_URL - ); - $linkMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url'); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->linkSample->execute()); - } - - /** - * Execute Download link's sample action with File link. - * - * @return void - */ - public function testExecuteLinkTypeFile() - { - $linkMock = $this->getMockBuilder(Link::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath']) - ->getMock(); - $fileMock = $this->getMockBuilder(File::class) - ->disableOriginalConstructor() - ->setMethods(['getFilePath', 'load', 'getSampleType', 'getSampleUrl']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id'); - $this->objectManager->expects($this->at(0)) - ->method('create') - ->with(Link::class) - ->willReturn($linkMock); - $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf(); - $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id'); - $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); - $linkMock->expects($this->any())->method('getSampleType')->willReturn( - Download::LINK_TYPE_FILE - ); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(File::class) - ->willReturn($fileMock); - $this->objectManager->expects($this->at(2)) - ->method('get') - ->with(Link::class) - ->willReturn($linkMock); - $linkMock->expects($this->once())->method('getBaseSamplePath')->willReturn('downloadable/files/link_samples'); - $this->objectManager->expects($this->at(3)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->linkSample->execute()); - } -} 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/Controller/Download/SampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php deleted file mode 100644 index 6dcd09a91dd2e..0000000000000 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php +++ /dev/null @@ -1,232 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Downloadable\Test\Unit\Controller\Download; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\SalabilityChecker; -use Magento\Downloadable\Controller\Download\Sample; -use Magento\Downloadable\Helper\Data; -use Magento\Downloadable\Helper\Download; -use Magento\Downloadable\Helper\File; -use Magento\Framework\App\Request\Http; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\App\Response\RedirectInterface; -use Magento\Framework\App\ResponseInterface; -use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Framework\UrlInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Unit tests for \Magento\Downloadable\Controller\Download\Sample. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SampleTest extends TestCase -{ - /** @var \Magento\Downloadable\Controller\Download\Sample */ - protected $sample; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** - * @var MockObject|Http - */ - protected $request; - - /** - * @var MockObject|ResponseInterface - */ - protected $response; - - /** - * @var MockObject|\Magento\Framework\ObjectManager\ObjectManager - */ - protected $objectManager; - - /** - * @var MockObject|ManagerInterface - */ - protected $messageManager; - - /** - * @var MockObject|RedirectInterface - */ - protected $redirect; - - /** - * @var MockObject|Data - */ - protected $helperData; - - /** - * @var MockObject|\Magento\Downloadable\Helper\Download - */ - protected $downloadHelper; - - /** - * @var MockObject|Product - */ - protected $product; - - /** - * @var MockObject|UrlInterface - */ - protected $urlInterface; - - /** - * @var SalabilityChecker|MockObject - */ - private $salabilityCheckerMock; - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function setUp(): void - { - $this->objectManagerHelper = new ObjectManagerHelper($this); - - $this->request = $this->getMockForAbstractClass(RequestInterface::class); - $this->response = $this->getMockBuilder(ResponseInterface::class) - ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect']) - ->onlyMethods(['sendResponse']) - ->getMockForAbstractClass(); - - $this->helperData = $this->createPartialMock( - Data::class, - ['getIsShareable'] - ); - $this->downloadHelper = $this->createPartialMock( - Download::class, - [ - 'setResource', - 'getFilename', - 'getContentType', - 'getFileSize', - 'getContentDisposition', - 'output' - ] - ); - $this->product = $this->getMockBuilder(Product::class) - ->addMethods(['_wakeup']) - ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class); - $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class); - $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class); - $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class); - $this->objectManager = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create', 'get'] - ); - $this->sample = $this->objectManagerHelper->getObject( - Sample::class, - [ - 'objectManager' => $this->objectManager, - 'request' => $this->request, - 'response' => $this->response, - 'messageManager' => $this->messageManager, - 'redirect' => $this->redirect, - 'salabilityChecker' => $this->salabilityCheckerMock, - ] - ); - } - - /** - * Execute Download sample action with Sample Url. - * - * @return void - */ - public function testExecuteSampleWithUrlType() - { - $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id'); - $this->objectManager->expects($this->once()) - ->method('create') - ->with(\Magento\Downloadable\Model\Sample::class) - ->willReturn($sampleMock); - $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf(); - $sampleMock->expects($this->once())->method('getId')->willReturn('some_link_id'); - $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); - $sampleMock->expects($this->once())->method('getSampleType')->willReturn( - Download::LINK_TYPE_URL - ); - $sampleMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url'); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->sample->execute()); - } - - /** - * Execute Download sample action with Sample File. - * - * @return void - */ - public function testExecuteSampleWithFileType() - { - $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath']) - ->getMock(); - $fileHelperMock = $this->getMockBuilder(File::class) - ->disableOriginalConstructor() - ->setMethods(['getFilePath']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id'); - $this->objectManager->expects($this->at(0)) - ->method('create') - ->with(\Magento\Downloadable\Model\Sample::class) - ->willReturn($sampleMock); - $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf(); - $sampleMock->expects($this->once())->method('getId')->willReturn('some_sample_id'); - $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); - $sampleMock->expects($this->any())->method('getSampleType')->willReturn( - Download::LINK_TYPE_FILE - ); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(File::class) - ->willReturn($fileHelperMock); - $fileHelperMock->expects($this->once())->method('getFilePath')->willReturn('file_path'); - $this->objectManager->expects($this->at(2)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->sample->execute()); - } -} diff --git a/app/code/Magento/Downloadable/Test/Unit/Model/Link/UpdateHandlerTest.php b/app/code/Magento/Downloadable/Test/Unit/Model/Link/UpdateHandlerTest.php index 069e8a4e1a3d9..22cf4b9abf7ca 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Model/Link/UpdateHandlerTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Model/Link/UpdateHandlerTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Downloadable\Test\Unit\Model\Link; @@ -16,37 +17,72 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Downloadable\Model\Link\UpdateHandler. + */ class UpdateHandlerTest extends TestCase { - /** @var UpdateHandler */ - protected $model; - - /** @var LinkRepositoryInterface|MockObject */ - protected $linkRepositoryMock; - + /** + * @var UpdateHandler + */ + private $model; + + /** + * @var LinkRepositoryInterface|MockObject + */ + private $linkRepositoryMock; + + /** + * @var LinkInterface|MockObject + */ + private $linkMock; + + /** + * @var ProductExtensionInterface|MockObject + */ + private $productExtensionMock; + + /** + * @var ProductInterface|MockObject + */ + private $entityMock; + + /** + * @inheritdoc + */ protected function setUp(): void { $this->linkRepositoryMock = $this->getMockBuilder(LinkRepositoryInterface::class) ->getMockForAbstractClass(); + $this->linkMock = $this->getMockBuilder(LinkInterface::class) + ->getMock(); + $this->productExtensionMock = $this->createMock(ProductExtensionInterface::class); + $this->productExtensionMock->expects($this->once()) + ->method('getDownloadableProductLinks') + ->willReturn([$this->linkMock]); + $this->entityMock = $this->getMockBuilder(ProductInterface::class) + ->addMethods(['getStoreId']) + ->getMockForAbstractClass(); $this->model = new UpdateHandler( $this->linkRepositoryMock ); } - public function testExecute() + /** + * Update links for downloadable product + * + * @return void + */ + public function testExecute(): void { $entitySku = 'sku'; $entityStoreId = 0; - $linkId = 11; $linkToDeleteId = 22; - /** @var LinkInterface|MockObject $linkMock */ - $linkMock = $this->getMockBuilder(LinkInterface::class) - ->getMock(); - $linkMock->expects($this->exactly(3)) + $this->linkMock->expects($this->exactly(3)) ->method('getId') - ->willReturn($linkId); + ->willReturn(1); /** @var LinkInterface|MockObject $linkToDeleteMock */ $linkToDeleteMock = $this->getMockBuilder(LinkInterface::class) @@ -55,59 +91,49 @@ public function testExecute() ->method('getId') ->willReturn($linkToDeleteId); - /** @var ProductExtensionInterface|MockObject $productExtensionMock */ - $productExtensionMock = $this->getMockBuilder(ProductExtensionInterface::class) - ->setMethods(['getDownloadableProductLinks']) - ->getMockForAbstractClass(); - $productExtensionMock->expects($this->once()) - ->method('getDownloadableProductLinks') - ->willReturn([$linkMock]); - - /** @var ProductInterface|MockObject $entityMock */ - $entityMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['getTypeId', 'getExtensionAttributes', 'getSku', 'getStoreId']) - ->getMockForAbstractClass(); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getTypeId') ->willReturn(Type::TYPE_DOWNLOADABLE); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getExtensionAttributes') - ->willReturn($productExtensionMock); - $entityMock->expects($this->exactly(2)) + ->willReturn($this->productExtensionMock); + $this->entityMock->expects($this->exactly(2)) ->method('getSku') ->willReturn($entitySku); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getStoreId') ->willReturn($entityStoreId); $this->linkRepositoryMock->expects($this->once()) ->method('getList') ->with($entitySku) - ->willReturn([$linkMock, $linkToDeleteMock]); + ->willReturn([$this->linkMock, $linkToDeleteMock]); $this->linkRepositoryMock->expects($this->once()) ->method('save') - ->with($entitySku, $linkMock, !$entityStoreId); + ->with($entitySku, $this->linkMock, !$entityStoreId); $this->linkRepositoryMock->expects($this->once()) ->method('delete') ->with($linkToDeleteId); - $this->assertEquals($entityMock, $this->model->execute($entityMock)); + $this->assertEquals($this->entityMock, $this->model->execute($this->entityMock)); } - public function testExecuteNonDownloadable() + /** + * Update links for non downloadable product + * + * @return void + */ + public function testExecuteNonDownloadable(): void { - /** @var ProductInterface|MockObject $entityMock */ - $entityMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['getTypeId', 'getExtensionAttributes', 'getSku', 'getStoreId']) - ->getMockForAbstractClass(); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getTypeId') ->willReturn(Type::TYPE_DOWNLOADABLE . 'some'); - $entityMock->expects($this->never()) - ->method('getExtensionAttributes'); - $entityMock->expects($this->never()) + $this->entityMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->entityMock->expects($this->never()) ->method('getSku'); - $entityMock->expects($this->never()) + $this->entityMock->expects($this->never()) ->method('getStoreId'); $this->linkRepositoryMock->expects($this->never()) @@ -117,6 +143,6 @@ public function testExecuteNonDownloadable() $this->linkRepositoryMock->expects($this->never()) ->method('delete'); - $this->assertEquals($entityMock, $this->model->execute($entityMock)); + $this->assertEquals($this->entityMock, $this->model->execute($this->entityMock)); } } diff --git a/app/code/Magento/Downloadable/Test/Unit/Model/Sample/UpdateHandlerTest.php b/app/code/Magento/Downloadable/Test/Unit/Model/Sample/UpdateHandlerTest.php index 34d313a175b55..0f8fe92e467ce 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Model/Sample/UpdateHandlerTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Model/Sample/UpdateHandlerTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Downloadable\Test\Unit\Model\Sample; @@ -16,37 +17,72 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Downloadable\Model\Sample\UpdateHandler. + */ class UpdateHandlerTest extends TestCase { - /** @var UpdateHandler */ - protected $model; - - /** @var SampleRepositoryInterface|MockObject */ - protected $sampleRepositoryMock; - + /** + * @var UpdateHandler + */ + private $model; + + /** + * @var SampleRepositoryInterface|MockObject + */ + private $sampleRepositoryMock; + + /** + * @var SampleInterface|MockObject + */ + private $sampleMock; + + /** + * @var ProductExtensionInterface|MockObject + */ + private $productExtensionMock; + + /** + * @var ProductInterface|MockObject + */ + private $entityMock; + + /** + * @inheritdoc + */ protected function setUp(): void { $this->sampleRepositoryMock = $this->getMockBuilder(SampleRepositoryInterface::class) ->getMockForAbstractClass(); + $this->sampleMock = $this->getMockBuilder(SampleInterface::class) + ->getMock(); + $this->productExtensionMock = $this->createMock(ProductExtensionInterface::class); + $this->productExtensionMock//->expects($this->once()) + ->method('getDownloadableProductSamples') + ->willReturn([$this->sampleMock]); + $this->entityMock = $this->getMockBuilder(ProductInterface::class) + ->addMethods(['getStoreId']) + ->getMockForAbstractClass(); $this->model = new UpdateHandler( $this->sampleRepositoryMock ); } - public function testExecute() + /** + * Update samples for downloadable product + * + * @return void + */ + public function testExecute(): void { $entitySku = 'sku'; $entityStoreId = 0; - $sampleId = 11; $sampleToDeleteId = 22; - /** @var SampleInterface|MockObject $sampleMock */ - $sampleMock = $this->getMockBuilder(SampleInterface::class) - ->getMock(); - $sampleMock->expects($this->exactly(3)) + $this->sampleMock->expects($this->exactly(3)) ->method('getId') - ->willReturn($sampleId); + ->willReturn(1); /** @var SampleInterface|MockObject $sampleToDeleteMock */ $sampleToDeleteMock = $this->getMockBuilder(SampleInterface::class) @@ -55,59 +91,49 @@ public function testExecute() ->method('getId') ->willReturn($sampleToDeleteId); - /** @var ProductExtensionInterface|MockObject $productExtensionMock */ - $productExtensionMock = $this->getMockBuilder(ProductExtensionInterface::class) - ->setMethods(['getDownloadableProductSamples']) - ->getMockForAbstractClass(); - $productExtensionMock->expects($this->once()) - ->method('getDownloadableProductSamples') - ->willReturn([$sampleMock]); - - /** @var ProductInterface|MockObject $entityMock */ - $entityMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['getTypeId', 'getExtensionAttributes', 'getSku', 'getStoreId']) - ->getMockForAbstractClass(); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getTypeId') ->willReturn(Type::TYPE_DOWNLOADABLE); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getExtensionAttributes') - ->willReturn($productExtensionMock); - $entityMock->expects($this->exactly(2)) + ->willReturn($this->productExtensionMock); + $this->entityMock->expects($this->exactly(2)) ->method('getSku') ->willReturn($entitySku); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getStoreId') ->willReturn($entityStoreId); $this->sampleRepositoryMock->expects($this->once()) ->method('getList') ->with($entitySku) - ->willReturn([$sampleMock, $sampleToDeleteMock]); + ->willReturn([$this->sampleMock, $sampleToDeleteMock]); $this->sampleRepositoryMock->expects($this->once()) ->method('save') - ->with($entitySku, $sampleMock, !$entityStoreId); + ->with($entitySku, $this->sampleMock, !$entityStoreId); $this->sampleRepositoryMock->expects($this->once()) ->method('delete') ->with($sampleToDeleteId); - $this->assertEquals($entityMock, $this->model->execute($entityMock)); + $this->assertEquals($this->entityMock, $this->model->execute($this->entityMock)); } - public function testExecuteNonDownloadable() + /** + * Update samples for non downloadable product + * + * @return void + */ + public function testExecuteNonDownloadable(): void { - /** @var ProductInterface|MockObject $entityMock */ - $entityMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['getTypeId', 'getExtensionAttributes', 'getSku', 'getStoreId']) - ->getMockForAbstractClass(); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getTypeId') ->willReturn(Type::TYPE_DOWNLOADABLE . 'some'); - $entityMock->expects($this->never()) - ->method('getExtensionAttributes'); - $entityMock->expects($this->never()) + $this->entityMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->entityMock->expects($this->never()) ->method('getSku'); - $entityMock->expects($this->never()) + $this->entityMock->expects($this->never()) ->method('getStoreId'); $this->sampleRepositoryMock->expects($this->never()) @@ -117,6 +143,6 @@ public function testExecuteNonDownloadable() $this->sampleRepositoryMock->expects($this->never()) ->method('delete'); - $this->assertEquals($entityMock, $this->model->execute($entityMock)); + $this->assertEquals($this->entityMock, $this->model->execute($this->entityMock)); } } diff --git a/app/code/Magento/Downloadable/Test/Unit/Observer/IsAllowedGuestCheckoutObserverTest.php b/app/code/Magento/Downloadable/Test/Unit/Observer/IsAllowedGuestCheckoutObserverTest.php index 1973715bfb645..6040b301a60a8 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Observer/IsAllowedGuestCheckoutObserverTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Observer/IsAllowedGuestCheckoutObserverTest.php @@ -16,8 +16,9 @@ use Magento\Framework\Event\Observer; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Quote\Model\Quote; -use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -26,13 +27,17 @@ */ class IsAllowedGuestCheckoutObserverTest extends TestCase { + private const XML_PATH_DISABLE_GUEST_CHECKOUT = 'catalog/downloadable/disable_guest_checkout'; + + private const STUB_STORE_ID = 1; + /** @var IsAllowedGuestCheckoutObserver */ private $isAllowedGuestCheckoutObserver; /** * @var MockObject|Config */ - private $scopeConfig; + private $scopeConfigMock; /** * @var MockObject|DataObject @@ -54,13 +59,18 @@ class IsAllowedGuestCheckoutObserverTest extends TestCase */ private $storeMock; + /** + * @var MockObject|StoreManagerInterface + */ + private $storeManagerMock; + /** * Sets up the fixture, for example, open a network connection. * This method is called before a test is executed. */ protected function setUp(): void { - $this->scopeConfig = $this->getMockBuilder(Config::class) + $this->scopeConfigMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->setMethods(['isSetFlag', 'getValue']) ->getMock(); @@ -84,12 +94,20 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); - $this->isAllowedGuestCheckoutObserver = (new ObjectManagerHelper($this))->getObject( - IsAllowedGuestCheckoutObserver::class, - [ - 'scopeConfig' => $this->scopeConfig, - ] - ); + $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->storeManagerMock + ->method('getStore') + ->with(self::STUB_STORE_ID) + ->willReturn($this->storeMock); + + $this->isAllowedGuestCheckoutObserver = (new ObjectManagerHelper($this)) + ->getObject( + IsAllowedGuestCheckoutObserver::class, + [ + 'scopeConfig' => $this->scopeConfigMock, + 'storeManager'=> $this->storeManagerMock + ] + ); } /** @@ -99,7 +117,7 @@ protected function setUp(): void * @param $productType * @param $isAllowed */ - public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllowed) + public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllowed): void { if ($isAllowed) { $this->resultMock->expects($this->at(0)) @@ -116,7 +134,7 @@ public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllow ->method('getTypeId') ->willReturn($productType); - $item = $this->getMockBuilder(Item::class) + $item = $this->getMockBuilder(QuoteItem::class) ->disableOriginalConstructor() ->setMethods(['getProduct']) ->getMock(); @@ -138,6 +156,10 @@ public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllow ->method('getStore') ->willReturn($this->storeMock); + $this->storeMock->expects($this->any()) + ->method('getId') + ->willReturn(self::STUB_STORE_ID); + $this->eventMock->expects($this->once()) ->method('getResult') ->willReturn($this->resultMock); @@ -146,12 +168,12 @@ public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllow ->method('getQuote') ->willReturn($quote); - $this->scopeConfig->expects($this->once()) + $this->scopeConfigMock->expects($this->any()) ->method('isSetFlag') ->with( - IsAllowedGuestCheckoutObserver::XML_PATH_DISABLE_GUEST_CHECKOUT, + self::XML_PATH_DISABLE_GUEST_CHECKOUT, ScopeInterface::SCOPE_STORE, - $this->storeMock + self::STUB_STORE_ID ) ->willReturn(true); @@ -168,7 +190,7 @@ public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllow /** * @return array */ - public function dataProviderForTestisAllowedGuestCheckoutConfigSetToTrue() + public function dataProviderForTestisAllowedGuestCheckoutConfigSetToTrue(): array { return [ 1 => [Type::TYPE_DOWNLOADABLE, true], @@ -176,26 +198,61 @@ public function dataProviderForTestisAllowedGuestCheckoutConfigSetToTrue() ]; } - public function testIsAllowedGuestCheckoutConfigSetToFalse() + public function testIsAllowedGuestCheckoutConfigSetToFalse(): void { + $product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getTypeId']) + ->getMock(); + + $product->expects($this->once()) + ->method('getTypeId') + ->willReturn(Type::TYPE_DOWNLOADABLE); + + $item = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMock(); + + $item->expects($this->once()) + ->method('getProduct') + ->willReturn($product); + + $quote = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->setMethods(['getAllItems']) + ->getMock(); + + $quote->expects($this->once()) + ->method('getAllItems') + ->willReturn([$item]); + $this->eventMock->expects($this->once()) ->method('getStore') ->willReturn($this->storeMock); + $this->storeMock->expects($this->any()) + ->method('getId') + ->willReturn(self::STUB_STORE_ID); + $this->eventMock->expects($this->once()) ->method('getResult') ->willReturn($this->resultMock); - $this->scopeConfig->expects($this->once()) + $this->eventMock->expects($this->once()) + ->method('getQuote') + ->will($this->returnValue($quote)); + + $this->scopeConfigMock->expects($this->once()) ->method('isSetFlag') ->with( - IsAllowedGuestCheckoutObserver::XML_PATH_DISABLE_GUEST_CHECKOUT, + self::XML_PATH_DISABLE_GUEST_CHECKOUT, ScopeInterface::SCOPE_STORE, - $this->storeMock + self::STUB_STORE_ID ) ->willReturn(false); - $this->observerMock->expects($this->exactly(2)) + $this->observerMock->expects($this->exactly(3)) ->method('getEvent') ->willReturn($this->eventMock); diff --git a/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php b/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php index 80f23c859a031..09edbf4935fe4 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php @@ -176,6 +176,9 @@ public function testSaveDownloadableOrderItem() $itemMock->expects($this->any()) ->method('getProductType') ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); + $itemMock->expects($this->any()) + ->method('getRealProductType') + ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); $this->orderMock->expects($this->once()) ->method('getStoreId') @@ -311,6 +314,9 @@ public function testSaveDownloadableOrderItemSavedPurchasedLink() $itemMock->expects($this->any()) ->method('getProductType') ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); + $itemMock->expects($this->any()) + ->method('getRealProductType') + ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); $purchasedLink = $this->getMockBuilder(Purchased::class) ->disableOriginalConstructor() 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/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/creditmemo/name.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/creditmemo/name.phtml index 94c8405c718a8..91dd22bc3ce5c 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/creditmemo/name.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/creditmemo/name.phtml @@ -3,42 +3,53 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** + * @var \Magento\Downloadable\Block\Adminhtml\Sales\Items\Column\Downloadable\Name $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($_item = $block->getItem()) : ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +if ($_item = $block->getItem()): ?> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> - <div><strong><?= $block->escapeHtml(__('SKU')) ?>:</strong> <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($block->getSku())) ?></div> - <?php if ($block->getOrderOptions()) : ?> + <div><strong><?= $block->escapeHtml(__('SKU')) ?>:</strong> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($block->getSku())) ?></div> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $_option) : ?> + <?php foreach ($block->getOrderOptions() as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> <dd> - <?php if (isset($_option['custom_view']) && $_option['custom_view']) : ?> + <?php if (isset($_option['custom_view']) && $_option['custom_view']): ?> <?= $block->escapeHtml($_option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($_option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"> + <?= $block->escapeHtml($_remainder) ?> + </span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); - + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){ $('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){ $('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> <?php endif; ?> - <?php if ($block->getLinks()) : ?> + <?php if ($block->getLinks()): ?> <dl class="item-options"> <dt><?= $block->escapeHtml($block->getLinksTitle()) ?></dt> - <?php foreach ($block->getLinks()->getPurchasedItems() as $_link) : ?> + <?php foreach ($block->getLinks()->getPurchasedItems() as $_link): ?> <dd><?= $block->escapeHtml($_link->getLinkTitle()) ?></dd> <?php endforeach; ?> </dl> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/invoice/name.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/invoice/name.phtml index 9a45066f64d15..a0b710bdb5d17 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/invoice/name.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/invoice/name.phtml @@ -3,42 +3,55 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var \Magento\Downloadable\Block\Adminhtml\Sales\Items\Column\Downloadable\Name $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($_item = $block->getItem()) : ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +if ($_item = $block->getItem()): ?> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> - <div><strong><?= $block->escapeHtml(__('SKU')) ?>:</strong> <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($block->getSku())) ?></div> - <?php if ($block->getOrderOptions()) : ?> + <div><strong><?= $block->escapeHtml(__('SKU')) ?>:</strong> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($block->getSku())) ?></div> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $_option) : ?> + <?php foreach ($block->getOrderOptions() as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> <dd> - <?php if (isset($_option['custom_view']) && $_option['custom_view']) : ?> + <?php if (isset($_option['custom_view']) && $_option['custom_view']): ?> <?= $block->escapeHtml($_option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($_option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"> + <?= $block->escapeHtml($_remainder) ?> + </span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){ $('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){ $('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> <?php endif; ?> - <?php if ($block->getLinks()) : ?> + <?php if ($block->getLinks()): ?> <dl class="item-options"> <dt><?= $block->escapeHtml($block->getLinksTitle()) ?></dt> - <?php foreach ($block->getLinks()->getPurchasedItems() as $_link) : ?> - <dd><?= $block->escapeHtml($_link->getLinkTitle()) ?> (<?= $block->escapeHtml($_link->getNumberOfDownloadsBought() ? $_link->getNumberOfDownloadsBought() : __('Unlimited')) ?>)</dd> + <?php foreach ($block->getLinks()->getPurchasedItems() as $_link): ?> + <dd><?= $block->escapeHtml($_link->getLinkTitle()) ?> + (<?= $block->escapeHtml($_link->getNumberOfDownloadsBought() ? + $_link->getNumberOfDownloadsBought() : __('Unlimited')) ?>) + </dd> <?php endforeach; ?> </dl> <?php endif; ?> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/name.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/name.phtml index b5fe7b3385630..7808a214dd76a 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/name.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/name.phtml @@ -3,45 +3,57 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var \Magento\Downloadable\Block\Adminhtml\Sales\Items\Column\Downloadable\Name $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($_item = $block->getItem()) : ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +if ($_item = $block->getItem()): ?> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($block->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($block->getSku())) ?> </div> - <?php if ($block->getOrderOptions()) : ?> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $_option) : ?> + <?php foreach ($block->getOrderOptions() as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?>:</dt> <dd> - <?php if (isset($_option['custom_view']) && $_option['custom_view']) : ?> + <?php if (isset($_option['custom_view']) && $_option['custom_view']): ?> <?= $block->escapeHtml($_option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($_option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"> + <?= $block->escapeHtml($_remainder) ?> + </span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){ $('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){ $('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> <?php endif; ?> - <?php if ($block->getLinks()) : ?> + <?php if ($block->getLinks()): ?> <dl class="item-options"> <dt><?= $block->escapeHtml($block->getLinksTitle()) ?>:</dt> - <?php foreach ($block->getLinks()->getPurchasedItems() as $_link) : ?> - <dd><?= $block->escapeHtml($_link->getLinkTitle()) ?> (<?= $block->escapeHtml($_link->getNumberOfDownloadsUsed() . ' / ' . ($_link->getNumberOfDownloadsBought() ? $_link->getNumberOfDownloadsBought() : __('U'))) ?>)</dd> + <?php foreach ($block->getLinks()->getPurchasedItems() as $_link): ?> + <dd><?= $block->escapeHtml($_link->getLinkTitle()) ?> + (<?= $block->escapeHtml($_link->getNumberOfDownloadsUsed() . ' / ' . + ($_link->getNumberOfDownloadsBought() ? $_link->getNumberOfDownloadsBought() : __('U'))) ?>) + </dd> <?php endforeach; ?> </dl> <?php endif; ?> diff --git a/app/code/Magento/Downloadable/view/frontend/templates/customer/products/list.phtml b/app/code/Magento/Downloadable/view/frontend/templates/customer/products/list.phtml index eca72b3500924..4935743c2de7d 100644 --- a/app/code/Magento/Downloadable/view/frontend/templates/customer/products/list.phtml +++ b/app/code/Magento/Downloadable/view/frontend/templates/customer/products/list.phtml @@ -3,14 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<?php + +use Magento\Downloadable\Model\Link\Purchased\Item; + /** * @var $block \Magento\Downloadable\Block\Customer\Products\ListProducts + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_items = $block->getItems(); ?> -<?php if (count($_items)) : ?> +<?php if (count($_items)): ?> <div class="table-wrapper downloadable-products"> <table id="my-downloadable-products-table" class="data table table-downloadable-products"> <caption class="table-caption"><?= $block->escapeHtml(__('Downloadable Products')) ?></caption> @@ -24,35 +26,57 @@ </tr> </thead> <tbody> - <?php foreach ($_items as $_item) : ?> + <?php foreach ($_items as $_item): ?> <tr> <td data-th="<?= $block->escapeHtmlAttr(__('Order #')) ?>" class="col id"> - <a href="<?= $block->escapeUrl($block->getOrderViewUrl($_item->getPurchased()->getOrderId())) ?>" + <a href="<?= $block->escapeUrl($block->getOrderViewUrl($_item->getPurchased()->getOrderId()))?>" title="<?= $block->escapeHtml(__('View Order')) ?>"> <?= $block->escapeHtml($_item->getPurchased()->getOrderIncrementId()) ?> </a> </td> - <td data-th="<?= $block->escapeHtmlAttr(__('Date')) ?>" class="col date"><?= $block->escapeHtml($block->formatDate($_item->getPurchased()->getCreatedAt())) ?></td> + <td data-th="<?= $block->escapeHtmlAttr(__('Date')) ?>" class="col date"> + <?= $block->escapeHtml($block->formatDate($_item->getPurchased()->getCreatedAt())) ?> + </td> <td data-th="<?= $block->escapeHtmlAttr(__('Title')) ?>" class="col title"> - <strong class="product-name"><?= $block->escapeHtml($_item->getPurchased()->getProductName()) ?></strong> - <?php if ($_item->getStatus() == \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_AVAILABLE) : ?> - <a href="<?= $block->escapeUrl($block->getDownloadUrl($_item)) ?>" title="<?= $block->escapeHtmlAttr(__('Start Download')) ?>" class="action download" <?= /* @noEscape */ $block->getIsOpenInNewWindow() ? 'onclick="this.target=\'_blank\'"' : '' ?>><?= $block->escapeHtml($_item->getLinkTitle()) ?></a> + <strong class="product-name"> + <?= $block->escapeHtml($_item->getPurchased()->getProductName()) ?> + </strong> + <?php if ($_item->getStatus() == Item::LINK_STATUS_AVAILABLE): ?> + <a href="<?= $block->escapeUrl($block->getDownloadUrl($_item)) ?>" + id="download_<?= /* @noEscape */ $_item->getPurchased()->getProductId() ?>" + title="<?= $block->escapeHtmlAttr(__('Start Download')) ?>" + class="action download"> + <?= $block->escapeHtml($_item->getLinkTitle()) ?> + </a> + <?php if ($block->getIsOpenInNewWindow()): ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "this.target='_blank'", + 'a#download_' . $_item->getPurchased()->getProductId() + ) ?> + <?php endif; ?> <?php endif; ?> </td> - <td data-th="<?= $block->escapeHtmlAttr(__('Status')) ?>" class="col status"><?= $block->escapeHtml(__(ucfirst($_item->getStatus()))) ?></td> - <td data-th="<?= $block->escapeHtmlAttr(__('Remaining Downloads')) ?>" class="col remaining"><?= $block->escapeHtml($block->getRemainingDownloads($_item)) ?></td> + <td data-th="<?= $block->escapeHtmlAttr(__('Status')) ?>" class="col status"> + <?= $block->escapeHtml(__(ucfirst($_item->getStatus()))) ?> + </td> + <td data-th="<?= $block->escapeHtmlAttr(__('Remaining Downloads')) ?>" class="col remaining"> + <?= $block->escapeHtml($block->getRemainingDownloads($_item)) ?> + </td> </tr> <?php endforeach; ?> </tbody> </table> </div> - <?php if ($block->getChildHtml('pager')) : ?> + <?php if ($block->getChildHtml('pager')): ?> <div class="toolbar downloadable-products-toolbar bottom"> <?= $block->getChildHtml('pager') ?> </div> <?php endif; ?> -<?php else : ?> - <div class="message info empty"><span><?= $block->escapeHtml(__('You have not purchased any downloadable products yet.')) ?></span></div> +<?php else: ?> + <div class="message info empty"> + <span><?= $block->escapeHtml(__('You have not purchased any downloadable products yet.')) ?></span> + </div> <?php endif; ?> <div class="actions-toolbar"> 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/Resolver/Product/DownloadableLinksValueUid.php b/app/code/Magento/DownloadableGraphQl/Resolver/Product/DownloadableLinksValueUid.php new file mode 100644 index 0000000000000..03727597104fd --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Resolver/Product/DownloadableLinksValueUid.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Resolver\Product; + +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; + +/** + * Formatting the uid for downloadable link + */ +class DownloadableLinksValueUid implements ResolverInterface +{ + /** + * Option type name + */ + private const OPTION_TYPE = 'downloadable'; + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['id']) || empty($value['id'])) { + throw new GraphQlInputException(__('"id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['id'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} 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 2226f1acd8501..ba178bb1a427e 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -53,6 +53,7 @@ type DownloadableProductLinks @doc(description: "DownloadableProductLinks define link_type: DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") 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") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") # A Base64 string that encodes option details. } type DownloadableProductSamples @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") { @@ -63,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/Api/AttributeOptionManagementInterface.php b/app/code/Magento/Eav/Api/AttributeOptionManagementInterface.php index 84aefa700a52a..5359230c08c2a 100644 --- a/app/code/Magento/Eav/Api/AttributeOptionManagementInterface.php +++ b/app/code/Magento/Eav/Api/AttributeOptionManagementInterface.php @@ -15,8 +15,8 @@ interface AttributeOptionManagementInterface /** * Add option to attribute * - * @param string $attributeCode * @param int $entityType + * @param string $attributeCode * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option * @throws \Magento\Framework\Exception\StateException * @throws \Magento\Framework\Exception\InputException diff --git a/app/code/Magento/Eav/Api/AttributeOptionUpdateInterface.php b/app/code/Magento/Eav/Api/AttributeOptionUpdateInterface.php new file mode 100644 index 0000000000000..fd755a08fdf9a --- /dev/null +++ b/app/code/Magento/Eav/Api/AttributeOptionUpdateInterface.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Api; + +/** + * Interface to update attribute option + * + * @api + */ +interface AttributeOptionUpdateInterface +{ + /** + * Update attribute option + * + * @param string $entityType + * @param string $attributeCode + * @param int $optionId + * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @return bool + * @throws \Magento\Framework\Exception\StateException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function update( + string $entityType, + string $attributeCode, + int $optionId, + \Magento\Eav\Api\Data\AttributeOptionInterface $option + ): bool; +} diff --git a/app/code/Magento/Eav/Api/Data/AttributeDefaultValueInterface.php b/app/code/Magento/Eav/Api/Data/AttributeDefaultValueInterface.php index 56ae16c53402c..45022e25c5c31 100644 --- a/app/code/Magento/Eav/Api/Data/AttributeDefaultValueInterface.php +++ b/app/code/Magento/Eav/Api/Data/AttributeDefaultValueInterface.php @@ -11,7 +11,7 @@ * Allows to manage attribute default value through interface * @api * @package Magento\Eav\Api\Data - * @since 100.2.0 + * @since 101.0.0 */ interface AttributeDefaultValueInterface { @@ -20,13 +20,13 @@ interface AttributeDefaultValueInterface /** * @param string $defaultValue * @return \Magento\Framework\Api\MetadataObjectInterface - * @since 100.2.0 + * @since 101.0.0 */ public function setDefaultValue($defaultValue); /** * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getDefaultValue(); } diff --git a/app/code/Magento/Eav/Api/Data/AttributeInterface.php b/app/code/Magento/Eav/Api/Data/AttributeInterface.php index 55d6e58b64b71..d96c2329ec594 100644 --- a/app/code/Magento/Eav/Api/Data/AttributeInterface.php +++ b/app/code/Magento/Eav/Api/Data/AttributeInterface.php @@ -316,6 +316,7 @@ public function getExtensionAttributes(); * * @param \Magento\Eav\Api\Data\AttributeExtensionInterface $extensionAttributes * @return $this + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Eav\Api\Data\AttributeExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php index 7dd6b0a19ec02..577dac5b0c28b 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php @@ -38,7 +38,7 @@ public function __construct( } /** - * @deprecated Misspelled method + * @deprecated 102.0.0 Misspelled method * @see getCompatibleInputTypes */ public function getComaptibleInputTypes() diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/AbstractOptions.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/AbstractOptions.php index 9b44b2c7395ac..70fd5b2914600 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/AbstractOptions.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/AbstractOptions.php @@ -9,7 +9,7 @@ * Attribute add/edit form options tab * * @api - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @since 100.0.2 */ abstract class AbstractOptions extends \Magento\Framework\View\Element\AbstractBlock diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Grid/AbstractGrid.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Grid/AbstractGrid.php index 55c0583191492..839ee7584cf03 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Grid/AbstractGrid.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Grid/AbstractGrid.php @@ -10,7 +10,7 @@ * * @api * @SuppressWarnings(PHPMD.DepthOfInheritance) - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @since 100.0.2 */ abstract class AbstractGrid extends \Magento\Backend\Block\Widget\Grid\Extended diff --git a/app/code/Magento/Eav/Model/Attribute/Data/File.php b/app/code/Magento/Eav/Model/Attribute/Data/File.php index a52c88261166e..be237c70ffd93 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/File.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/File.php @@ -7,12 +7,14 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\RequestInterface; +use Magento\Framework\Filesystem\Io\File as FileIo; /** * EAV Entity Attribute File Data Model * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class File extends \Magento\Eav\Model\Attribute\Data\AbstractData { @@ -38,6 +40,11 @@ class File extends \Magento\Eav\Model\Attribute\Data\AbstractData */ protected $_directory; + /** + * @var FileIo + */ + private $fileIo; + /** * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Psr\Log\LoggerInterface $logger @@ -45,6 +52,7 @@ class File extends \Magento\Eav\Model\Attribute\Data\AbstractData * @param \Magento\Framework\Url\EncoderInterface $urlEncoder * @param \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $fileValidator * @param \Magento\Framework\Filesystem $filesystem + * @param FileIo $fileIo * @codeCoverageIgnore */ public function __construct( @@ -53,12 +61,14 @@ public function __construct( \Magento\Framework\Locale\ResolverInterface $localeResolver, \Magento\Framework\Url\EncoderInterface $urlEncoder, \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $fileValidator, - \Magento\Framework\Filesystem $filesystem + \Magento\Framework\Filesystem $filesystem, + FileIo $fileIo ) { parent::__construct($localeDate, $logger, $localeResolver); $this->urlEncoder = $urlEncoder; $this->_fileValidator = $fileValidator; $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->fileIo = $fileIo; } /** @@ -86,7 +96,7 @@ public function extractValue(RequestInterface $request) $mainScope = $this->_requestScope; $scopes = []; } - + // phpcs:disable Magento2.Security.Superglobal if (!empty($_FILES[$mainScope])) { foreach ($_FILES[$mainScope] as $fileKey => $scopeData) { foreach ($scopes as $scopeName) { @@ -104,12 +114,15 @@ public function extractValue(RequestInterface $request) } else { $value = []; } + // phpcs:enable Magento2.Security.Superglobal } else { + // phpcs:disable Magento2.Security.Superglobal if (isset($_FILES[$attrCode])) { $value = $_FILES[$attrCode]; } else { $value = []; } + // phpcs:enable Magento2.Security.Superglobal } if (!empty($extend['delete'])) { @@ -129,7 +142,7 @@ protected function _validateByRules($value) { $label = $this->getAttribute()->getStoreLabel(); $rules = $this->getAttribute()->getValidateRules(); - $extension = pathinfo($value['name'], PATHINFO_EXTENSION); + $extension = $this->fileIo->getPathInfo($value['name'])['extension']; if (!empty($rules['file_extensions'])) { $extensions = explode(',', $rules['file_extensions']); @@ -146,7 +159,9 @@ protected function _validateByRules($value) return $this->_fileValidator->getMessages(); } - if (!empty($value['tmp_name']) && !file_exists($value['tmp_name'])) { + if (!empty($value['tmp_name']) + && !$this->_directory->getDriver()->isExists($value['tmp_name']) + ) { return [__('"%1" is not a valid file.', $label)]; } @@ -177,8 +192,9 @@ public function validateValue($value) if (is_string($value) && !empty($value)) { $dir = $this->_directory->getAbsolutePath($this->getAttribute()->getEntityType()->getEntityTypeCode()); + $stat = $this->_directory->getDriver()->stat($dir . $value); $fileData = [ - 'size' => filesize($dir . $value), + 'size' => $stat['size'], 'name' => $value, 'tmp_name' => $dir . $value ]; @@ -209,8 +225,6 @@ public function validateValue($value) if (count($errors) == 0) { return true; - } elseif (is_string($value) && !empty($value)) { - $this->_directory->delete($dir . $value); } return $errors; diff --git a/app/code/Magento/Eav/Model/Attribute/GroupRepository.php b/app/code/Magento/Eav/Model/Attribute/GroupRepository.php index 07ca71d95eba5..f717a01a4384f 100644 --- a/app/code/Magento/Eav/Model/Attribute/GroupRepository.php +++ b/app/code/Magento/Eav/Model/Attribute/GroupRepository.php @@ -181,7 +181,7 @@ public function deleteById($groupId) /** * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return null|string - * @deprecated + * @deprecated 101.0.3 */ protected function retrieveAttributeSetIdFromSearchCriteria( \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria @@ -199,7 +199,7 @@ protected function retrieveAttributeSetIdFromSearchCriteria( /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Eav/Model/AttributeRepository.php b/app/code/Magento/Eav/Model/AttributeRepository.php index 337ae7334486e..bb307d5581121 100644 --- a/app/code/Magento/Eav/Model/AttributeRepository.php +++ b/app/code/Magento/Eav/Model/AttributeRepository.php @@ -208,7 +208,7 @@ public function deleteById($attributeId) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Eav/Model/AttributeSetRepository.php b/app/code/Magento/Eav/Model/AttributeSetRepository.php index caab82da3910d..73e8749952812 100644 --- a/app/code/Magento/Eav/Model/AttributeSetRepository.php +++ b/app/code/Magento/Eav/Model/AttributeSetRepository.php @@ -126,7 +126,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr /** * Retrieve entity type code from search criteria * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return null|string */ @@ -188,7 +188,7 @@ public function deleteById($attributeSetId) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Eav/Model/Config.php b/app/code/Magento/Eav/Model/Config.php index 718ef1a748590..8522700adbb6d 100644 --- a/app/code/Magento/Eav/Model/Config.php +++ b/app/code/Magento/Eav/Model/Config.php @@ -509,12 +509,12 @@ protected function _initAttributes($entityType) /** * Get attributes by entity type * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see \Magento\Eav\Model\Config::getEntityAttributes * * @param string $entityType * @return AbstractAttribute[] - * @since 100.2.0 + * @since 101.0.0 */ public function getAttributes($entityType) { @@ -724,7 +724,7 @@ private function createAttribute($model) /** * Get codes of all entity type attributes * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see \Magento\Eav\Model\Config::getEntityAttributes * * @param mixed $entityType @@ -745,7 +745,7 @@ public function getEntityAttributeCodes($entityType, $object = null) * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) - * @since 100.2.0 + * @since 101.0.0 */ public function getEntityAttributes($entityType, $object = null) { @@ -839,6 +839,7 @@ protected function _createAttribute($entityType, $attributeData) } /** @var AbstractAttribute $attribute */ $attribute = $this->createAttribute($model)->setData($attributeData); + $attribute->setOrigData('entity_type_id', $attribute->getEntityTypeId()); $this->_addAttributeReference( $attributeData['attribute_id'], $code, diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index a298aad6356c3..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; @@ -1013,7 +1016,7 @@ public function load($object, $entityId, $attributes = []) /** * Loads attributes metadata. * - * @deprecated 100.2.0 Use self::loadAttributesForObject instead + * @deprecated 101.0.0 Use self::loadAttributesForObject instead * @param array|null $attributes * @return $this * @since 100.1.0 @@ -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) { @@ -1991,7 +1997,7 @@ public function afterDelete(DataObject $object) * @param array $attributes * @param AbstractEntity|null $object * @return void - * @since 100.2.0 + * @since 101.0.0 */ protected function loadAttributesForObject($attributes, $object = null) { diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php index 651bc96193780..04175c2da94d1 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute.php @@ -260,6 +260,8 @@ public function beforeSave() ); } + $this->validateEntityType(); + $defaultValue = $this->getDefaultValue(); $hasDefaultValue = (string)$defaultValue != ''; @@ -535,4 +537,21 @@ public function __wakeup() $this->reservedAttributeList = $objectManager->get(\Magento\Catalog\Model\Product\ReservedAttributeList::class); $this->dateTimeFormatter = $objectManager->get(DateTimeFormatterInterface::class); } + + /** + * Entity type for existing attribute shouldn't be changed. + * + * @return void + * @throws LocalizedException + */ + private function validateEntityType(): void + { + if ($this->getId() !== null) { + $origEntityTypeId = $this->getOrigData('entity_type_id'); + + if (($origEntityTypeId !== null) && ((int)$this->getEntityTypeId() !== (int)$origEntityTypeId)) { + throw new LocalizedException(__('Do not change entity type.')); + } + } + } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index 7066a752fe2a2..af621e17f4249 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -130,7 +130,7 @@ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtens * Serializer Instance. * * @var Json - * @since 100.2.0 + * @since 101.0.0 */ protected $serializer; @@ -219,10 +219,10 @@ public function __construct( /** * Get Serializer instance. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * * @return Json - * @since 100.2.0 + * @since 101.0.0 */ protected function getSerializer() { @@ -929,7 +929,7 @@ public function _getFlatColumnsDdlDefinition() * * Used in database compatible mode * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AttributeGroupAlreadyExistsException.php b/app/code/Magento/Eav/Model/Entity/Attribute/AttributeGroupAlreadyExistsException.php index fa50aa588b4ed..892018983cd1c 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AttributeGroupAlreadyExistsException.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AttributeGroupAlreadyExistsException.php @@ -9,7 +9,7 @@ /** * @api - * @since 100.2.0 + * @since 101.0.0 */ class AttributeGroupAlreadyExistsException extends AlreadyExistsException { diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php index 156c0326f2b6f..b9fbb876dd6c3 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php @@ -11,7 +11,7 @@ * Backend model for attribute that stores structures in json format * * @api - * @since 100.2.0 + * @since 101.0.0 */ class JsonEncoded extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend { @@ -35,7 +35,7 @@ public function __construct(Json $jsonSerializer) * * @param \Magento\Framework\DataObject $object * @return $this - * @since 100.2.0 + * @since 101.0.0 */ public function beforeSave($object) { @@ -52,7 +52,7 @@ public function beforeSave($object) * * @param \Magento\Framework\DataObject $object * @return $this - * @since 100.2.0 + * @since 101.0.0 */ public function afterLoad($object) { diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php index 0ea4c324fe5c9..e99f4395953ad 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php @@ -7,7 +7,12 @@ namespace Magento\Eav\Model\Entity\Attribute; +use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Eav\Api\AttributeOptionUpdateInterface; use Magento\Eav\Api\Data\AttributeInterface as EavAttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\AttributeRepository; +use Magento\Eav\Model\ResourceModel\Entity\Attribute; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; @@ -15,26 +20,26 @@ /** * Eav Option Management */ -class OptionManagement implements \Magento\Eav\Api\AttributeOptionManagementInterface +class OptionManagement implements AttributeOptionManagementInterface, AttributeOptionUpdateInterface { /** - * @var \Magento\Eav\Model\AttributeRepository + * @var AttributeRepository */ protected $attributeRepository; /** - * @var \Magento\Eav\Model\ResourceModel\Entity\Attribute + * @var Attribute */ protected $resourceModel; /** - * @param \Magento\Eav\Model\AttributeRepository $attributeRepository - * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute $resourceModel + * @param AttributeRepository $attributeRepository + * @param Attribute $resourceModel * @codeCoverageIgnore */ public function __construct( - \Magento\Eav\Model\AttributeRepository $attributeRepository, - \Magento\Eav\Model\ResourceModel\Entity\Attribute $resourceModel + AttributeRepository $attributeRepository, + Attribute $resourceModel ) { $this->attributeRepository = $attributeRepository; $this->resourceModel = $resourceModel; @@ -45,45 +50,100 @@ public function __construct( * * @param int $entityType * @param string $attributeCode - * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @param AttributeOptionInterface $option * @return string * @throws InputException * @throws NoSuchEntityException * @throws StateException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function add($entityType, $attributeCode, $option) { - if (empty($attributeCode)) { - throw new InputException(__('The attribute code is empty. Enter the code and try again.')); + $attribute = $this->loadAttribute($entityType, (string)$attributeCode); + + $label = trim($option->getLabel() ?: ''); + if (empty($label)) { + throw new InputException(__('The attribute option label is empty. Enter the value and try again.')); } - $attribute = $this->attributeRepository->get($entityType, $attributeCode); - if (!$attribute->usesSource()) { - throw new StateException(__('The "%1" attribute doesn\'t work with options.', $attributeCode)); + if ($attribute->getSource()->getOptionId($label) !== null) { + throw new InputException( + __( + 'Admin store attribute option label "%1" is already exists.', + $option->getLabel() + ) + ); } - $optionLabel = $option->getLabel(); - $optionId = $this->getOptionId($option); - $options = []; - $options['value'][$optionId][0] = $optionLabel; - $options['order'][$optionId] = $option->getSortOrder(); + $optionId = $this->getNewOptionId($option); + $this->saveOption($attribute, $option, $optionId); - if (is_array($option->getStoreLabels())) { - foreach ($option->getStoreLabels() as $label) { - $options['value'][$optionId][$label->getStoreId()] = $label->getLabel(); - } - } + return $this->retrieveOptionId($attribute, $option); + } - if (!$this->isAttributeOptionLabelExists($attribute, (string) $options['value'][$optionId][0])) { + /** + * @inheritdoc + */ + public function update( + string $entityType, + string $attributeCode, + int $optionId, + AttributeOptionInterface $option + ): bool { + $attribute = $this->loadAttribute($entityType, (string)$attributeCode); + if (empty($optionId)) { + throw new InputException(__('The option id is empty. Enter the value and try again.')); + } + $label = trim($option->getLabel() ?: ''); + if (empty($label)) { + throw new InputException(__('The attribute option label is empty. Enter the value and try again.')); + } + if ($attribute->getSource()->getOptionText($optionId) === false) { throw new InputException( __( - 'Admin store attribute option label "%1" is already exists.', - $options['value'][$optionId][0] + 'The \'%1\' attribute doesn\'t include an option id \'%2\'.', + $attribute->getAttributeCode(), + $optionId + ) + ); + } + $optionIdByLabel = $attribute->getSource()->getOptionId($label); + if (!empty($optionIdByLabel) && (int)$optionIdByLabel !== (int)$optionId) { + throw new InputException( + __( + 'Admin store attribute option label \'%1\' is already exists.', + $option->getLabel() ) ); } + $this->saveOption($attribute, $option, $optionId); + + return true; + } + + /** + * Save attribute option + * + * @param EavAttributeInterface $attribute + * @param AttributeOptionInterface $option + * @param int|string $optionId + * @return AttributeOptionInterface + * @throws StateException + */ + private function saveOption( + EavAttributeInterface $attribute, + AttributeOptionInterface $option, + $optionId + ): AttributeOptionInterface { + $optionLabel = trim($option->getLabel()); + $options = []; + $options['value'][$optionId][0] = $optionLabel; + $options['order'][$optionId] = $option->getSortOrder(); + if (is_array($option->getStoreLabels())) { + foreach ($option->getStoreLabels() as $label) { + $options['value'][$optionId][$label->getStoreId()] = $label->getLabel(); + } + } if ($option->getIsDefault()) { $attribute->setDefault([$optionId]); } @@ -91,29 +151,35 @@ public function add($entityType, $attributeCode, $option) $attribute->setOption($options); try { $this->resourceModel->save($attribute); - if ($optionLabel && $attribute->getAttributeCode()) { - $this->setOptionValue($option, $attribute, $optionLabel); - } } catch (\Exception $e) { - throw new StateException(__('The "%1" attribute can\'t be saved.', $attributeCode)); + throw new StateException(__('The "%1" attribute can\'t be saved.', $attribute->getAttributeCode())); } - return $this->getOptionId($option); + return $option; } /** - * @inheritdoc + * Get option id to create new option + * + * @param AttributeOptionInterface $option + * @return string */ - public function delete($entityType, $attributeCode, $optionId) + private function getNewOptionId(AttributeOptionInterface $option): string { - if (empty($attributeCode)) { - throw new InputException(__('The attribute code is empty. Enter the code and try again.')); + $optionId = trim($option->getValue() ?: ''); + if (empty($optionId)) { + $optionId = 'new_option'; } - $attribute = $this->attributeRepository->get($entityType, $attributeCode); - if (!$attribute->usesSource()) { - throw new StateException(__('The "%1" attribute has no option.', $attributeCode)); - } + return 'id_' . $optionId; + } + + /** + * @inheritdoc + */ + public function delete($entityType, $attributeCode, $optionId) + { + $attribute = $this->loadAttribute($entityType, $attributeCode); $this->validateOption($attribute, $optionId); $removalMarker = [ @@ -173,63 +239,55 @@ protected function validateOption($attribute, $optionId) } /** - * Returns option id + * Load attribute * - * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option - * @return string + * @param string|int $entityType + * @param string $attributeCode + * @return EavAttributeInterface + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException */ - private function getOptionId(\Magento\Eav\Api\Data\AttributeOptionInterface $option) : string + private function loadAttribute($entityType, string $attributeCode): EavAttributeInterface { - return 'id_' . ($option->getValue() ?: 'new_option'); + if (empty($attributeCode)) { + throw new InputException(__('The attribute code is empty. Enter the code and try again.')); + } + + $attribute = $this->attributeRepository->get($entityType, $attributeCode); + if (!$attribute->usesSource()) { + throw new StateException(__('The "%1" attribute doesn\'t work with options.', $attributeCode)); + } + + $attribute->setStoreId(0); + + return $attribute; } /** - * Set option value + * Retrieve option id * - * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option * @param EavAttributeInterface $attribute - * @param string $optionLabel - * @return void + * @param AttributeOptionInterface $option + * @return string */ - private function setOptionValue( - \Magento\Eav\Api\Data\AttributeOptionInterface $option, + private function retrieveOptionId( EavAttributeInterface $attribute, - string $optionLabel - ) { - $optionId = $attribute->getSource()->getOptionId($optionLabel); + AttributeOptionInterface $option + ) : string { + $label = trim($option->getLabel()); + $optionId = $attribute->getSource()->getOptionId($label); if ($optionId) { - $option->setValue($attribute->getSource()->getOptionId($optionId)); + $option->setValue($optionId); } elseif (is_array($option->getStoreLabels())) { foreach ($option->getStoreLabels() as $label) { - if ($optionId = $attribute->getSource()->getOptionId($label->getLabel())) { - $option->setValue($attribute->getSource()->getOptionId($optionId)); + $optionId = $attribute->getSource()->getOptionId($label->getLabel()); + if ($optionId) { break; } } } - } - - /** - * Checks if the incoming attribute option label for admin store is already exists. - * - * @param EavAttributeInterface $attribute - * @param string $adminStoreLabel - * @param int $storeId - * @return bool - */ - private function isAttributeOptionLabelExists( - EavAttributeInterface $attribute, - string $adminStoreLabel, - int $storeId = 0 - ) :bool { - $attribute->setStoreId($storeId); - - foreach ($attribute->getSource()->toOptionArray() as $existingAttributeOption) { - if ($existingAttributeOption['label'] === $adminStoreLabel) { - return false; - } - } - return true; + return (string) $optionId; } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Set.php b/app/code/Magento/Eav/Model/Entity/Attribute/Set.php index c3725ac580dcf..71c090c359fd4 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Set.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Set.php @@ -375,7 +375,7 @@ public function getDefaultGroupId($setId = null) * Get resource instance * * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb - * @deprecated 100.2.0 because resource models should be used directly + * @deprecated 101.0.0 because resource models should be used directly */ protected function _getResource() { diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index 1fc513ed0ea80..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()) { @@ -1604,6 +1610,7 @@ protected function _reset() * * @param string $attributeCode * @return bool + * @since 102.0.0 */ public function isAttributeAdded($attributeCode) : bool { diff --git a/app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php b/app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php new file mode 100644 index 0000000000000..305ed202ff22b --- /dev/null +++ b/app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model\ResourceModel; + +use Magento\Eav\Model\Config; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\UnionExpression; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Entity attribute values resource + */ +class AttributeValue +{ + /** + * @var MetadataPool + */ + private $metadataPool; + /** + * @var ResourceConnection + */ + private $resourceConnection; + /** + * @var Config + */ + private $config; + + /** + * @param ResourceConnection $resourceConnection + * @param MetadataPool $metadataPool + * @param Config $config + */ + public function __construct( + ResourceConnection $resourceConnection, + MetadataPool $metadataPool, + Config $config + ) { + $this->resourceConnection = $resourceConnection; + $this->metadataPool = $metadataPool; + $this->config = $config; + } + + /** + * Get attribute values for given entity type, entity ID, attribute codes and store IDs + * + * @param string $entityType + * @param int $entityId + * @param string[] $attributeCodes + * @param int[] $storeIds + * @return array + */ + public function getValues( + string $entityType, + int $entityId, + array $attributeCodes = [], + array $storeIds = [] + ): array { + $metadata = $this->metadataPool->getMetadata($entityType); + $connection = $metadata->getEntityConnection(); + $selects = []; + $attributeTables = []; + $attributes = []; + $allAttributes = $this->getEntityAttributes($entityType); + $result = []; + if ($attributeCodes) { + foreach ($attributeCodes as $attributeCode) { + $attributes[$attributeCode] = $allAttributes[$attributeCode]; + } + } else { + $attributes = $allAttributes; + } + + foreach ($attributes as $attribute) { + if (!$attribute->isStatic()) { + $attributeTables[$attribute->getBackend()->getTable()][] = $attribute->getAttributeId(); + } + } + + if ($attributeTables) { + foreach ($attributeTables as $attributeTable => $attributeIds) { + $select = $connection->select() + ->from( + ['t' => $attributeTable], + ['*'] + ) + ->where($metadata->getLinkField() . ' = ?', $entityId) + ->where('attribute_id IN (?)', $attributeIds); + if (!empty($storeIds)) { + $select->where( + 'store_id IN (?)', + $storeIds + ); + } + $selects[] = $select; + } + + if (count($selects) > 1) { + $select = $connection->select(); + $select->from(['u' => new UnionExpression($selects, Select::SQL_UNION_ALL, '( %s )')]); + } else { + $select = reset($selects); + } + + $result = $connection->fetchAll($select); + } + + return $result; + } + + /** + * Delete attribute values + * + * @param string $entityType + * @param array[][] $values + * Format: + * array( + * 0 => array( + * value_id => 1, + * attribute_id => 11 + * ), + * 1 => array( + * value_id => 2, + * attribute_id => 22 + * ) + * ) + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function deleteValues(string $entityType, array $values): void + { + $metadata = $this->metadataPool->getMetadata($entityType); + $connection = $metadata->getEntityConnection(); + $attributeTables = []; + $allAttributes = []; + + foreach ($this->getEntityAttributes($entityType) as $attribute) { + $allAttributes[(int) $attribute->getAttributeId()] = $attribute; + } + + foreach ($values as $value) { + $attribute = $allAttributes[(int) $value['attribute_id']] ?? null; + if ($attribute && !$attribute->isStatic()) { + $attributeTables[$attribute->getBackend()->getTable()][] = (int) $value['value_id']; + } + } + + foreach ($attributeTables as $attributeTable => $valueIds) { + $connection->delete( + $attributeTable, + [ + 'value_id IN (?)' => $valueIds + ] + ); + } + } + + /** + * Get attribute of given entity type + * + * @param string $entityType + */ + private function getEntityAttributes(string $entityType) + { + $metadata = $this->metadataPool->getMetadata($entityType); + $eavEntityType = $metadata->getEavEntityType(); + return null === $eavEntityType ? [] : $this->config->getEntityAttributes($eavEntityType); + } +} diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index c8780341271ac..29cad62bf0ca4 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -114,6 +114,7 @@ public function loadByCode(AbstractModel $object, $entityTypeId, $code) if ($data) { $object->setData($data); + $object->setOrigData('entity_type_id', $object->getEntityTypeId()); $this->_afterLoad($object); return true; } @@ -204,6 +205,7 @@ protected function _beforeSave(AbstractModel $object) * @param AbstractModel $attribute * @return AbstractDb * @throws CouldNotDeleteException + * @since 102.0.2 */ protected function _beforeDelete(AbstractModel $attribute) { @@ -776,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 bf1405fa64122..e8c8d4c5190fe 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php @@ -71,7 +71,7 @@ public function __construct( * @param string $entityType * @return \Magento\Eav\Api\Data\AttributeInterface[] * @throws Exception if for unknown entity type - * @deprecated Not used anymore + * @deprecated 101.0.5 Not used anymore * @see ReadHandler::getEntityAttributes */ protected function getAttributes($entityType) @@ -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/Eav/Model/TypeLocator/ServiceClassLocator.php b/app/code/Magento/Eav/Model/TypeLocator/ServiceClassLocator.php index a4225b550ab10..7ffcf689c4381 100644 --- a/app/code/Magento/Eav/Model/TypeLocator/ServiceClassLocator.php +++ b/app/code/Magento/Eav/Model/TypeLocator/ServiceClassLocator.php @@ -14,7 +14,7 @@ /** * Class to find type based off of ServiceTypeToEntityTypeMap. This locator is introduced for backwards compatibility. - * @deprecated + * @deprecated 102.0.0 */ class ServiceClassLocator implements CustomAttributeTypeLocatorInterface { diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php index 15dcea077c887..7e434166a15be 100644 --- a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php +++ b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php @@ -23,12 +23,12 @@ class Data extends \Magento\Framework\Validator\AbstractValidator /** * @var array */ - protected $_attributesWhiteList = []; + protected $allowedAttributesList = []; /** * @var array */ - protected $_attributesBlackList = []; + protected $deniedAttributesList = []; /** * @var array @@ -68,9 +68,9 @@ public function setAttributes(array $attributes) * @param array $attributesCodes * @return $this */ - public function setAttributesWhiteList(array $attributesCodes) + public function setAllowedAttributesList(array $attributesCodes) { - $this->_attributesWhiteList = $attributesCodes; + $this->allowedAttributesList = $attributesCodes; return $this; } @@ -82,9 +82,9 @@ public function setAttributesWhiteList(array $attributesCodes) * @param array $attributesCodes * @return $this */ - public function setAttributesBlackList(array $attributesCodes) + public function setDeniedAttributesList(array $attributesCodes) { - $this->_attributesBlackList = $attributesCodes; + $this->deniedAttributesList = $attributesCodes; return $this; } @@ -171,11 +171,11 @@ protected function _getAttributes($entity) $attributesCodes[] = $attributeCode; } - $ignoreAttributes = $this->_attributesBlackList; - if ($this->_attributesWhiteList) { + $ignoreAttributes = $this->deniedAttributesList; + if ($this->allowedAttributesList) { $ignoreAttributes = array_merge( $ignoreAttributes, - array_diff($attributesCodes, $this->_attributesWhiteList) + array_diff($attributesCodes, $this->allowedAttributesList) ); } diff --git a/app/code/Magento/Eav/Setup/EavSetup.php b/app/code/Magento/Eav/Setup/EavSetup.php index d440a84fc8e65..96c7b86a8682d 100644 --- a/app/code/Magento/Eav/Setup/EavSetup.php +++ b/app/code/Magento/Eav/Setup/EavSetup.php @@ -125,7 +125,7 @@ public function __construct( /** * Gets setup model. * - * @deprecated + * @deprecated 102.0.0 * @return ModuleDataSetupInterface */ public function getSetup() diff --git a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/FileTest.php b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/FileTest.php index ccea3ea4ab950..3cdb30ec2606f 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/FileTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/FileTest.php @@ -11,15 +11,21 @@ use Magento\Eav\Model\Attribute\Data\File; use Magento\Eav\Model\AttributeDataFactory; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Io\File as FileIo; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\Url\EncoderInterface; use Magento\MediaStorage\Model\File\Validator\NotProtectedExtension; +use Psr\Log\LoggerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; +/** + * Test for Magento\Eav\Model\Attribute\Data\File class. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class FileTest extends TestCase { /** @@ -37,6 +43,9 @@ class FileTest extends TestCase */ protected $fileValidatorMock; + /** + * @inheritDoc + */ protected function setUp(): void { $timezoneMock = $this->getMockForAbstractClass(TimezoneInterface::class); @@ -48,6 +57,7 @@ protected function setUp(): void ['isValid', 'getMessages'] ); $filesystemMock = $this->createMock(Filesystem::class); + $fileIo = $this->createMock(FileIo::class); $this->model = new File( $timezoneMock, @@ -55,7 +65,8 @@ protected function setUp(): void $localeResolverMock, $this->urlEncoder, $this->fileValidatorMock, - $filesystemMock + $filesystemMock, + $fileIo ); } diff --git a/app/code/Magento/Eav/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Eav/Test/Unit/Model/ConfigTest.php index e4a0e935b325d..83fb1253aba96 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/ConfigTest.php @@ -223,11 +223,13 @@ public function testGetAttributes($cacheEnabled) ->method('getData') ->willReturn([$attributeData]); $entityAttributeMock = $this->getMockBuilder(Attribute::class) - ->setMethods(['setData', 'load', 'toArray']) + ->setMethods(['setData', 'setOrigData', 'load', 'toArray']) ->disableOriginalConstructor() ->getMock(); $entityAttributeMock->method('setData') ->willReturnSelf(); + $entityAttributeMock->method('setOrigData') + ->willReturn($attributeData); $entityAttributeMock->method('load') ->willReturnSelf(); $entityAttributeMock->method('toArray') diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php index 2084db08a1afb..b96b1e26696cd 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php @@ -15,10 +15,17 @@ use Magento\Eav\Model\Entity\Attribute\Source\SourceInterface; use Magento\Eav\Model\Entity\Attribute\Source\Table as EavAttributeSource; use Magento\Eav\Model\ResourceModel\Entity\Attribute; -use Magento\Framework\Model\AbstractModel; -use PHPUnit\Framework\MockObject\MockObject; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\StateException; +use PHPUnit\Framework\MockObject\MockObject as MockObject; use PHPUnit\Framework\TestCase; +/** + * Tests for Eav Option Management functionality + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class OptionManagementTest extends TestCase { /** @@ -27,15 +34,18 @@ class OptionManagementTest extends TestCase protected $model; /** - * @var \PHPUnit\Framework\MockObject\MockObject + * @var MockObject|AttributeRepository */ protected $attributeRepositoryMock; /** - * @var \PHPUnit\Framework\MockObject\MockObject + * @var MockObject|Attribute */ protected $resourceModelMock; + /** + * @inheritdoc + */ protected function setUp(): void { $this->attributeRepositoryMock = $this->createMock(AttributeRepository::class); @@ -47,124 +57,189 @@ protected function setUp(): void ); } + /** + * Test to add attribute option + */ public function testAdd() { $entityType = 42; + $storeId = 4; $attributeCode = 'atrCde'; - $attributeMock = $this->getAttribute(); - $optionMock = $this->getAttributeOption(); - $labelMock = $this->getAttributeOptionLabel(); - $option = - ['value' => [ + $label = 'optionLabel'; + $storeLabel = 'labelLabel'; + $sortOder = 'optionSortOrder'; + $option = [ + 'value' => [ 'id_new_option' => [ - 0 => 'optionLabel', - 42 => 'labelLabel', + 0 => $label, + $storeId => $storeLabel, ], ], - 'order' => [ - 'id_new_option' => 'optionSortOrder', - ], - ]; + 'order' => [ + 'id_new_option' => $sortOder, + ] + ]; + $newOptionId = 10; - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) - ->willReturn($attributeMock); - $attributeMock->expects($this->once())->method('usesSource')->willReturn(true); - $optionMock->expects($this->once())->method('getLabel')->willReturn('optionLabel'); - $optionMock->expects($this->once())->method('getSortOrder')->willReturn('optionSortOrder'); - $optionMock->expects($this->exactly(2))->method('getStoreLabels')->willReturn([$labelMock]); - $labelMock->expects($this->once())->method('getStoreId')->willReturn(42); - $labelMock->expects($this->once())->method('getLabel')->willReturn('labelLabel'); - $optionMock->expects($this->once())->method('getIsDefault')->willReturn(true); + $optionMock = $this->getAttributeOption(); + $labelMock = $this->getAttributeOptionLabel(); + /** @var SourceInterface|MockObject $sourceMock */ + $sourceMock = $this->createMock(EavAttributeSource::class); + $sourceMock->method('getOptionId') + ->willReturnMap( + [ + [$label, null], + [$storeLabel, $newOptionId], + [$newOptionId, $newOptionId], + ] + ); + + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['setDefault', 'setOption']) + ->onlyMethods(['usesSource', 'getSource']) + ->getMock(); + $attributeMock->method('usesSource')->willReturn(true); $attributeMock->expects($this->once())->method('setDefault')->with(['id_new_option']); $attributeMock->expects($this->once())->method('setOption')->with($option); + $attributeMock->method('getSource')->willReturn($sourceMock); + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) + ->willReturn($attributeMock); + $optionMock->method('getLabel')->willReturn($label); + $optionMock->method('getSortOrder')->willReturn($sortOder); + $optionMock->method('getIsDefault')->willReturn(true); + $optionMock->method('getStoreLabels')->willReturn([$labelMock]); + $labelMock->method('getStoreId')->willReturn($storeId); + $labelMock->method('getLabel')->willReturn($storeLabel); $this->resourceModelMock->expects($this->once())->method('save')->with($attributeMock); - $this->assertEquals('id_new_option', $this->model->add($entityType, $attributeCode, $optionMock)); + $this->assertEquals( + $newOptionId, + $this->model->add($entityType, $attributeCode, $optionMock) + ); } + /** + * Test to add attribute option with empty attribute code + */ public function testAddWithEmptyAttributeCode() { - $this->expectException('Magento\Framework\Exception\InputException'); - $this->expectExceptionMessage('The attribute code is empty. Enter the code and try again.'); + $this->expectExceptionMessage("The attribute code is empty. Enter the code and try again."); + $this->expectException(InputException::class); $entityType = 42; $attributeCode = ''; $optionMock = $this->getAttributeOption(); $this->resourceModelMock->expects($this->never())->method('save'); $this->model->add($entityType, $attributeCode, $optionMock); } - + /** + * Test to add attribute option without use source + */ public function testAddWithWrongOptions() { - $this->expectException('Magento\Framework\Exception\StateException'); $this->expectExceptionMessage('The "testAttribute" attribute doesn\'t work with options.'); + $this->expectException(StateException::class); $entityType = 42; $attributeCode = 'testAttribute'; - $attributeMock = $this->getAttribute(); + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['setDefault', 'setOption', 'setStoreId']) + ->onlyMethods(['usesSource', 'getSource']) + ->getMock(); $optionMock = $this->getAttributeOption(); - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); $attributeMock->expects($this->once())->method('usesSource')->willReturn(false); $this->resourceModelMock->expects($this->never())->method('save'); $this->model->add($entityType, $attributeCode, $optionMock); } + /** + * Test to add attribute option wit save exception + */ public function testAddWithCannotSaveException() { - $this->expectException('Magento\Framework\Exception\StateException'); + $this->expectException(StateException::class); $this->expectExceptionMessage('The "atrCde" attribute can\'t be saved.'); + $entityType = 42; + $storeId = 4; $attributeCode = 'atrCde'; - $optionMock = $this->getAttributeOption(); - $attributeMock = $this->getAttribute(); - $labelMock = $this->getAttributeOptionLabel(); - $option = - ['value' => [ + $label = 'optionLabel'; + $storeLabel = 'labelLabel'; + $sortOder = 'optionSortOrder'; + $option = [ + 'value' => [ 'id_new_option' => [ - 0 => 'optionLabel', - 42 => 'labelLabel', + 0 => $label, + $storeId => $storeLabel, ], ], - 'order' => [ - 'id_new_option' => 'optionSortOrder', - ], - ]; + 'order' => [ + 'id_new_option' => $sortOder, + ] + ]; - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) - ->willReturn($attributeMock); - $attributeMock->expects($this->once())->method('usesSource')->willReturn(true); - $optionMock->expects($this->once())->method('getLabel')->willReturn('optionLabel'); - $optionMock->expects($this->once())->method('getSortOrder')->willReturn('optionSortOrder'); - $optionMock->expects($this->exactly(2))->method('getStoreLabels')->willReturn([$labelMock]); - $labelMock->expects($this->once())->method('getStoreId')->willReturn(42); - $labelMock->expects($this->once())->method('getLabel')->willReturn('labelLabel'); - $optionMock->expects($this->once())->method('getIsDefault')->willReturn(true); + $optionMock = $this->getAttributeOption(); + $labelMock = $this->getAttributeOptionLabel(); + /** @var SourceInterface|MockObject $sourceMock */ + $sourceMock = $this->createMock(EavAttributeSource::class); + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['setDefault', 'setOption', 'setStoreId']) + ->onlyMethods(['usesSource', 'getSource', 'getAttributeCode']) + ->getMock(); + $attributeMock->method('usesSource')->willReturn(true); $attributeMock->expects($this->once())->method('setDefault')->with(['id_new_option']); $attributeMock->expects($this->once())->method('setOption')->with($option); + $attributeMock->method('getSource')->willReturn($sourceMock); + $attributeMock->method('getAttributeCode')->willReturn($attributeCode); + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) + ->willReturn($attributeMock); + $optionMock->method('getLabel')->willReturn($label); + $optionMock->method('getSortOrder')->willReturn($sortOder); + $optionMock->method('getIsDefault')->willReturn(true); + $optionMock->method('getStoreLabels')->willReturn([$labelMock]); + $labelMock->method('getStoreId')->willReturn($storeId); + $labelMock->method('getLabel')->willReturn($storeLabel); + $this->resourceModelMock->expects($this->once())->method('save')->with($attributeMock) ->willThrowException(new \Exception()); $this->model->add($entityType, $attributeCode, $optionMock); } + /** + * Test to delete attribute option + */ public function testDelete() { $entityType = 42; $attributeCode = 'atrCode'; $optionId = 'option'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'getSource', 'getId', 'getOptionText', 'addData'] - ); + + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['getOptionText']) + ->onlyMethods(['usesSource', 'getSource', 'getId', 'addData']) + ->getMock(); $removalMarker = [ 'option' => [ 'value' => [$optionId => []], 'delete' => [$optionId => '1'], ], ]; - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); $attributeMock->expects($this->once())->method('usesSource')->willReturn(true); $attributeMock->expects($this->once())->method('getSource')->willReturnSelf(); @@ -175,22 +250,23 @@ public function testDelete() $this->assertTrue($this->model->delete($entityType, $attributeCode, $optionId)); } + /** + * Test to delete attribute option with save exception + */ public function testDeleteWithCannotSaveException() { - $this->expectException('Magento\Framework\Exception\StateException'); $this->expectExceptionMessage('The "atrCode" attribute can\'t be saved.'); + $this->expectException(StateException::class); + $entityType = 42; $attributeCode = 'atrCode'; $optionId = 'option'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'getSource', 'getId', 'getOptionText', 'addData'] - ); + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['getOptionText']) + ->onlyMethods(['usesSource', 'getSource', 'getId', 'addData']) + ->getMock(); $removalMarker = [ 'option' => [ 'value' => [$optionId => []], @@ -204,28 +280,29 @@ public function testDeleteWithCannotSaveException() $attributeMock->expects($this->once())->method('getOptionText')->willReturn('optionText'); $attributeMock->expects($this->never())->method('getId'); $attributeMock->expects($this->once())->method('addData')->with($removalMarker); - $this->resourceModelMock->expects($this->once())->method('save')->with($attributeMock) + $this->resourceModelMock->expects($this->once()) + ->method('save') + ->with($attributeMock) ->willThrowException(new \Exception()); $this->model->delete($entityType, $attributeCode, $optionId); } + /** + * Test to delete with wrong option + */ public function testDeleteWithWrongOption() { - $this->expectException('Magento\Framework\Exception\NoSuchEntityException'); $this->expectExceptionMessage('The "atrCode" attribute doesn\'t include an option with "option" ID.'); + $this->expectException(NoSuchEntityException::class); + $entityType = 42; $attributeCode = 'atrCode'; $optionId = 'option'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'getSource', 'getAttributeCode'] - ); - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->createMock(EavAbstractAttribute::class); + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); $sourceMock = $this->getMockForAbstractClass(SourceInterface::class); $sourceMock->expects($this->once())->method('getOptionText')->willReturn(false); @@ -236,33 +313,40 @@ public function testDeleteWithWrongOption() $this->model->delete($entityType, $attributeCode, $optionId); } + /** + * Test to delete with absent option + */ public function testDeleteWithAbsentOption() { - $this->expectException('Magento\Framework\Exception\StateException'); - $this->expectExceptionMessage('The "atrCode" attribute has no option.'); + $this->expectExceptionMessage('The "atrCode" attribute doesn\'t work with options.'); + $this->expectException(StateException::class); + $entityType = 42; $attributeCode = 'atrCode'; $optionId = 'option'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'getSource', 'getId', 'getOptionText', 'addData'] - ); - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['getOptionText']) + ->onlyMethods(['usesSource', 'getSource', 'getId', 'addData']) + ->getMock(); + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); $attributeMock->expects($this->once())->method('usesSource')->willReturn(false); $this->resourceModelMock->expects($this->never())->method('save'); $this->model->delete($entityType, $attributeCode, $optionId); } + /** + * Test to delete with empty attribute code + */ public function testDeleteWithEmptyAttributeCode() { - $this->expectException('Magento\Framework\Exception\InputException'); - $this->expectExceptionMessage('The attribute code is empty. Enter the code and try again.'); + $this->expectExceptionMessage("The attribute code is empty. Enter the code and try again."); + $this->expectException(InputException::class); + $entityType = 42; $attributeCode = ''; $optionId = 'option'; @@ -270,86 +354,56 @@ public function testDeleteWithEmptyAttributeCode() $this->model->delete($entityType, $attributeCode, $optionId); } + /** + * Test to get items + */ public function testGetItems() { $entityType = 42; $attributeCode = 'atrCode'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['getOptions'] - ); - $optionsMock = [$this->getMockForAbstractClass(EavAttributeOptionInterface::class)]; - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + $attributeMock = $this->createMock(EavAbstractAttribute::class); + $optionsMock = [$this->createMock(EavAttributeOptionInterface::class)]; + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); $attributeMock->expects($this->once())->method('getOptions')->willReturn($optionsMock); $this->assertEquals($optionsMock, $this->model->getItems($entityType, $attributeCode)); } + /** + * Test to get items with load exception + */ public function testGetItemsWithCannotLoadException() { - $this->expectException('Magento\Framework\Exception\StateException'); $this->expectExceptionMessage('The options for "atrCode" attribute can\'t be loaded.'); + $this->expectException(StateException::class); $entityType = 42; $attributeCode = 'atrCode'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['getOptions'] - ); - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + $attributeMock = $this->createMock(EavAbstractAttribute::class); + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); - $attributeMock->expects($this->once())->method('getOptions')->willThrowException(new \Exception()); + $attributeMock->expects($this->once()) + ->method('getOptions') + ->willThrowException(new \Exception()); $this->model->getItems($entityType, $attributeCode); } + /** + * Test to get items with empty attribute code + */ public function testGetItemsWithEmptyAttributeCode() { - $this->expectException('Magento\Framework\Exception\InputException'); - $this->expectExceptionMessage('The attribute code is empty. Enter the code and try again.'); + $this->expectExceptionMessage("The attribute code is empty. Enter the code and try again."); + $this->expectException(InputException::class); + $entityType = 42; $attributeCode = ''; $this->model->getItems($entityType, $attributeCode); } - /** - * Returns attribute entity mock. - * - * @param array $attributeOptions attribute options for return - * @return MockObject|EavAbstractAttribute - */ - private function getAttribute(array $attributeOptions = []) - { - $attribute = $this->getMockBuilder(EavAbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods( - [ - 'usesSource', - 'setDefault', - 'setOption', - 'setStoreId', - 'getSource', - ] - ) - ->getMock(); - $source = $this->getMockBuilder(EavAttributeSource::class) - ->disableOriginalConstructor() - ->getMock(); - - $attribute->method('getSource')->willReturn($source); - $source->method('toOptionArray')->willReturn($attributeOptions); - - return $attribute; - } - /** * Return attribute option entity mock. * diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php index 774b968f1b697..a8ecbb8371ac9 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php @@ -249,10 +249,10 @@ public function testIsValidAttributesFromCollection() } /** - * @dataProvider whiteBlackListProvider + * @dataProvider allowDenyListProvider * @param callable $callback */ - public function testIsValidBlackListWhiteListChecks($callback) + public function testIsValidExclusionInclusionListChecks($callback) { $attribute = $this->_getAttributeMock( [ @@ -302,19 +302,19 @@ public function testIsValidBlackListWhiteListChecks($callback) /** * @return array */ - public function whiteBlackListProvider() + public function allowDenyListProvider() { - $whiteCallback = function ($validator) { - $validator->setAttributesWhiteList(['attribute']); + $allowedCallbackList = function ($validator) { + $validator->setAllowedAttributesList(['attribute']); }; - $blackCallback = function ($validator) { - $validator->setAttributesBlackList(['attribute2']); + $deniedCallbackList = function ($validator) { + $validator->setDeniedAttributesList(['attribute2']); }; - return ['white_list' => [$whiteCallback], 'black_list' => [$blackCallback]]; + return ['allowed' => [$allowedCallbackList], 'denied' => [$deniedCallbackList]]; } - public function testSetAttributesWhiteList() + public function testSetAttributesAllowedList() { $this->markTestSkipped('Skipped in #27500 due to testing protected/private methods and properties'); @@ -328,12 +328,14 @@ public function testSetAttributesWhiteList() ) ->getMock(); $validator = new Data($attrDataFactory); - $result = $validator->setAttributesWhiteList($attributes); - $this->assertAttributeEquals($attributes, '_attributesWhiteList', $validator); + $result = $validator->setIncludedAttributesList($attributes); + + // phpstan:ignore + $this->assertAttributeEquals($attributes, '_attributesAllowed', $validator); $this->assertEquals($validator, $result); } - public function testSetAttributesBlackList() + public function testSetAttributesDeniedList() { $this->markTestSkipped('Skipped in #27500 due to testing protected/private methods and properties'); @@ -347,8 +349,9 @@ public function testSetAttributesBlackList() ) ->getMock(); $validator = new Data($attrDataFactory); - $result = $validator->setAttributesBlackList($attributes); - $this->assertAttributeEquals($attributes, '_attributesBlackList', $validator); + $result = $validator->setDeniedAttributesList($attributes); + // phpstan:ignore + $this->assertAttributeEquals($attributes, '_attributesDenied', $validator); $this->assertEquals($validator, $result); } diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index 21f248f1b1094..4f5d7d7112961 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -20,6 +20,7 @@ <preference for="Magento\Eav\Api\Data\AttributeFrontendLabelInterface" type="Magento\Eav\Model\Entity\Attribute\FrontendLabel" /> <preference for="Magento\Eav\Api\Data\AttributeOptionInterface" type="Magento\Eav\Model\Entity\Attribute\Option" /> <preference for="Magento\Eav\Api\AttributeOptionManagementInterface" type="Magento\Eav\Model\Entity\Attribute\OptionManagement" /> + <preference for="Magento\Eav\Api\AttributeOptionUpdateInterface" type="Magento\Eav\Model\Entity\Attribute\OptionManagement" /> <preference for="Magento\Eav\Api\Data\AttributeOptionLabelInterface" type="Magento\Eav\Model\Entity\Attribute\OptionLabel" /> <preference for="Magento\Eav\Api\Data\AttributeValidationRuleInterface" type="Magento\Eav\Model\Entity\Attribute\ValidationRule" /> <preference for="Magento\Eav\Api\Data\AttributeSearchResultsInterface" type="Magento\Eav\Model\AttributeSearchResults" /> diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php b/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php index ef21a26f1f62e..7ee87681dc630 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php @@ -15,6 +15,7 @@ * Translate type names found by the custom type locator to GraphQL type names. * * @api + * @since 100.3.0 */ class Type { @@ -55,6 +56,7 @@ public function __construct( * @param string $entityType * @return string * @throws GraphQlInputException + * @since 100.3.0 */ public function getType(string $attributeCode, string $entityType) : string { diff --git a/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php b/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php index 2b2da7522dfa6..41a5edc900af8 100644 --- a/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php +++ b/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php @@ -8,7 +8,7 @@ /** * Elasticsearch 5x test connection block * @codeCoverageIgnore - * @deprecated because of EOL for Elasticsearch5 + * @deprecated 100.3.5 because of EOL for Elasticsearch5 */ class TestConnection extends \Magento\AdvancedSearch\Block\Adminhtml\System\Config\TestConnection { diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php index 1f6e05c9e02fc..8576d8df0cc95 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -19,7 +19,7 @@ class Converter implements ConverterInterface */ private const ES_DATA_TYPE_TEXT = 'text'; private const ES_DATA_TYPE_KEYWORD = 'keyword'; - private const ES_DATA_TYPE_FLOAT = 'float'; + private const ES_DATA_TYPE_DOUBLE = 'double'; private const ES_DATA_TYPE_INT = 'integer'; private const ES_DATA_TYPE_DATE = 'date'; /**#@-*/ @@ -32,7 +32,7 @@ class Converter implements ConverterInterface private $mapping = [ self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_TEXT, self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_KEYWORD, - self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE, self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, ]; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php index b912446acd63e..840a4e16e8ab2 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php @@ -50,7 +50,6 @@ private function getProductFieldMapper() * @param string $attributeCode * @param array $context * @return string - * @since 100.1.0 */ public function getFieldName($attributeCode, $context = []) { @@ -62,7 +61,6 @@ public function getFieldName($attributeCode, $context = []) * * @param array $context * @return array - * @since 100.1.0 */ public function getAllAttributesTypes($context = []) { diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php index bd9a380230652..2560d7e26e7d9 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Elasticsearch5\Model\Client; use Magento\Framework\Exception\LocalizedException; @@ -11,7 +12,7 @@ /** * Elasticsearch client * - * @deprecated the Elasticsearch 5 doesn't supported due to EOL + * @deprecated 100.3.5 the Elasticsearch 5 doesn't supported due to EOL */ class Elasticsearch implements ClientInterface { @@ -48,8 +49,10 @@ public function __construct( $options = [], $elasticsearchClient = null ) { - if (empty($options['hostname']) || ((!empty($options['enableAuth']) && - ($options['enableAuth'] == 1)) && (empty($options['username']) || empty($options['password'])))) { + if (empty($options['hostname']) + || ((!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) + && (empty($options['username']) || empty($options['password']))) + ) { throw new LocalizedException( __('The search failed because of a search engine misconfiguration.') ); @@ -163,6 +166,23 @@ public function createIndex($index, $settings) ); } + /** + * Add/update an Elasticsearch index settings. + * + * @param string $index + * @param array $settings + * @return void + */ + public function putIndexSettings(string $index, array $settings): void + { + $this->getClient()->indices()->putSettings( + [ + 'index' => $index, + 'body' => $settings, + ] + ); + } + /** * Delete an Elasticsearch index. * @@ -276,7 +296,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -302,7 +322,15 @@ public function addFieldsMapping(array $fields, $index, $entityType) ] ), ], - ] + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], ], ], ], @@ -323,7 +351,6 @@ public function addFieldsMapping(array $fields, $index, $entityType) */ private function prepareFieldInfo($fieldInfo) { - if (strcmp($this->getServerVersion(), '5') < 0) { if ($fieldInfo['type'] == 'keyword') { $fieldInfo['type'] = 'string'; @@ -338,6 +365,17 @@ private function prepareFieldInfo($fieldInfo) return $fieldInfo; } + /** + * Get mapping from Elasticsearch index. + * + * @param array $params + * @return array + */ + public function getMapping(array $params): array + { + return $this->getClient()->indices()->getMapping($params); + } + /** * Delete mapping in Elasticsearch index * diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php index abd27abdac8a7..9db1375f16c71 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php @@ -17,25 +17,25 @@ /** * Mapper class * @api - * @since 100.1.0 + * @since 100.2.2 */ class Mapper { /** * @var QueryBuilder - * @since 100.1.0 + * @since 100.2.2 */ protected $queryBuilder; /** * @var MatchQueryBuilder - * @since 100.1.0 + * @since 100.2.2 */ protected $matchQueryBuilder; /** * @var FilterBuilder - * @since 100.1.0 + * @since 100.2.2 */ protected $filterBuilder; @@ -59,7 +59,7 @@ public function __construct( * * @param RequestInterface $request * @return array - * @since 100.1.0 + * @since 100.2.2 */ public function buildQuery(RequestInterface $request) { @@ -89,7 +89,7 @@ public function buildQuery(RequestInterface $request) * @param string $conditionType * @return array * @throws \InvalidArgumentException - * @since 100.1.0 + * @since 100.2.2 */ protected function processQuery( RequestQueryInterface $requestQuery, @@ -126,7 +126,7 @@ protected function processQuery( * @param BoolQuery $query * @param array $selectQuery * @return array - * @since 100.1.0 + * @since 100.2.2 */ protected function processBoolQuery( BoolQuery $query, @@ -160,7 +160,7 @@ protected function processBoolQuery( * @param array $selectQuery * @param string $conditionType * @return array - * @since 100.1.0 + * @since 100.2.2 */ protected function processBoolQueryCondition( array $subQueryList, diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php index b75621191dae7..ac99c91dcfac1 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php @@ -18,31 +18,31 @@ * Query builder for search adapter. * * @api - * @since 100.1.0 + * @since 100.2.2 */ class Builder { /** * @var Config - * @since 100.1.0 + * @since 100.2.2 */ protected $clientConfig; /** * @var SearchIndexNameResolver - * @since 100.1.0 + * @since 100.2.2 */ protected $searchIndexNameResolver; /** * @var AggregationBuilder - * @since 100.1.0 + * @since 100.2.2 */ protected $aggregationBuilder; /** * @var ScopeResolverInterface - * @since 100.1.0 + * @since 100.2.2 */ protected $scopeResolver; @@ -77,7 +77,7 @@ public function __construct( * * @param RequestInterface $request * @return array - * @since 100.1.0 + * @since 100.2.2 */ public function initQuery(RequestInterface $request) { @@ -104,7 +104,7 @@ public function initQuery(RequestInterface $request) * @param RequestInterface $request * @param array $searchQuery * @return array - * @since 100.1.0 + * @since 100.2.2 */ public function initAggregations( RequestInterface $request, diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index 7f0ecf899e51c..9fa001097df87 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Model\Adapter\BatchDataMapper; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; @@ -74,7 +75,7 @@ class ProductDataMapper implements BatchDataMapperInterface private $attributesExcludedFromMerge = [ 'status', 'visibility', - 'tax_class_id' + 'tax_class_id', ]; /** @@ -85,8 +86,11 @@ class ProductDataMapper implements BatchDataMapperInterface ]; /** - * Construction for DocumentDataMapper - * + * @var string[] + */ + private $filterableAttributeTypes; + + /** * @param Builder $builder * @param FieldMapperInterface $fieldMapper * @param DateFieldType $dateFieldType @@ -94,6 +98,7 @@ class ProductDataMapper implements BatchDataMapperInterface * @param DataProvider $dataProvider * @param array $excludedAttributes * @param array $sortableAttributesValuesToImplode + * @param array $filterableAttributeTypes */ public function __construct( Builder $builder, @@ -102,7 +107,8 @@ public function __construct( AdditionalFieldsProviderInterface $additionalFieldsProvider, DataProvider $dataProvider, array $excludedAttributes = [], - array $sortableAttributesValuesToImplode = [] + array $sortableAttributesValuesToImplode = [], + array $filterableAttributeTypes = [] ) { $this->builder = $builder; $this->fieldMapper = $fieldMapper; @@ -115,6 +121,7 @@ public function __construct( $this->additionalFieldsProvider = $additionalFieldsProvider; $this->dataProvider = $dataProvider; $this->attributeOptionsCache = []; + $this->filterableAttributeTypes = $filterableAttributeTypes; } /** @@ -209,10 +216,10 @@ private function convertAttribute(Attribute $attribute, array $attributeValues, $productAttributes = []; $retrievedValue = $this->retrieveFieldValue($attributeValues); - if ($retrievedValue) { + if ($retrievedValue !== null) { $productAttributes[$attribute->getAttributeCode()] = $retrievedValue; - if ($attribute->getIsSearchable()) { + if ($this->isAttributeLabelsShouldBeMapped($attribute)) { $attributeLabels = $this->getValuesLabels($attribute, $attributeValues, $storeId); $retrievedLabel = $this->retrieveFieldValue($attributeLabels); if ($retrievedLabel) { @@ -224,6 +231,26 @@ private function convertAttribute(Attribute $attribute, array $attributeValues, return $productAttributes; } + /** + * Check if an attribute has one of the next storefront properties enabled for mapping labels: + * - "Use in Search" (is_searchable) + * - "Visible in Advanced Search" (is_visible_in_advanced_search) + * - "Use in Layered Navigation" (is_filterable) + * - "Use in Search Results Layered Navigation" (is_filterable_in_search) + * + * @param Attribute $attribute + * @return bool + */ + private function isAttributeLabelsShouldBeMapped(Attribute $attribute): bool + { + return ( + $attribute->getIsSearchable() + || $attribute->getIsVisibleInAdvancedSearch() + || $attribute->getIsFilterable() + || $attribute->getIsFilterableInSearch() + ); + } + /** * Prepare attribute values. * @@ -249,6 +276,15 @@ private function prepareAttributeValues( $attributeValues = $this->prepareMultiselectValues($attributeValues); } + if (in_array($attribute->getFrontendInput(), $this->filterableAttributeTypes)) { + $attributeValues = array_map( + function (string $valueId) { + return (int)$valueId; + }, + $attributeValues + ); + } + if ($this->isAttributeDate($attribute)) { foreach ($attributeValues as $key => $attributeValue) { $attributeValues[$key] = $this->dateFieldType->formatDate($storeId, $attributeValue); @@ -354,7 +390,7 @@ private function getAttributeOptions(Attribute $attribute, int $storeId): array */ private function retrieveFieldValue(array $values) { - $values = \array_filter(\array_unique($values)); + $values = \array_unique($values); return count($values) === 1 ? \array_shift($values) : \array_values($values); } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php index 5ab6669a34cc4..261f8d84b5baa 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php @@ -6,6 +6,12 @@ namespace Magento\Elasticsearch\Model\Adapter; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Stdlib\ArrayManager; + /** * Elasticsearch adapter * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -69,6 +75,31 @@ class Elasticsearch */ private $batchDocumentDataMapper; + /** + * @var array + */ + private $mappedAttributes = []; + + /** + * @var string[] + */ + private $indexByCode = []; + + /** + * @var ProductAttributeRepositoryInterface + */ + private $productAttributeRepository; + + /** + * @var StaticField + */ + private $staticFieldProvider; + + /** + * @var ArrayManager + */ + private $arrayManager; + /** * @param \Magento\Elasticsearch\SearchAdapter\ConnectionManager $connectionManager * @param FieldMapperInterface $fieldMapper @@ -78,7 +109,11 @@ class Elasticsearch * @param Index\IndexNameResolver $indexNameResolver * @param BatchDataMapperInterface $batchDocumentDataMapper * @param array $options + * @param ProductAttributeRepositoryInterface|null $productAttributeRepository + * @param StaticField|null $staticFieldProvider + * @param ArrayManager|null $arrayManager * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Elasticsearch\SearchAdapter\ConnectionManager $connectionManager, @@ -88,7 +123,10 @@ public function __construct( \Psr\Log\LoggerInterface $logger, \Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver $indexNameResolver, BatchDataMapperInterface $batchDocumentDataMapper, - $options = [] + $options = [], + ProductAttributeRepositoryInterface $productAttributeRepository = null, + StaticField $staticFieldProvider = null, + ArrayManager $arrayManager = null ) { $this->connectionManager = $connectionManager; $this->fieldMapper = $fieldMapper; @@ -97,6 +135,12 @@ public function __construct( $this->logger = $logger; $this->indexNameResolver = $indexNameResolver; $this->batchDocumentDataMapper = $batchDocumentDataMapper; + $this->productAttributeRepository = $productAttributeRepository ?: + ObjectManager::getInstance()->get(ProductAttributeRepositoryInterface::class); + $this->staticFieldProvider = $staticFieldProvider ?: + ObjectManager::getInstance()->get(StaticField::class); + $this->arrayManager = $arrayManager ?: + ObjectManager::getInstance()->get(ArrayManager::class); try { $this->client = $this->connectionManager->getConnection($options); @@ -322,6 +366,93 @@ public function updateAlias($storeId, $mappedIndexerId) // remove obsolete index if ($oldIndex) { $this->client->deleteIndex($oldIndex); + unset($this->indexByCode[$mappedIndexerId . '_' . $storeId]); + } + + return $this; + } + + /** + * Update Elasticsearch mapping for index. + * + * @param int $storeId + * @param string $mappedIndexerId + * @param string $attributeCode + * @return $this + */ + public function updateIndexMapping(int $storeId, string $mappedIndexerId, string $attributeCode): self + { + $indexName = $this->getIndexFromAlias($storeId, $mappedIndexerId); + if (empty($indexName)) { + return $this; + } + + $attribute = $this->productAttributeRepository->get($attributeCode); + $newAttributeMapping = $this->staticFieldProvider->getField($attribute); + $mappedAttributes = $this->getMappedAttributes($indexName); + + $attrToUpdate = array_diff_key($newAttributeMapping, $mappedAttributes); + if (!empty($attrToUpdate)) { + $settings['index']['mapping']['total_fields']['limit'] = $this + ->getMappingTotalFieldsLimit(array_merge($mappedAttributes, $attrToUpdate)); + $this->client->putIndexSettings($indexName, ['settings' => $settings]); + + $this->client->addFieldsMapping( + $attrToUpdate, + $indexName, + $this->clientConfig->getEntityType() + ); + $this->setMappedAttributes($indexName, $attrToUpdate); + } + + return $this; + } + + /** + * Retrieve index definition from class. + * + * @param int $storeId + * @param string $mappedIndexerId + * @return string + */ + private function getIndexFromAlias(int $storeId, string $mappedIndexerId): string + { + $indexCode = $mappedIndexerId . '_' . $storeId; + if (!isset($this->indexByCode[$indexCode])) { + $this->indexByCode[$indexCode] = $this->indexNameResolver->getIndexFromAlias($storeId, $mappedIndexerId); + } + + return $this->indexByCode[$indexCode]; + } + + /** + * Retrieve mapped attributes from class. + * + * @param string $indexName + * @return array + */ + private function getMappedAttributes(string $indexName): array + { + if (empty($this->mappedAttributes[$indexName])) { + $mappedAttributes = $this->client->getMapping(['index' => $indexName]); + $pathField = $this->arrayManager->findPath('properties', $mappedAttributes); + $this->mappedAttributes[$indexName] = $this->arrayManager->get($pathField, $mappedAttributes, []); + } + + return $this->mappedAttributes[$indexName]; + } + + /** + * Set mapped attributes to class. + * + * @param string $indexName + * @param array $mappedAttributes + * @return $this + */ + private function setMappedAttributes(string $indexName, array $mappedAttributes): self + { + foreach ($mappedAttributes as $attributeCode => $attributeParams) { + $this->mappedAttributes[$indexName][$attributeCode] = $attributeParams; } return $this; @@ -366,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/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php index 88dab83698794..2067dcdc7fe9f 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -16,7 +16,7 @@ class Converter implements ConverterInterface * Text flags for Elasticsearch field types */ private const ES_DATA_TYPE_STRING = 'string'; - private const ES_DATA_TYPE_FLOAT = 'float'; + private const ES_DATA_TYPE_DOUBLE = 'double'; private const ES_DATA_TYPE_INT = 'integer'; private const ES_DATA_TYPE_DATE = 'date'; /**#@-*/ @@ -29,7 +29,7 @@ class Converter implements ConverterInterface private $mapping = [ self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_STRING, self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_STRING, - self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE, self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, ]; diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php index 3e7c3e9b592bd..a1563f75e6607 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php @@ -10,6 +10,7 @@ /** * @api * Field type converter from internal data types to elastic service. + * @since 100.3.0 */ interface ConverterInterface { @@ -28,6 +29,7 @@ interface ConverterInterface * * @param string $internalType * @return string + * @since 100.3.0 */ public function convert(string $internalType): string; } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php index f7dfcd29e5036..bc031fc988fb0 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php @@ -7,8 +7,9 @@ namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider; -use Magento\Eav\Model\Config; use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface as IndexTypeConverterInterface; @@ -109,67 +110,82 @@ public function getFields(array $context = []): array $allAttributes = []; foreach ($attributes as $attribute) { - if (in_array($attribute->getAttributeCode(), $this->excludedAttributes, true)) { - continue; - } - $attributeAdapter = $this->attributeAdapterProvider->getByAttributeCode($attribute->getAttributeCode()); - $fieldName = $this->fieldNameResolver->getFieldName($attributeAdapter); + $allAttributes += $this->getField($attribute); + } - $allAttributes[$fieldName] = [ - 'type' => $this->fieldTypeResolver->getFieldType($attributeAdapter), - ]; + $allAttributes['store_id'] = [ + 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING), + 'index' => $this->indexTypeConverter->convert(IndexTypeConverterInterface::INTERNAL_NO_INDEX_VALUE), + ]; - $index = $this->fieldIndexResolver->getFieldIndex($attributeAdapter); - if (null !== $index) { - $allAttributes[$fieldName]['index'] = $index; - } + return $allAttributes; + } - if ($attributeAdapter->isSortable()) { - $sortFieldName = $this->fieldNameResolver->getFieldName( - $attributeAdapter, - ['type' => FieldMapperInterface::TYPE_SORT] - ); - $allAttributes[$fieldName]['fields'][$sortFieldName] = [ - 'type' => $this->fieldTypeConverter->convert( - FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD - ), - 'index' => $this->indexTypeConverter->convert( - IndexTypeConverterInterface::INTERNAL_NO_ANALYZE_VALUE - ) - ]; - } + /** + * Get field mapping for specific attribute. + * + * @param AbstractAttribute $attribute + * @return array + */ + public function getField(AbstractAttribute $attribute): array + { + $fieldMapping = []; + if (in_array($attribute->getAttributeCode(), $this->excludedAttributes, true)) { + return $fieldMapping; + } + + $attributeAdapter = $this->attributeAdapterProvider->getByAttributeCode($attribute->getAttributeCode()); + $fieldName = $this->fieldNameResolver->getFieldName($attributeAdapter); - if ($attributeAdapter->isTextType()) { - $keywordFieldName = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD; - $index = $this->indexTypeConverter->convert( + $fieldMapping[$fieldName] = [ + 'type' => $this->fieldTypeResolver->getFieldType($attributeAdapter), + ]; + + $index = $this->fieldIndexResolver->getFieldIndex($attributeAdapter); + if (null !== $index) { + $fieldMapping[$fieldName]['index'] = $index; + } + + if ($attributeAdapter->isSortable()) { + $sortFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $fieldMapping[$fieldName]['fields'][$sortFieldName] = [ + 'type' => $this->fieldTypeConverter->convert( + FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD + ), + 'index' => $this->indexTypeConverter->convert( IndexTypeConverterInterface::INTERNAL_NO_ANALYZE_VALUE - ); - $allAttributes[$fieldName]['fields'][$keywordFieldName] = [ - 'type' => $this->fieldTypeConverter->convert( - FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD - ) - ]; - if ($index) { - $allAttributes[$fieldName]['fields'][$keywordFieldName]['index'] = $index; - } - } + ) + ]; + } - if ($attributeAdapter->isComplexType()) { - $childFieldName = $this->fieldNameResolver->getFieldName( - $attributeAdapter, - ['type' => FieldMapperInterface::TYPE_QUERY] - ); - $allAttributes[$childFieldName] = [ - 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING) - ]; + if ($attributeAdapter->isTextType()) { + $keywordFieldName = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD; + $index = $this->indexTypeConverter->convert( + IndexTypeConverterInterface::INTERNAL_NO_ANALYZE_VALUE + ); + $fieldMapping[$fieldName]['fields'][$keywordFieldName] = [ + 'type' => $this->fieldTypeConverter->convert( + FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD + ) + ]; + if ($index) { + $fieldMapping[$fieldName]['fields'][$keywordFieldName]['index'] = $index; } } - $allAttributes['store_id'] = [ - 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING), - 'index' => $this->indexTypeConverter->convert(IndexTypeConverterInterface::INTERNAL_NO_INDEX_VALUE), - ]; + if ($attributeAdapter->isComplexType()) { + $childFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_QUERY] + ); + $fieldMapping[$childFieldName] = [ + 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING) + ]; + } - return $allAttributes; + return $fieldMapping; } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php index e7d8d0672aaf0..069bf6e2ab33a 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php @@ -14,7 +14,7 @@ * @api * @since 100.1.0 * - * @deprecated This class provide not full data about field type. Only basic rules apply in this class. + * @deprecated 100.3.0 This class provide not full data about field type. Only basic rules apply in this class. * @see ResolverInterface */ class FieldType @@ -37,11 +37,12 @@ class FieldType /** * Get field type. * - * @deprecated + * @deprecated 100.3.0 * @see ResolverInterface::getFieldType * * @param AbstractAttribute $attribute * @return string + * @since 100.1.0 */ public function getFieldType($attribute) { diff --git a/app/code/Magento/Elasticsearch/Model/DataProvider/Base/Suggestions.php b/app/code/Magento/Elasticsearch/Model/DataProvider/Base/Suggestions.php index 8364b6c116b7d..2d4f8abeb8ecd 100644 --- a/app/code/Magento/Elasticsearch/Model/DataProvider/Base/Suggestions.php +++ b/app/code/Magento/Elasticsearch/Model/DataProvider/Base/Suggestions.php @@ -5,19 +5,23 @@ */ namespace Magento\Elasticsearch\Model\DataProvider\Base; -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; -use Magento\Store\Model\ScopeInterface; -use Magento\Search\Model\QueryInterface; +use Elasticsearch\Common\Exceptions\BadRequest400Exception; use Magento\AdvancedSearch\Model\SuggestedQueriesInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; -use Magento\Search\Model\QueryResultFactory; -use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Search\Model\QueryInterface; +use Magento\Search\Model\QueryResultFactory; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface as StoreManager; +use Psr\Log\LoggerInterface; /** * Default implementation to provide suggestions mechanism for Elasticsearch + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Suggestions implements SuggestedQueriesInterface { @@ -56,6 +60,11 @@ class Suggestions implements SuggestedQueriesInterface */ private $fieldProvider; + /** + * @var LoggerInterface + */ + private $logger; + /** * Suggestions constructor. * @@ -66,6 +75,7 @@ class Suggestions implements SuggestedQueriesInterface * @param SearchIndexNameResolver $searchIndexNameResolver * @param StoreManager $storeManager * @param FieldProviderInterface $fieldProvider + * @param LoggerInterface|null $logger */ public function __construct( ScopeConfigInterface $scopeConfig, @@ -74,7 +84,8 @@ public function __construct( ConnectionManager $connectionManager, SearchIndexNameResolver $searchIndexNameResolver, StoreManager $storeManager, - FieldProviderInterface $fieldProvider + FieldProviderInterface $fieldProvider, + LoggerInterface $logger = null ) { $this->queryResultFactory = $queryResultFactory; $this->connectionManager = $connectionManager; @@ -83,6 +94,7 @@ public function __construct( $this->searchIndexNameResolver = $searchIndexNameResolver; $this->storeManager = $storeManager; $this->fieldProvider = $fieldProvider; + $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -93,8 +105,14 @@ public function getItems(QueryInterface $query) $result = []; if ($this->isSuggestionsAllowed()) { $isResultsCountEnabled = $this->isResultsCountEnabled(); + try { + $suggestions = $this->getSuggestions($query); + } catch (BadRequest400Exception $e) { + $this->logger->critical($e); + $suggestions = []; + } - foreach ($this->getSuggestions($query) as $suggestion) { + foreach ($suggestions as $suggestion) { $count = null; if ($isResultsCountEnabled) { $count = isset($suggestion['freq']) ? $suggestion['freq'] : null; diff --git a/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php b/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php index 54e9890e02e59..56cdebdfc2813 100644 --- a/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php +++ b/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php @@ -20,7 +20,7 @@ /** * The implementation to provide suggestions mechanism for Elasticsearch5 * - * @deprecated because of EOL for Elasticsearch5 + * @deprecated 100.3.5 because of EOL for Elasticsearch5 * @see \Magento\Elasticsearch\Model\DataProvider\Base\Suggestions */ class Suggestions implements SuggestedQueriesInterface diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php b/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php new file mode 100644 index 0000000000000..53f036a3b8e38 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product; + +use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; +use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\Model\Indexer\IndexerHandler as ElasticsearchIndexerHandler; +use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Exception\LocalizedException; + +/** + * Catalog search indexer plugin for catalog attribute. + */ +class Attribute +{ + /** + * @var Config + */ + private $config; + + /** + * @var Processor + */ + private $indexerProcessor; + + /** + * @var DimensionProviderInterface + */ + private $dimensionProvider; + + /** + * @var IndexerHandlerFactory + */ + private $indexerHandlerFactory; + + /** + * @var bool + */ + private $isNewObject; + + /** + * @var string + */ + private $attributeCode; + + /** + * @param Config $config + * @param Processor $indexerProcessor + * @param DimensionProviderInterface $dimensionProvider + * @param IndexerHandlerFactory $indexerHandlerFactory + */ + public function __construct( + Config $config, + Processor $indexerProcessor, + DimensionProviderInterface $dimensionProvider, + IndexerHandlerFactory $indexerHandlerFactory + ) { + $this->config = $config; + $this->indexerProcessor = $indexerProcessor; + $this->dimensionProvider = $dimensionProvider; + $this->indexerHandlerFactory = $indexerHandlerFactory; + } + + /** + * Update catalog search indexer mapping if third party search engine is used. + * + * @param AttributeResourceModel $subject + * @param AttributeResourceModel $result + * @return AttributeResourceModel + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws LocalizedException + */ + public function afterSave( + AttributeResourceModel $subject, + AttributeResourceModel $result + ): AttributeResourceModel { + $indexer = $this->indexerProcessor->getIndexer(); + if ($this->isNewObject + && !$indexer->isScheduled() + && $this->config->isElasticsearchEnabled() + ) { + $indexerHandler = $this->indexerHandlerFactory->create(['data' => $indexer->getData()]); + if (!$indexerHandler instanceof ElasticsearchIndexerHandler) { + throw new LocalizedException( + __('Created indexer handler must be instance of %1.', ElasticsearchIndexerHandler::class) + ); + } + foreach ($this->dimensionProvider->getIterator() as $dimension) { + $indexerHandler->updateIndex($dimension, $this->attributeCode); + } + } + + return $result; + } + + /** + * Set class variables before saving attribute. + * + * @param AttributeResourceModel $subject + * @param AbstractModel $attribute + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave( + AttributeResourceModel $subject, + AbstractModel $attribute + ): void { + $this->isNewObject = $attribute->isObjectNew(); + $this->attributeCode = $attribute->getAttributeCode(); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php b/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php index 847710eaa445a..90e21e9e3ea1e 100644 --- a/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php +++ b/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php @@ -5,12 +5,13 @@ */ namespace Magento\Elasticsearch\Model\Indexer; -use Magento\Framework\Indexer\SaveHandler\IndexerInterface; -use Magento\Framework\Indexer\SaveHandler\Batch; -use Magento\Framework\Indexer\IndexStructureInterface; use Magento\Elasticsearch\Model\Adapter\Elasticsearch as ElasticsearchAdapter; use Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver; use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\Indexer\IndexStructureInterface; +use Magento\Framework\Indexer\SaveHandler\Batch; +use Magento\Framework\Indexer\SaveHandler\IndexerInterface; +use Magento\Framework\Search\Request\Dimension; /** * Indexer Handler for Elasticsearch engine. @@ -18,7 +19,7 @@ class IndexerHandler implements IndexerInterface { /** - * Default batch size + * Size of default batch */ const DEFAULT_BATCH_SIZE = 500; @@ -132,6 +133,22 @@ public function isAvailable($dimensions = []) return $this->adapter->ping(); } + /** + * Update mapping data for index. + * + * @param Dimension[] $dimensions + * @param string $attributeCode + * @return IndexerInterface + */ + public function updateIndex(array $dimensions, string $attributeCode): IndexerInterface + { + $dimension = current($dimensions); + $scopeId = (int)$this->scopeResolver->getScope($dimension->getValue())->getId(); + $this->adapter->updateIndexMapping($scopeId, $this->getIndexerId(), $attributeCode); + + return $this; + } + /** * Returns indexer id. * diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php index 21ff9a53e4f96..12887207e2c5e 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php @@ -10,7 +10,7 @@ /** * This class add in backward compatibility purposes to check if need to apply old strategy for filter prepare process. - * @deprecated + * @deprecated 100.3.2 */ class DefaultFilterStrategyApplyChecker implements DefaultFilterStrategyApplyCheckerInterface { diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php index 1e106023ea00d..548a57e55f3e2 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php @@ -35,7 +35,7 @@ public function __construct(Repository $algorithmRepository, EntityStorageFactor } /** - * {@inheritdoc} + * @inheritdoc */ public function build( RequestBucketInterface $bucket, @@ -46,9 +46,7 @@ public function build( /** @var DynamicBucket $bucket */ $algorithm = $this->algorithmRepository->get($bucket->getMethod(), ['dataProvider' => $dataProvider]); $data = $algorithm->getItems($bucket, $dimensions, $this->getEntityStorage($queryResult)); - $resultData = $this->prepareData($data); - - return $resultData; + return $this->prepareData($data); } /** @@ -77,12 +75,9 @@ private function prepareData($data) { $resultData = []; foreach ($data as $value) { - $from = is_numeric($value['from']) ? $value['from'] : '*'; - $to = is_numeric($value['to']) ? $value['to'] : '*'; - unset($value['from'], $value['to']); - - $rangeName = "{$from}_{$to}"; - $resultData[$rangeName] = array_merge(['value' => $rangeName], $value); + $rangeName = "{$value['from']}_{$value['to']}"; + $value['value'] = $rangeName; + $resultData[$rangeName] = $value; } return $resultData; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php index 496a77e4c5ac3..7bc64b59ffe78 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php @@ -235,11 +235,9 @@ public function prepareData($range, array $dbRanges) { $data = []; if (!empty($dbRanges)) { - $lastIndex = array_keys($dbRanges); - $lastIndex = $lastIndex[count($lastIndex) - 1]; foreach ($dbRanges as $index => $count) { - $fromPrice = $index == 1 ? '' : ($index - 1) * $range; - $toPrice = $index == $lastIndex ? '' : $index * $range; + $fromPrice = $index == 1 ? 0 : ($index - 1) * $range; + $toPrice = $index * $range; $data[] = [ 'from' => $fromPrice, 'to' => $toPrice, diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php index 76a2f00f44fe2..ce79f433460d9 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php @@ -68,7 +68,7 @@ public function buildFilter(RequestFilterInterface $filter) $fieldName .= '.' . $suffix; } - if ($filter->getValue()) { + if ($filter->getValue() !== false) { $operator = is_array($filter->getValue()) ? 'terms' : 'term'; $filterQuery []= [ $operator => [ diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php b/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php index d76086ee2f809..1654e02558a83 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php @@ -19,7 +19,7 @@ * * @api * @since 100.1.0 - * @deprecated because of EOL for Elasticsearch2 + * @deprecated 100.3.5 because of EOL for Elasticsearch2 */ class Mapper extends Elasticsearch5Mapper { diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php index 68bec2580f621..3de88ff9f0307 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php @@ -57,7 +57,7 @@ public function transform(string $value): string */ private function escape(string $value): string { - $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; + $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\/|\*|\?|:|\\\)/'; $replace = '\\\$1'; return preg_replace($pattern, $replace, $value); diff --git a/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php new file mode 100644 index 0000000000000..7cd72c322d647 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Elasticsearch\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; +use Magento\Framework\Setup\Patch\PatchInterface; + +/** + * Invalidate fulltext index + */ +class InvalidateIndex implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var IndexerRegistry + */ + private $indexerRegistry; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param IndexerRegistry $indexerRegistry + */ + public function __construct(ModuleDataSetupInterface $moduleDataSetup, IndexerRegistry $indexerRegistry) + { + $this->moduleDataSetup = $moduleDataSetup; + $this->indexerRegistry = $indexerRegistry; + } + + /** + * @inheritDoc + */ + public function apply(): PatchInterface + { + $this->indexerRegistry->get(FulltextIndexer::INDEXER_ID)->invalidate(); + return $this; + } + + /** + * @inheritDoc + */ + public static function getDependencies(): array + { + return []; + } + + /** + * @inheritDoc + */ + public function getAliases(): array + { + return []; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml index d612f5bd17a2f..c2c8644a6fcf5 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml @@ -9,8 +9,12 @@ <suite name="SearchEngineElasticsearchSuite"> <before> <magentoCLI stepKey="setSearchEngineToElasticsearch" command="config:set {{SearchEngineElasticsearchConfigData.path}} {{SearchEngineElasticsearchConfigData.value}}"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after></after> <include> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml index e8a0df9b9dc87..8d1b420f3c17f 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml @@ -46,9 +46,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> <!--Navigate to storefront and do a quick search for the product --> <comment userInput="Navigate to Storefront to check if quick search works" stepKey="commentCheckQuickSearch" /> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> - - <waitForPageLoad stepKey="waitForHomePageToLoad" time="30"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> <fillField userInput="Simple" selector="{{StorefrontQuickSearchSection.searchPhrase}}" stepKey="fillSearchBar"/> <waitForPageLoad stepKey="wait2" time="30"/> <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml index a94a6a2e3d133..1e067f1560404 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml @@ -36,7 +36,9 @@ <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> - <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushFullPageCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushFullPageCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchWithDecimalAttributeUsingElasticSearchTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchWithDecimalAttributeUsingElasticSearchTest.xml index d9988577009bc..1c4d53b273661 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchWithDecimalAttributeUsingElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchWithDecimalAttributeUsingElasticSearchTest.xml @@ -55,8 +55,12 @@ <!--Delete attribute--> <deleteData createDataKey="customAttribute" stepKey="deleteCustomAttribute"/> <!--Reindex and clear cache--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:clean" stepKey="cleanCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> <!--Navigate to backend and update value for custom attribute --> @@ -77,8 +81,12 @@ </actionGroup> <!--Reindex and clear cache--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:clean" stepKey="cleanCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> <!-- Navigate to Storefront and search --> diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php index 49a894f1295c7..398c79f056810 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php @@ -69,6 +69,7 @@ protected function setUp(): void 'create', 'delete', 'putMapping', + 'getMapping', 'deleteMapping', 'stats', 'updateAliases', @@ -329,7 +330,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -340,7 +341,7 @@ public function testAddFieldsMapping() 'match_mapping_type' => 'string', 'mapping' => [ 'type' => 'integer', - 'index' => true + 'index' => true, ], ], ], @@ -354,6 +355,14 @@ public function testAddFieldsMapping() ], ], ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], ], ], ], @@ -400,7 +409,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -424,7 +433,15 @@ public function testAddFieldsMappingFailure() 'index' => true, ], ], - ] + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], ], ], ], @@ -517,6 +534,22 @@ public function testDeleteMappingFailure() ); } + /** + * Test get Elasticsearch mapping process. + * + * @return void + */ + public function testGetMapping(): void + { + $params = ['index' => 'indexName']; + $this->indicesMock->expects($this->once()) + ->method('getMapping') + ->with($params) + ->willReturn([]); + + $this->model->getMapping($params); + } + /** * Test query() method * @return void diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php index 2c87549da6075..9f1b59b1bfc81 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php @@ -21,6 +21,8 @@ use PHPUnit\Framework\TestCase; /** + * Unit tests for \Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper class. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductDataMapperTest extends TestCase @@ -56,12 +58,12 @@ class ProductDataMapperTest extends TestCase private $additionalFieldsProvider; /** - * @var MockObject + * @var DataProvider|MockObject */ private $dataProvider; /** - * Set up test environment. + * @inheritdoc */ protected function setUp(): void { @@ -71,6 +73,11 @@ protected function setUp(): void $this->attribute = $this->createMock(Attribute::class); $this->additionalFieldsProvider = $this->getMockForAbstractClass(AdditionalFieldsProviderInterface::class); $this->dateFieldTypeMock = $this->createMock(Date::class); + $filterableAttributeTypes = [ + 'boolean' => 'boolean', + 'multiselect' => 'multiselect', + 'select' => 'select', + ]; $objectManager = new ObjectManagerHelper($this); $this->model = $objectManager->getObject( @@ -81,6 +88,7 @@ protected function setUp(): void 'dateFieldType' => $this->dateFieldTypeMock, 'dataProvider' => $this->dataProvider, 'additionalFieldsProvider' => $this->additionalFieldsProvider, + 'filterableAttributeTypes' => $filterableAttributeTypes, ] ); } @@ -159,8 +167,8 @@ public function testGetMap(int $productId, array $attributeData, $attributeValue $productId => [$attributeId => $attributeValue], ]; $documents = $this->model->map($documentData, $storeId, $context); - $returnAttributeData['store_id'] = $storeId; - $this->assertEquals($returnAttributeData, $documents[$productId]); + $returnAttributeData = ['store_id' => $storeId] + $returnAttributeData; + $this->assertSame($returnAttributeData, $documents[$productId]); } /** @@ -305,8 +313,8 @@ public static function mapProvider(): array ['value' => '2', 'label' => 'Disabled'], ], ], - [10 => '1', 11 => '2'], - ['status' => '1'], + [10 => '1', 11 => '2'], + ['status' => 1], ], 'select without options' => [ 10, @@ -318,7 +326,7 @@ public static function mapProvider(): array 'options' => [], ], '44', - ['color' => '44'], + ['color' => 44], ], 'unsearchable select with options' => [ 10, @@ -333,7 +341,7 @@ public static function mapProvider(): array ], ], '44', - ['color' => '44'], + ['color' => 44], ], 'searchable select with options' => [ 10, @@ -348,7 +356,7 @@ public static function mapProvider(): array ], ], '44', - ['color' => '44', 'color_value' => 'red'], + ['color' => 44, 'color_value' => 'red'], ], 'composite select with options' => [ 10, @@ -363,7 +371,7 @@ public static function mapProvider(): array ], ], [10 => '44', 11 => '45'], - ['color' => ['44', '45'], 'color_value' => ['red', 'black']], + ['color' => [44, 45], 'color_value' => ['red', 'black']], ], 'multiselect without options' => [ 10, @@ -430,10 +438,10 @@ public static function mapProvider(): array 'backend_type' => 'int', 'frontend_input' => 'int', 'is_searchable' => false, - 'options' => [] + 'options' => [], ], 15, - [] + [], ], ]; } 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 dd4bffe8e7c33..b070e3324ed78 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php @@ -11,14 +11,18 @@ use Elasticsearch\Namespaces\IndicesNamespace; use Magento\AdvancedSearch\Model\Client\ClientInterface as ElasticsearchClient; use Magento\AdvancedSearch\Model\Client\ClientOptionsInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface; use Magento\Elasticsearch\Model\Adapter\Elasticsearch as ElasticsearchAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField; use Magento\Elasticsearch\Model\Adapter\Index\BuilderInterface; use Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Stdlib\ArrayManager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -81,6 +85,21 @@ class ElasticsearchTest extends TestCase */ protected $indexNameResolver; + /** + * @var ProductAttributeRepositoryInterface|MockObject + */ + private $productAttributeRepository; + + /** + * @var StaticField|MockObject + */ + private $staticFieldProvider; + + /** + * @var ArrayManager|MockObject + */ + private $arrayManager; + /** * Setup * @@ -155,7 +174,14 @@ protected function setUp(): void ->method('getAllAttributesTypes') ->willReturn( [ - 'name' => 'string', + 'name' => [ + 'type' => 'string', + 'fields' => [ + 'keyword' => [ + 'type' => "keyword", + ], + ], + ], ] ); $this->clientConfig->expects($this->any()) @@ -177,9 +203,17 @@ protected function setUp(): void ) ->disableOriginalConstructor() ->getMock(); - $this->batchDocumentDataMapper = $this->getMockBuilder( - BatchDataMapperInterface::class - )->disableOriginalConstructor() + $this->batchDocumentDataMapper = $this->getMockBuilder(BatchDataMapperInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->productAttributeRepository = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->staticFieldProvider = $this->getMockBuilder(StaticField::class) + ->disableOriginalConstructor() + ->getMock(); + $this->arrayManager = $this->getMockBuilder(ArrayManager::class) + ->disableOriginalConstructor() ->getMock(); $this->model = $this->objectManager->getObject( \Magento\Elasticsearch\Model\Adapter\Elasticsearch::class, @@ -192,6 +226,9 @@ protected function setUp(): void 'logger' => $this->logger, 'indexNameResolver' => $this->indexNameResolver, 'options' => [], + 'productAttributeRepository' => $this->productAttributeRepository, + 'staticFieldProvider' => $this->staticFieldProvider, + 'arrayManager' => $this->arrayManager, ] ); } @@ -459,6 +496,103 @@ public function testUpdateAliasWithoutOldIndex() $this->assertEquals($this->model, $this->model->updateAlias(1, 'product')); } + /** + * Test update Elasticsearch mapping for index without alias definition. + * + * @return void + */ + public function testUpdateIndexMappingWithoutAliasDefinition(): void + { + $storeId = 1; + $mappedIndexerId = 'product'; + + $this->indexNameResolver->expects($this->once()) + ->method('getIndexFromAlias') + ->with($storeId, $mappedIndexerId) + ->willReturn(''); + + $this->productAttributeRepository->expects($this->never()) + ->method('get'); + + $this->model->updateIndexMapping($storeId, $mappedIndexerId, 'attribute_code'); + } + + /** + * Test update Elasticsearch mapping for index with alias definition. + * + * @return void + */ + public function testUpdateIndexMappingWithAliasDefinition(): void + { + $storeId = 1; + $mappedIndexerId = 'product'; + $indexName = '_product_1_v1'; + $attributeCode = 'example_attribute_code'; + + $this->indexNameResolver->expects($this->once()) + ->method('getIndexFromAlias') + ->with($storeId, $mappedIndexerId) + ->willReturn($indexName); + + $attribute = $this->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->productAttributeRepository->expects($this->once()) + ->method('get') + ->with($attributeCode) + ->willReturn($attribute); + + $this->staticFieldProvider->expects($this->once()) + ->method('getField') + ->with($attribute) + ->willReturn([$attributeCode => ['type' => 'text']]); + + $mappedAttributes = ['another_attribute_code' => 'attribute_mapping']; + $this->client->expects($this->once()) + ->method('getMapping') + ->with(['index' => $indexName]) + ->willReturn(['properties' => $mappedAttributes]); + + $this->arrayManager->expects($this->once()) + ->method('findPath') + ->with('properties', ['properties' => $mappedAttributes]) + ->willReturn('example/path/to/properties'); + + $this->arrayManager->expects($this->once()) + ->method('get') + ->with('example/path/to/properties', ['properties' => $mappedAttributes], []) + ->willReturn($mappedAttributes); + + $this->client->expects($this->once()) + ->method('addFieldsMapping') + ->with([$attributeCode => ['type' => 'text']], $indexName, 'product'); + + $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/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php index 87f072836544e..a9bcd1a20a1b2 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php @@ -246,7 +246,7 @@ function ($type) use ($complexType) { if ($type === 'string') { return 'string'; } elseif ($type === 'float') { - return 'float'; + return 'double'; } elseif ($type === 'integer') { return 'integer'; } else { @@ -281,7 +281,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ] @@ -300,7 +300,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ], @@ -319,7 +319,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ] diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php index 75b79bc43e805..718adf255254f 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php @@ -56,7 +56,7 @@ public function convertProvider() { return [ ['string', 'string'], - ['float', 'float'], + ['float', 'double'], ['integer', 'integer'], ]; } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php index 7151677db3405..9f1c5db60b3d8 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php @@ -7,7 +7,10 @@ namespace Magento\Elasticsearch\Test\Unit\Model\DataProvider\Base; +use Elasticsearch\Common\Exceptions\BadRequest400Exception; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\Model\DataProvider\Base\Suggestions; use Magento\Elasticsearch\Model\DataProvider\Suggestions as SuggestionsDataProvider; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; @@ -21,6 +24,7 @@ use Magento\Store\Model\StoreManagerInterface as StoreManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -62,6 +66,21 @@ class SuggestionsTest extends TestCase */ private $storeManager; + /** + * @var FieldProviderInterface|MockObject + */ + private $fieldProvider; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var Elasticsearch|MockObject + */ + private $client; + /** * @var QueryInterface|MockObject */ @@ -99,7 +118,19 @@ protected function setUp(): void ->setMethods(['getIndexName']) ->getMock(); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + $this->storeManager = $this->getMockBuilder(StoreManager::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->fieldProvider = $this->getMockBuilder(FieldProviderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->logger = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->client = $this->getMockBuilder(Elasticsearch::class) ->disableOriginalConstructor() ->getMock(); @@ -110,81 +141,155 @@ protected function setUp(): void $objectManager = new ObjectManagerHelper($this); $this->model = $objectManager->getObject( - \Magento\Elasticsearch\Model\DataProvider\Base\Suggestions::class, + Suggestions::class, [ 'queryResultFactory' => $this->queryResultFactory, 'connectionManager' => $this->connectionManager, 'scopeConfig' => $this->scopeConfig, 'config' => $this->config, 'searchIndexNameResolver' => $this->searchIndexNameResolver, - 'storeManager' => $this->storeManager + 'storeManager' => $this->storeManager, + 'fieldProvider' => $this->fieldProvider, + 'logger' => $this->logger, ] ); } /** - * Test getItems() method + * Test get items process with search suggestions disabled. + * @return void */ - public function testGetItems() + public function testGetItemsWithDisabledSearchSuggestion(): void { - $this->scopeConfig->expects($this->any()) - ->method('getValue') - ->willReturn(1); - - $this->config->expects($this->any()) - ->method('isElasticsearchEnabled') - ->willReturn(1); - - $store = $this->getMockBuilder(StoreInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->willReturn(false); - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($store); - - $store->expects($this->any()) - ->method('getId') - ->willReturn(1); + $this->scopeConfig->expects($this->never()) + ->method('getValue'); - $this->searchIndexNameResolver->expects($this->any()) - ->method('getIndexName') - ->willReturn('magento2_product_1'); + $this->config->expects($this->once()) + ->method('isElasticsearchEnabled') + ->willReturn(true); - $this->query->expects($this->any()) - ->method('getQueryText') - ->willReturn('query'); + $this->logger->expects($this->never()) + ->method('critical'); - $client = $this->getMockBuilder(Elasticsearch::class) - ->disableOriginalConstructor() - ->getMock(); + $this->queryResultFactory->expects($this->never()) + ->method('create'); - $this->connectionManager->expects($this->any()) - ->method('getConnection') - ->willReturn($client); + $this->assertEmpty($this->model->getItems($this->query)); + } - $client->expects($this->any()) + /** + * Test get items process with search suggestions enabled. + * @return void + */ + public function testGetItemsWithEnabledSearchSuggestion(): void + { + $this->prepareSearchQuery(); + $this->client->expects($this->once()) ->method('query') ->willReturn([ 'suggest' => [ 'phrase_field' => [ - 'options' => [ - 'text' => 'query', - 'score' => 1, - 'freq' => 1, + [ + 'options' => [ + 'suggestion' => [ + 'text' => 'query', + 'score' => 1, + 'freq' => 1, + ] + ] ] ], ], ]); + $this->logger->expects($this->never()) + ->method('critical'); + $query = $this->getMockBuilder(QueryResult::class) ->disableOriginalConstructor() ->getMock(); - $this->queryResultFactory->expects($this->any()) + $this->queryResultFactory->expects($this->once()) ->method('create') ->willReturn($query); - $this->assertIsArray($this->model->getItems($this->query)); + $this->assertEquals([$query], $this->model->getItems($this->query)); + } + + /** + * Test get items process when throwing an exception. + * @return void + */ + public function testGetItemsException(): void + { + $this->prepareSearchQuery(); + $exception = new BadRequest400Exception(); + + $this->client->expects($this->once()) + ->method('query') + ->willThrowException($exception); + + $this->logger->expects($this->once()) + ->method('critical') + ->with($exception); + + $this->queryResultFactory->expects($this->never()) + ->method('create'); + + $this->assertEmpty($this->model->getItems($this->query)); + } + + /** + * Prepare Mocks for default get items process. + * @return void + */ + private function prepareSearchQuery(): void + { + $storeId = 1; + + $this->scopeConfig->expects($this->exactly(2)) + ->method('isSetFlag') + ->willReturn(true); + + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->willReturn(1); + + $this->config->expects($this->once()) + ->method('isElasticsearchEnabled') + ->willReturn(true); + + $store = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $store->expects($this->once()) + ->method('getId') + ->willReturn($storeId); + + $this->storeManager->expects($this->once()) + ->method('getStore') + ->willReturn($store); + + $this->searchIndexNameResolver->expects($this->once()) + ->method('getIndexName') + ->with($storeId, Config::ELASTICSEARCH_TYPE_DEFAULT) + ->willReturn('magento2_product_1'); + + $this->query->expects($this->once()) + ->method('getQueryText') + ->willReturn('query'); + + $this->fieldProvider->expects($this->once()) + ->method('getFields') + ->willReturn([]); + + $this->connectionManager->expects($this->once()) + ->method('getConnection') + ->willReturn($this->client); } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php new file mode 100644 index 0000000000000..801c7ca3be216 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Indexer\Fulltext\Plugin\Category\Product; + +use ArrayIterator; +use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as AttributeModel; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; +use Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory; +use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product\Attribute as AttributePlugin; +use Magento\Elasticsearch\Model\Indexer\IndexerHandler; +use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\Framework\Indexer\IndexerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher; +use PHPUnit\Framework\TestCase; + +/** + * Tests for catalog search indexer plugin. + */ +class AttributeTest extends TestCase +{ + /** + * @var Config|MockObject + */ + private $configMock; + + /** + * @var Processor|MockObject + */ + private $indexerProcessorMock; + + /** + * @var DimensionProviderInterface|MockObject + */ + private $dimensionProviderMock; + + /** + * @var IndexerHandlerFactory|MockObject + */ + private $indexerHandlerFactoryMock; + + /** + * @var AttributePlugin + */ + private $attributePlugin; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->configMock = $this->createMock(Config::class); + $this->indexerProcessorMock = $this->createMock(Processor::class); + $this->dimensionProviderMock = $this->getMockBuilder(DimensionProviderInterface::class) + ->getMockForAbstractClass(); + $this->indexerHandlerFactoryMock = $this->createMock(IndexerHandlerFactory::class); + + $this->attributePlugin = (new ObjectManager($this))->getObject( + AttributePlugin::class, + [ + 'config' => $this->configMock, + 'indexerProcessor' => $this->indexerProcessorMock, + 'dimensionProvider' => $this->dimensionProviderMock, + 'indexerHandlerFactory' => $this->indexerHandlerFactoryMock, + ] + ); + } + + /** + * Test update catalog search indexer process. + * + * @param bool $isNewObject + * @param bool $isElasticsearchEnabled + * @param array $dimensions + * @return void + * @dataProvider afterSaveDataProvider + * + */ + public function testAfterSave(bool $isNewObject, bool $isElasticsearchEnabled, array $dimensions): void + { + /** @var AttributeModel|MockObject $attributeMock */ + $attributeMock = $this->createMock(AttributeModel::class); + $attributeMock->expects($this->once()) + ->method('isObjectNew') + ->willReturn($isNewObject); + + $attributeMock->expects($this->once()) + ->method('getAttributeCode') + ->willReturn('example_attribute_code'); + + /** @var AttributeResourceModel|MockObject $subjectMock */ + $subjectMock = $this->createMock(AttributeResourceModel::class); + $this->attributePlugin->beforeSave($subjectMock, $attributeMock); + + $indexerData = ['indexer_example_data']; + + /** @var IndexerInterface|MockObject $indexerMock */ + $indexerMock = $this->getMockBuilder(IndexerInterface::class) + ->setMethods(['getData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $indexerMock->expects($this->getExpectsCount($isNewObject, $isElasticsearchEnabled)) + ->method('getData') + ->willReturn($indexerData); + + $this->indexerProcessorMock->expects($this->once()) + ->method('getIndexer') + ->willReturn($indexerMock); + + $this->configMock->expects($isNewObject ? $this->once() : $this->never()) + ->method('isElasticsearchEnabled') + ->willReturn($isElasticsearchEnabled); + + /** @var IndexerHandler|MockObject $indexerHandlerMock */ + $indexerHandlerMock = $this->createMock(IndexerHandler::class); + + $indexerHandlerMock + ->expects(($isNewObject && $isElasticsearchEnabled) ? $this->exactly(count($dimensions)) : $this->never()) + ->method('updateIndex') + ->willReturnSelf(); + + $this->indexerHandlerFactoryMock->expects($this->getExpectsCount($isNewObject, $isElasticsearchEnabled)) + ->method('create') + ->with(['data' => $indexerData]) + ->willReturn($indexerHandlerMock); + + $this->dimensionProviderMock->expects($this->getExpectsCount($isNewObject, $isElasticsearchEnabled)) + ->method('getIterator') + ->willReturn(new ArrayIterator($dimensions)); + + $this->assertEquals($subjectMock, $this->attributePlugin->afterSave($subjectMock, $subjectMock)); + } + + /** + * DataProvider for testAfterSave(). + * + * @return array + */ + public function afterSaveDataProvider(): array + { + $dimensions = [['scope' => 1], ['scope' => 2]]; + + return [ + 'save_existing_object' => [false, false, $dimensions], + 'save_with_another_search_engine' => [true, false, $dimensions], + 'save_with_elasticsearch' => [true, true, []], + 'save_with_elasticsearch_and_dimensions' => [true, true, $dimensions], + ]; + } + + /** + * Retrieves how many times method is executed. + * + * @param bool $isNewObject + * @param bool $isElasticsearchEnabled + * @return InvokedCountMatcher + */ + private function getExpectsCount(bool $isNewObject, bool $isElasticsearchEnabled): InvokedCountMatcher + { + return ($isNewObject && $isElasticsearchEnabled) ? $this->once() : $this->never(); + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php index a147ca1b42b3b..3a9aef68c328c 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php @@ -267,4 +267,45 @@ public function testCleanIndex() $this->assertEquals($model, $result); } + + /** + * Test mapping data is updated for index. + * + * @return void + */ + public function testUpdateIndex(): void + { + $dimensionValue = 'SomeDimension'; + $indexMapping = 'some_index_mapping'; + $attributeCode = 'example_attribute_code'; + + $dimension = $this->getMockBuilder(Dimension::class) + ->disableOriginalConstructor() + ->getMock(); + + $dimension->expects($this->once()) + ->method('getValue') + ->willReturn($dimensionValue); + + $this->scopeResolver->expects($this->once()) + ->method('getScope') + ->with($dimensionValue) + ->willReturn($this->scopeInterface); + + $this->scopeInterface->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->indexNameResolver->expects($this->once()) + ->method('getIndexMapping') + ->with('catalogsearch_fulltext') + ->willReturn($indexMapping); + + $this->adapter->expects($this->once()) + ->method('updateIndexMapping') + ->with(1, $indexMapping, $attributeCode) + ->willReturnSelf(); + + $this->model->updateIndex([$dimension], $attributeCode); + } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php index c5b9089acd91c..0595b667f4ee8 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php @@ -390,13 +390,13 @@ public function testPrepareData() { $expectedResult = [ [ - 'from' => '', + 'from' => 0, 'to' => 10, 'count' => 1, ], [ 'from' => 10, - 'to' => '', + 'to' => 20, 'count' => 1, ], ]; diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/ValueTransformer/TextTransformerTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/ValueTransformer/TextTransformerTest.php new file mode 100644 index 0000000000000..66c0ce624fbfd --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/ValueTransformer/TextTransformerTest.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\SearchAdapter\Query\ValueTransformer; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer; +use PHPUnit\Framework\TestCase; + +/** + * Test value transformer + */ +class TextTransformerTest extends TestCase +{ + /** + * @var TextTransformer + */ + protected $model; + + /** + * Setup method + * @return void + */ + public function setUp(): void + { + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + TextTransformer::class, + [ + '$preprocessors' => [], + ] + ); + } + + /** + * Test transform value + * + * @param string $value + * @param string $expected + * @return void + * @dataProvider valuesDataProvider + */ + public function testTransform(string $value, string $expected): void + { + $result = $this->model->transform($value); + $this->assertEquals($expected, $result); + } + + /** + * Values data provider + * + * @return array + */ + public function valuesDataProvider(): array + { + return [ + ['Laptop^camera{microphone}', 'Laptop\^camera\{microphone\}'], + ['Birthday 25-Pack w/ Greatest of All Time Cupcake', 'Birthday 25\-Pack w\/ Greatest of All Time Cupcake'], + ['Retro vinyl record ~d123 *star', 'Retro vinyl record \~d123 \*star'], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json index 386bb1af298bb..b79ae7bc5cc47 100644 --- a/app/code/Magento/Elasticsearch/composer.json +++ b/app/code/Magento/Elasticsearch/composer.json @@ -12,7 +12,7 @@ "magento/module-store": "*", "magento/module-catalog-inventory": "*", "magento/framework": "*", - "elasticsearch/elasticsearch": "~7.6" + "elasticsearch/elasticsearch": "~7.7.0" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 633889e70591b..edec07cb5d51e 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -153,6 +153,11 @@ <type name="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper"> <arguments> <argument name="additionalFieldsProvider" xsi:type="object">additionalFieldsProviderForElasticsearch</argument> + <argument name="filterableAttributeTypes" xsi:type="array"> + <item name="boolean" xsi:type="string">boolean</item> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="select" xsi:type="string">select</item> + </argument> </arguments> </type> <preference for="Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface" type="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\DataMapperResolver" /> @@ -470,6 +475,11 @@ <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </virtualType> + <type name="Magento\Elasticsearch\Model\Adapter\Elasticsearch"> + <arguments> + <argument name="staticFieldProvider" xsi:type="object">elasticsearch5StaticFieldProvider</argument> + </arguments> + </type> <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> <arguments> <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> @@ -537,7 +547,7 @@ </type> <type name="Magento\Search\Model\SearchEngine\Validator"> <arguments> - <argument name="engineBlacklist" xsi:type="array"> + <argument name="excludedEngineList" xsi:type="array"> <item name="elasticsearch" xsi:type="string">Elasticsearch 2</item> </argument> <argument name="engineValidators" xsi:type="array"> @@ -545,4 +555,12 @@ </argument> </arguments> </type> + <type name="Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product\Attribute"> + <arguments> + <argument name="dimensionProvider" xsi:type="object" shared="false">Magento\Store\Model\StoreDimensionProvider</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\ResourceModel\Attribute"> + <plugin name="updateElasticsearchIndexerMapping" type="Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product\Attribute"/> + </type> </config> diff --git a/app/code/Magento/Elasticsearch/i18n/en_US.csv b/app/code/Magento/Elasticsearch/i18n/en_US.csv index 3a0ec556dbf8d..e36b3e054accd 100644 --- a/app/code/Magento/Elasticsearch/i18n/en_US.csv +++ b/app/code/Magento/Elasticsearch/i18n/en_US.csv @@ -11,3 +11,4 @@ "Elasticsearch Server Timeout","Elasticsearch Server Timeout" "Test Connection","Test Connection" "Minimum Terms to Match","Minimum Terms to Match" +"Created indexer handler must be instance of %1.", "Created indexer handler must be instance of %1." diff --git a/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php b/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php index 1b17db1a00f6e..c192b43bdc081 100644 --- a/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php +++ b/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php @@ -7,6 +7,7 @@ /** * Elasticsearch 6.x test connection block + * @deprecated the new minor release supports compatibility with Elasticsearch 7 */ class TestConnection extends \Magento\AdvancedSearch\Block\Adminhtml\System\Config\TestConnection { diff --git a/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php b/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php index 7532927f1dc85..cc8f69e92a858 100644 --- a/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php +++ b/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php @@ -12,6 +12,8 @@ /** * Default name resolver. + * + * @deprecated the new minor release supports compatibility with Elasticsearch 7 */ class DefaultResolver extends Base { diff --git a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php index 2c1c283c5b24d..2d787f4d4377d 100644 --- a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch6\Model\Client; use Magento\AdvancedSearch\Model\Client\ClientInterface; @@ -11,6 +12,8 @@ /** * Elasticsearch client + * + * @deprecated the new minor release supports compatibility with Elasticsearch 7 */ class Elasticsearch implements ClientInterface { @@ -48,8 +51,10 @@ public function __construct( $elasticsearchClient = null, $fieldsMappingPreprocessors = [] ) { - if (empty($options['hostname']) || ((!empty($options['enableAuth']) && - ($options['enableAuth'] == 1)) && (empty($options['username']) || empty($options['password'])))) { + if (empty($options['hostname']) + || ((!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) + && (empty($options['username']) || empty($options['password']))) + ) { throw new LocalizedException( __('The search failed because of a search engine misconfiguration.') ); @@ -174,6 +179,23 @@ public function createIndex($index, $settings) ); } + /** + * Add/update an Elasticsearch index settings. + * + * @param string $index + * @param array $settings + * @return void + */ + public function putIndexSettings(string $index, array $settings): void + { + $this->getClient()->indices()->putSettings( + [ + 'index' => $index, + 'body' => $settings, + ] + ); + } + /** * Delete an Elasticsearch index. * @@ -281,7 +303,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -303,7 +325,15 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', + ], + ], + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', ], ], ], @@ -319,6 +349,17 @@ public function addFieldsMapping(array $fields, $index, $entityType) $this->getClient()->indices()->putMapping($params); } + /** + * Get mapping from Elasticsearch index. + * + * @param array $params + * @return array + */ + public function getMapping(array $params): array + { + return $this->getClient()->indices()->getMapping($params); + } + /** * Delete mapping in Elasticsearch index * diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml index a15c8f5e30e86..75e355126ff2b 100644 --- a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml @@ -26,7 +26,9 @@ <magentoCLI command="config:set --scope={{GeneralLocalCodeConfigsForChina.scope}} --scope-code={{GeneralLocalCodeConfigsForChina.scope_code}} {{GeneralLocalCodeConfigsForChina.path}} {{GeneralLocalCodeConfigsForChina.value}}" stepKey="setLocaleToChina"/> <comment userInput="Moved to appropriate test suite" stepKey="enableElasticsearch6"/> <comment userInput="Moved to appropriate test suite" stepKey="checkConnection"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="ApiCategory" stepKey="createCategory"/> <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml index e173090bfa318..627105751507a 100644 --- a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml @@ -26,8 +26,12 @@ <!--Set Minimal Query Length--> <magentoCLI command="config:set {{SetMinQueryLength2Config.path}} {{SetMinQueryLength2Config.value}}" stepKey="setMinQueryLength"/> <!--Reindex indexes and clear cache--> - <magentoCLI command="indexer:reindex catalogsearch_fulltext" stepKey="reindex"/> - <magentoCLI command="cache:flush config" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <!--Set configs to default--> @@ -41,19 +45,21 @@ <argument name="productAttributeCode" value="{{textProductAttribute.attribute_code}}"/> </actionGroup> <actionGroup ref="AssertProductAttributeRemovedSuccessfullyActionGroup" stepKey="deleteProductAttributeSuccess"/> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> - <waitForPageLoad stepKey="waitForAttributePageLoad"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteProduct"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfExist"/> - <magentoCLI command="indexer:reindex catalogsearch_fulltext" stepKey="reindex"/> - <magentoCLI command="cache:flush config" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="config"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Create new searchable product attribute--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <actionGroup ref="AdminCreateSearchableProductAttributeActionGroup" stepKey="createAttribute"> <argument name="attribute" value="textProductAttribute"/> </actionGroup> @@ -78,10 +84,14 @@ <argument name="categoryName" value="$$createCategory.name$$"/> </actionGroup> <fillField selector="{{AdminProductFormSection.attributeRequiredInput(textProductAttribute.attribute_code)}}" userInput="searchable" stepKey="fillTheAttributeRequiredInputField"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush eav" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="eav"/> + </actionGroup> <!--Assert search results on storefront--> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForFirstSearchTerm"> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml index 9a025a6d04b14..653c460733976 100644 --- a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml @@ -31,7 +31,9 @@ </createData> <magentoCLI command="config:set {{CustomGridPerPageValuesConfigData.path}} {{CustomGridPerPageValuesConfigData.value}}" stepKey="setCustomGridPerPageValues"/> <magentoCLI command="config:set {{CustomGridPerPageDefaultConfigData.path}} {{CustomGridPerPageDefaultConfigData.value}}" stepKey="setCustomGridPerPageDefaults"/> - <magentoCLI stepKey="flushConfigCache" command="cache:flush config"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> <magentoCron groups="index" stepKey="runCronIndex"/> </before> diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php index aa0b49400c517..f52e24d72e4d4 100644 --- a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php @@ -73,6 +73,7 @@ protected function setUp(): void 'delete', 'putMapping', 'deleteMapping', + 'getMapping', 'stats', 'updateAliases', 'existsAlias', @@ -439,7 +440,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -461,10 +462,18 @@ public function testAddFieldsMapping() 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', ], ], - ] + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], ], ], ], @@ -509,7 +518,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -531,10 +540,18 @@ public function testAddFieldsMappingFailure() 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', + ], + ], + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', ], ], - ] + ], ], ], ], @@ -592,6 +609,22 @@ public function testDeleteMappingFailure() ); } + /** + * Test get Elasticsearch mapping process. + * + * @return void + */ + public function testGetMapping(): void + { + $params = ['index' => 'indexName']; + $this->indicesMock->expects($this->once()) + ->method('getMapping') + ->with($params) + ->willReturn([]); + + $this->model->getMapping($params); + } + /** * Test query() method * @return void diff --git a/app/code/Magento/Elasticsearch6/composer.json b/app/code/Magento/Elasticsearch6/composer.json index 36297b03198e2..1ee92c0b0a3b3 100644 --- a/app/code/Magento/Elasticsearch6/composer.json +++ b/app/code/Magento/Elasticsearch6/composer.json @@ -8,7 +8,7 @@ "magento/module-catalog-search": "*", "magento/module-search": "*", "magento/module-elasticsearch": "*", - "elasticsearch/elasticsearch": "~7.6" + "elasticsearch/elasticsearch": "~7.7.0" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/Elasticsearch6/etc/di.xml b/app/code/Magento/Elasticsearch6/etc/di.xml index 7263ae01f0f6d..e60f331f9ee8d 100644 --- a/app/code/Magento/Elasticsearch6/etc/di.xml +++ b/app/code/Magento/Elasticsearch6/etc/di.xml @@ -17,7 +17,7 @@ <type name="Magento\Search\Model\Adminhtml\System\Config\Source\Engine"> <arguments> <argument name="engines" xsi:type="array"> - <item sortOrder="20" name="elasticsearch6" xsi:type="string">Elasticsearch 6.x</item> + <item sortOrder="20" name="elasticsearch6" xsi:type="string">Elasticsearch 6.x (Deprecated)</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php index feacca8d62804..d193c8aa108c8 100644 --- a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php @@ -51,8 +51,10 @@ public function __construct( $elasticsearchClient = null, $fieldsMappingPreprocessors = [] ) { - if (empty($options['hostname']) || ((!empty($options['enableAuth']) && - ($options['enableAuth'] == 1)) && (empty($options['username']) || empty($options['password'])))) { + if (empty($options['hostname']) + || ((!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) + && (empty($options['username']) || empty($options['password']))) + ) { throw new LocalizedException( __('The search failed because of a search engine misconfiguration.') ); @@ -71,7 +73,7 @@ public function __construct( * @param array $query * @return array */ - public function suggest(array $query) : array + public function suggest(array $query): array { return $this->getElasticsearchClient()->suggest($query); } @@ -96,7 +98,7 @@ private function getElasticsearchClient(): \Elasticsearch\Client * * @return bool */ - public function ping() : bool + public function ping(): bool { if ($this->pingResult === null) { $this->pingResult = $this->getElasticsearchClient() @@ -111,7 +113,7 @@ public function ping() : bool * * @return bool */ - public function testConnection() : bool + public function testConnection(): bool { return $this->ping(); } @@ -122,7 +124,7 @@ public function testConnection() : bool * @param array $options * @return array */ - private function buildESConfig(array $options = []) : array + private function buildESConfig(array $options = []): array { $hostname = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); // @codingStandardsIgnoreStart @@ -177,6 +179,23 @@ public function createIndex(string $index, array $settings) ); } + /** + * Add/update an Elasticsearch index settings. + * + * @param string $index + * @param array $settings + * @return void + */ + public function putIndexSettings(string $index, array $settings): void + { + $this->getElasticsearchClient()->indices()->putSettings( + [ + 'index' => $index, + 'body' => $settings, + ] + ); + } + /** * Delete an Elasticsearch 7 index. * @@ -194,12 +213,13 @@ public function deleteIndex(string $index) * @param string $index * @return bool */ - public function isEmptyIndex(string $index) : bool + public function isEmptyIndex(string $index): bool { $stats = $this->getElasticsearchClient()->indices()->stats(['index' => $index, 'metric' => 'docs']); - if ($stats['indices'][$index]['primaries']['docs']['count'] === 0) { + if ($stats['indices'][$index]['primaries']['docs']['count'] === 0) { return true; } + return false; } @@ -234,7 +254,7 @@ public function updateAlias(string $alias, string $newIndex, string $oldIndex = * @param string $index * @return bool */ - public function indexExists(string $index) : bool + public function indexExists(string $index): bool { return $this->getElasticsearchClient()->indices()->exists(['index' => $index]); } @@ -246,12 +266,13 @@ public function indexExists(string $index) : bool * @param string $index * @return bool */ - public function existsAlias(string $alias, string $index = '') : bool + public function existsAlias(string $alias, string $index = ''): bool { $params = ['name' => $alias]; if ($index) { $params['index'] = $index; } + return $this->getElasticsearchClient()->indices()->existsAlias($params); } @@ -261,7 +282,7 @@ public function existsAlias(string $alias, string $index = '') : bool * @param string $alias * @return array */ - public function getAlias(string $alias) : array + public function getAlias(string $alias): array { return $this->getElasticsearchClient()->indices()->getAlias(['name' => $alias]); } @@ -289,7 +310,7 @@ public function addFieldsMapping(array $fields, string $index, string $entityTyp 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -311,7 +332,15 @@ public function addFieldsMapping(array $fields, string $index, string $entityTyp 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', + ], + ], + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', ], ], ], @@ -333,11 +362,22 @@ public function addFieldsMapping(array $fields, string $index, string $entityTyp * @param array $query * @return array */ - public function query(array $query) : array + public function query(array $query): array { return $this->getElasticsearchClient()->search($query); } + /** + * Get mapping from Elasticsearch index. + * + * @param array $params + * @return array + */ + public function getMapping(array $params): array + { + return $this->getElasticsearchClient()->indices()->getMapping($params); + } + /** * Delete mapping in Elasticsearch 7 index * diff --git a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php index 593bbd7792f46..3b3cbcfbb15f8 100644 --- a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php @@ -73,6 +73,7 @@ protected function setUp(): void 'delete', 'putMapping', 'deleteMapping', + 'getMapping', 'stats', 'updateAliases', 'existsAlias', @@ -438,7 +439,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -460,10 +461,18 @@ public function testAddFieldsMapping() 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', ], ], - ] + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], ], ], ], @@ -509,7 +518,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -531,10 +540,18 @@ public function testAddFieldsMappingFailure() 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', + ], + ], + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', ], ], - ] + ], ], ], ], @@ -592,6 +609,22 @@ public function testDeleteMappingFailure() ); } + /** + * Test get Elasticsearch mapping process. + * + * @return void + */ + public function testGetMapping(): void + { + $params = ['index' => 'indexName']; + $this->indicesMock->expects($this->once()) + ->method('getMapping') + ->with($params) + ->willReturn([]); + + $this->model->getMapping($params); + } + /** * Test query() method * @return void diff --git a/app/code/Magento/Elasticsearch7/composer.json b/app/code/Magento/Elasticsearch7/composer.json index 739ac1019c5ae..1e59ceaebaf84 100644 --- a/app/code/Magento/Elasticsearch7/composer.json +++ b/app/code/Magento/Elasticsearch7/composer.json @@ -5,7 +5,7 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-elasticsearch": "*", - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "magento/module-advanced-search": "*", "magento/module-catalog-search": "*" }, diff --git a/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml b/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml index 52e5be5a5beeb..9e818ff61eb89 100644 --- a/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml +++ b/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml @@ -10,7 +10,7 @@ <system> <section id="catalog"> <group id="search"> - <!-- Elasticsearch 7.0+ --> + <!-- Elasticsearch 7 --> <field id="elasticsearch7_server_hostname" translate="label" type="text" sortOrder="61" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Elasticsearch Server Hostname</label> diff --git a/app/code/Magento/Elasticsearch7/etc/di.xml b/app/code/Magento/Elasticsearch7/etc/di.xml index b5d013a294e26..446331edc63fb 100644 --- a/app/code/Magento/Elasticsearch7/etc/di.xml +++ b/app/code/Magento/Elasticsearch7/etc/di.xml @@ -17,7 +17,7 @@ <type name="Magento\Search\Model\Adminhtml\System\Config\Source\Engine"> <arguments> <argument name="engines" xsi:type="array"> - <item sortOrder="30" name="elasticsearch7" xsi:type="string">Elasticsearch 7.0+</item> + <item sortOrder="30" name="elasticsearch7" xsi:type="string">Elasticsearch 7</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Email/Block/Adminhtml/Template/Edit.php b/app/code/Magento/Email/Block/Adminhtml/Template/Edit.php index 112813c3b096c..7beb266508cc9 100644 --- a/app/code/Magento/Email/Block/Adminhtml/Template/Edit.php +++ b/app/code/Magento/Email/Block/Adminhtml/Template/Edit.php @@ -20,7 +20,7 @@ class Edit extends Widget implements ContainerInterface { /** * @var \Magento\Framework\Registry - * @deprecated since 2.3.0 in favor of stateful global objects elimination. + * @deprecated 101.0.0 since 2.3.0 in favor of stateful global objects elimination. */ protected $_registryManager; diff --git a/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php b/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php index 2cd3ea42649c1..ec97d462c0f74 100644 --- a/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php +++ b/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php @@ -9,8 +9,13 @@ */ namespace Magento\Email\Block\Adminhtml\Template\Edit; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Adminhtml email template edit form block + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Form extends \Magento\Backend\Block\Widget\Form\Generic { @@ -29,6 +34,11 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic */ private $serializer; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -37,6 +47,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Variable\Model\Source\Variables $variables * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param SecureHtmlRenderer|null $secureRenderer * @throws \RuntimeException */ public function __construct( @@ -46,12 +57,14 @@ public function __construct( \Magento\Variable\Model\VariableFactory $variableFactory, \Magento\Variable\Model\Source\Variables $variables, array $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_variableFactory = $variableFactory; $this->_variables = $variables; $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); parent::__construct($context, $registry, $formFactory, $data); } @@ -93,11 +106,16 @@ protected function _prepareForm() [ 'label' => __('Currently Used For'), 'container_id' => 'currently_used_for', - 'after_element_html' => '<script>require(["prototype"], function () {' . - (!$this->getEmailTemplate()->getSystemConfigPathsWhereCurrentlyUsed() ? '$(\'' . - 'currently_used_for' . - '\').hide(); ' : '') . - '});</script>' + 'after_element_html' => $this->secureRenderer->renderTag( + 'script', + [], + 'require(["prototype"], function () {' . + (!$this->getEmailTemplate()->getSystemConfigPathsWhereCurrentlyUsed() ? '$(\'' . + 'currently_used_for' . + '\').hide(); ' : '') . + '});', + false + ), ] ); diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php index 50153b2bb6520..5af5230b0e33d 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php @@ -23,7 +23,7 @@ abstract class Template extends \Magento\Backend\App\Action * Core registry * * @var \Magento\Framework\Registry - * @deprecated since 2.3.0 in favor of stateful global objects elimination. + * @deprecated 101.0.0 since 2.3.0 in favor of stateful global objects elimination. */ protected $_coreRegistry = null; diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index 82ebc8c78d8b2..648e4ab8fc380 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -6,8 +6,9 @@ namespace Magento\Email\Model\Template; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\MailException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filter\VariableResolverInterface; use Magento\Framework\View\Asset\ContentProcessorException; use Magento\Framework\View\Asset\ContentProcessorInterface; @@ -44,6 +45,7 @@ class Filter extends \Magento\Framework\Filter\Template * Whether to allow SID in store directive: NO * * @var bool + * @deprecated SID is not being used as query parameter anymore. */ protected $_useSessionInUrl = false; @@ -51,7 +53,7 @@ class Filter extends \Magento\Framework\Filter\Template * Modifier Callbacks * * @var array - * @deprecated Use the new Directive Processor interfaces + * @deprecated 101.0.4 Use the new Directive Processor interfaces */ protected $_modifiers = ['nl2br' => '']; @@ -263,10 +265,14 @@ public function setUseAbsoluteLinks($flag) * * @param bool $flag * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @deprecated SID query parameter is not used in URLs anymore. */ public function setUseSessionInUrl($flag) { - $this->_useSessionInUrl = $flag; + // phpcs:disable Magento2.Functions.DiscouragedFunction + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); + return $this; } @@ -659,7 +665,7 @@ public function varDirective($construction) * @param string $value * @param string $default assumed modifier if none present * @return array - * @deprecated Use the new FilterApplier or Directive Processor interfaces + * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces */ protected function explodeModifiers($value, $default = null) { @@ -678,7 +684,7 @@ protected function explodeModifiers($value, $default = null) * @param string $value * @param string $modifiers * @return string - * @deprecated Use the new FilterApplier or Directive Processor interfaces + * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces */ protected function applyModifiers($value, $modifiers) { @@ -706,7 +712,7 @@ protected function applyModifiers($value, $modifiers) * @param string $value * @param string $type * @return string - * @deprecated Use the new FilterApplier or Directive Processor interfaces + * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces */ public function modifierEscape($value, $type = 'html') { @@ -734,27 +740,32 @@ public function modifierEscape($value, $type = 'html') * {{protocol store="1"}} - Optional parameter which gets protocol from provide store based on store ID or code * * @param string[] $construction - * @throws \Magento\Framework\Exception\MailException * @return string + * @throws MailException + * @throws NoSuchEntityException */ public function protocolDirective($construction) { $params = $this->getParameters($construction[2]); + $store = null; if (isset($params['store'])) { try { $store = $this->_storeManager->getStore($params['store']); } catch (\Exception $e) { - throw new \Magento\Framework\Exception\MailException( + throw new MailException( __('Requested invalid store "%1"', $params['store']) ); } } + $isSecure = $this->_storeManager->getStore($store)->isCurrentlySecure(); $protocol = $isSecure ? 'https' : 'http'; if (isset($params['url'])) { return $protocol . '://' . $params['url']; } elseif (isset($params['http']) && isset($params['https'])) { + $this->validateProtocolDirectiveHttpScheme($params); + if ($isSecure) { return $params['https']; } @@ -764,6 +775,37 @@ public function protocolDirective($construction) return $protocol; } + /** + * Validate protocol directive HTTP parameters. + * + * @param string[] $params + * @return void + * @throws MailException + */ + private function validateProtocolDirectiveHttpScheme(array $params) : void + { + $parsed_http = parse_url($params['http']); + $parsed_https = parse_url($params['https']); + + if (empty($parsed_http)) { + throw new MailException( + __('Contents of %1 could not be loaded or is empty', $params['http']) + ); + } elseif (empty($parsed_https)) { + throw new MailException( + __('Contents of %1 could not be loaded or is empty', $params['https']) + ); + } elseif ($parsed_http['scheme'] !== 'http') { + throw new MailException( + __('Contents of %1 could not be loaded or is empty', $params['http']) + ); + } elseif ($parsed_https['scheme'] !== 'https') { + throw new MailException( + __('Contents of %1 could not be loaded or is empty', $params['https']) + ); + } + } + /** * Store config directive * diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php index 2589d88476725..ac890dd3d4a73 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php @@ -14,6 +14,8 @@ use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\MailException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\App\State; use Magento\Framework\Css\PreProcessor\Adapter\CssInliner; use Magento\Framework\Escaper; @@ -452,4 +454,45 @@ public function testConfigDirectiveUnavailable() $this->assertEquals($scopeConfigValue, $this->getModel()->configDirective($construction)); } + + /** + * @throws MailException + * @throws NoSuchEntityException + */ + public function testProtocolDirectiveWithValidSchema() + { + $model = $this->getModel(); + $storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $storeMock->expects($this->once())->method('isCurrentlySecure')->willReturn(true); + $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); + + $data = [ + "{{protocol http=\"http://url\" https=\"https://url\"}}", + "protocol", + " http=\"http://url\" https=\"https://url\"" + ]; + $this->assertEquals('https://url', $model->protocolDirective($data)); + } + + /** + * @throws NoSuchEntityException + */ + public function testProtocolDirectiveWithInvalidSchema() + { + $this->expectException( + \Magento\Framework\Exception\MailException::class + ); + + $model = $this->getModel(); + $storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $storeMock->expects($this->once())->method('isCurrentlySecure')->willReturn(true); + $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); + + $data = [ + "{{protocol http=\"https://url\" https=\"http://url\"}}", + "protocol", + " http=\"https://url\" https=\"http://url\"" + ]; + $model->protocolDirective($data); + } } diff --git a/app/code/Magento/Email/composer.json b/app/code/Magento/Email/composer.json index a85c6177c8a48..334bbcf9d4617 100644 --- a/app/code/Magento/Email/composer.json +++ b/app/code/Magento/Email/composer.json @@ -14,7 +14,8 @@ "magento/module-theme": "*", "magento/module-require-js": "*", "magento/module-media-storage": "*", - "magento/module-variable": "*" + "magento/module-variable": "*", + "magento/module-ui": "*" }, "suggest": { "magento/module-theme": "*" diff --git a/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml index 29ceb71a138e4..900c527dcff17 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -5,6 +5,7 @@ */ /** @var \Magento\Backend\Block\Page $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="preview" class="cms-revision-preview"> <iframe name="preview_iframe" @@ -20,12 +21,12 @@ target="preview_iframe" > <input type="hidden" name="form_key" value="<?= /* @noEscape */ $block->getFormKey() ?>" /> - <?php foreach ($block->getPreviewFormViewModel()->getFormFields() as $name => $value) : ?> + <?php foreach ($block->getPreviewFormViewModel()->getFormFields() as $name => $value): ?> <input type="hidden" name="<?= $block->escapeHtmlAttr($name) ?>" value="<?= $block->escapeHtmlAttr($value) ?>"/> <?php endforeach; ?> </form> </div> -<script> +<?php $scriptString = <<<script require([ 'jquery' ], function($) { @@ -37,4 +38,6 @@ require([ $(this).height($(this).contents().height()); }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> 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 0dceb9d51a99e..a377cd8ae6722 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml @@ -9,6 +9,7 @@ use Magento\Framework\App\TemplateTypesInterface; // phpcs:disable Generic.Files.LineLength.TooLong /** @var $block \Magento\Email\Block\Adminhtml\Template\Edit */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php if (!$block->getEditMode()): ?> <form action="<?= $block->escapeUrl($block->getLoadUrl()) ?>" method="post" id="email_template_load_form"> @@ -16,7 +17,9 @@ use Magento\Framework\App\TemplateTypesInterface; <fieldset class="admin__fieldset form-inline"> <legend class="admin__legend"><span><?= $block->escapeHtml(__('Load Default Template')) ?></span></legend><br> <div class="admin__field required"> - <label class="admin__field-label" for="template_select"><span><?= $block->escapeHtml(__('Template')) ?></span></label> + <label class="admin__field-label" for="template_select"> + <span><?= $block->escapeHtml(__('Template')) ?></span> + </label> <div class="admin__field-control"> <select id="template_select" name="code" class="admin__control-select required-entry"> <?php foreach ($block->getTemplateOptions() as $group => $options): ?> @@ -24,7 +27,10 @@ use Magento\Framework\App\TemplateTypesInterface; <optgroup label="<?= $block->escapeHtmlAttr($group) ?>"> <?php endif; ?> <?php foreach ($options as $option): ?> - <option value="<?= $block->escapeHtmlAttr($option['value']) ?>"<?= /* @noEscape */ $block->getOrigTemplateCode() == $option['value'] ? ' selected="selected"' : '' ?>><?= $block->escapeHtml($option['label']) ?></option> + <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" + <?= /* @noEscape */ $block->getOrigTemplateCode() == $option['value'] ? + ' selected="selected"' : '' ?>><?= $block->escapeHtml($option['label']) ?> + </option> <?php endforeach; ?> <?php if ($group): ?> </optgroup> @@ -46,19 +52,26 @@ use Magento\Framework\App\TemplateTypesInterface; <form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="email_template_edit_form"> <?= /* @noEscape */ $block->getBlockHtml('formkey') ?> <input type="hidden" id="change_flag_element" name="_change_type_flag" value="" /> - <input type="hidden" id="orig_template_code" name="orig_template_code" value="<?= $block->escapeHtmlAttr($block->getOrigTemplateCode()) ?>" /> + <input type="hidden" id="orig_template_code" name="orig_template_code" + value="<?= $block->escapeHtmlAttr($block->getOrigTemplateCode()) ?>" /> <?= /* @noEscape */ $block->getFormHtml() ?> </form> -<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="email_template_preview_form" target="_blank"> +<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="email_template_preview_form" + target="_blank"> <?= /* @noEscape */ $block->getBlockHtml('formkey') ?> <div class="no-display"> - <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>" /> + <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>"/> <input type="hidden" id="preview_text" name="text" value="" /> <input type="hidden" id="preview_styles" name="styles" value="" /> </div> </form> -<script> +<?php +$currentlyUsedForPaths = /* @noEscape */ $block->getCurrentlyUsedForPaths(); +$templateType = (int)$block->getTemplateType(); +$typeText = /* @noEscape */ TemplateTypesInterface::TYPE_TEXT; +$scriptString = <<<script + require([ "jquery", "wysiwygAdapter", @@ -94,7 +107,7 @@ require([ this.bindEvents(); - this.renderPaths(<?= /* @noEscape */ $block->getCurrentlyUsedForPaths() ?>, 'currently_used_for'); + this.renderPaths({$currentlyUsedForPaths}, 'currently_used_for'); }, bindEvents: function(){ @@ -119,10 +132,10 @@ require([ var self = this; confirm({ - content: "<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to strip tags?'))) ?>", + 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'), '' @@ -153,9 +166,9 @@ require([ }, preview: function() { if (this.typeChange) { - $('preview_type').value = <?= /* @noEscape */ TemplateTypesInterface::TYPE_TEXT ?>; + $('preview_type').value = {$typeText}; } else { - $('preview_type').value = <?= (int) $block->getTemplateType() ?>; + $('preview_type').value = {$templateType}; } if (typeof tinyMCE == 'undefined' || !tinyMCE.get('template_text')) { @@ -175,10 +188,10 @@ require([ deleteTemplate: function() { confirm({ - content: "<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to delete this template?'))) ?>", + content: "{$block->escapeJs(__('Are you sure you want to delete this template?'))}", actions: { confirm: function () { - window.location.href = '<?= $block->escapeJs($block->escapeUrl($block->getDeleteUrl())) ?>'; + window.location.href = '{$block->escapeJs($block->getDeleteUrl())}'; } } }); @@ -197,7 +210,7 @@ require([ area: $('email_template_load_form'), onComplete: function (transport) { if (transport.responseText.isJSON()) { - var fields = $H(transport.responseText.evalJSON()); + var fields = \$H(transport.responseText.evalJSON()); fields.each(function(pair) { if ($(pair.key)) { $(pair.key).value = pair.value.strip(); @@ -225,7 +238,9 @@ require([ }.bind(this)); } else { alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('The template did not load. Please review the log for details.'))) ?>' + content: '{$block->escapeJs(__( + 'The template did not load. Please review the log for details.' + ))}' }); } }.bind(this) @@ -236,7 +251,8 @@ require([ renderPaths: function(paths, fieldId) { var field = $(fieldId); if (field) { - field.down('div').down('div').update(this.parsePath(paths, '<span class="path-delimiter"> -> </span>', '<br />')); + field.down('div').down('div') + .update(this.parsePath(paths, '<span class="path-delimiter"> -> </span>', '<br />')); } }, @@ -250,7 +266,8 @@ require([ } if(!Object.isString(value) && value.title) { - value = (value.url ? '<a href="' + value.url + '">' + value.title + '</a>' : value.title) + (value.scope ? '  <span class="path-scope-label">(' + value.scope + ')</span>' : ''); + value = (value.url ? '<a href="' + value.url + '">' + value.title + '</a>' : value.title) + + (value.scope ? '  <span class="path-scope-label">(' + value.scope + ')</span>' : ''); } return value; @@ -278,4 +295,6 @@ require([ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.xml index 7ce37af60fd7f..b0bf115915c2d 100644 --- a/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.xml +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminEncryptionKeyChangeFormSection"> <element name="autoGenerate" type="select" selector="#generate_random"/> - <element name="cryptKey" type="input" selector="#crypt_key"/> + <element name="cryptKey" type="input" selector="input#crypt_key"/> <element name="changeEncryptionKey" type="button" selector=".page-actions-buttons #save" timeout="10"/> </section> </sections> diff --git a/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml index 1c61bd290f005..2144f486f9bde 100644 --- a/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml +++ b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml @@ -50,8 +50,12 @@ <magentoCLI command="config:set {{AdminShippingSettingsOriginZipCodeConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.postcode}}" stepKey="setOriginZipCode"/> <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddressConfigData.path}} '{{DE_Address_Berlin_Not_Default_Address.street[0]}}'" stepKey="setOriginStreetAddress"/> <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddress2ConfigData.path}} '{{US_Address_California.street[0]}}'" stepKey="setOriginStreetAddress2"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!--Reset configs--> @@ -70,8 +74,12 @@ <magentoCLI command="config:set {{AdminShippingSettingsOriginZipCodeConfigData.path}} ''" stepKey="setOriginZipCode"/> <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddressConfigData.path}} ''" stepKey="setOriginStreetAddress"/> <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddress2ConfigData.path}} ''" stepKey="setOriginStreetAddress2"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Delete created data--> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -105,7 +113,7 @@ </actionGroup> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> <!--Open created order in admin--> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrdersPage"/> <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchOrder"> <argument name="keyword" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl index 62795f07239a6..3629bb424f207 100644 --- a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl @@ -472,7 +472,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -983,7 +983,7 @@ </xs:element> <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> <xs:annotation> - <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the master transaction and all child transactions</xs:documentation> + <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions</xs:documentation> </xs:annotation> </xs:element> <xs:element name="FreightOnValue" type="ns:FreightOnValueType" minOccurs="0"> @@ -1005,7 +1005,7 @@ <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="99"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -4867,4 +4867,4 @@ <s1:address location="https://wsbeta.fedex.com:443/web-services/rate"/> </port> </service> -</definitions> \ No newline at end of file +</definitions> diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl index 17a6f74cc09b8..2f3feecb58084 100644 --- a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl @@ -471,7 +471,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment commitment more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -983,7 +983,7 @@ </xs:element> <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> <xs:annotation> - <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the master transaction and all child transactions</xs:documentation> + <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions</xs:documentation> </xs:annotation> </xs:element> <xs:element name="FreightOnValue" type="ns:FreightOnValueType" minOccurs="0"> @@ -1005,7 +1005,7 @@ <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="99"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl index 54bb57d490c76..439d032a61fd0 100644 --- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl @@ -497,7 +497,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -724,7 +724,7 @@ </xs:element> <xs:element name="MasterTrackingId" type="ns:TrackingId" minOccurs="0"> <xs:annotation> - <xs:documentation>The master tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> + <xs:documentation>The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="ServiceTypeDescription" type="xs:string" minOccurs="0"> diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl index d8dc0fdfed4ab..a449bf41dbd68 100644 --- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl @@ -497,7 +497,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -724,7 +724,7 @@ </xs:element> <xs:element name="MasterTrackingId" type="ns:TrackingId" minOccurs="0"> <xs:annotation> - <xs:documentation>The master tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> + <xs:documentation>The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="ServiceTypeDescription" type="xs:string" minOccurs="0"> diff --git a/app/code/Magento/GiftMessage/Block/Message/Inline.php b/app/code/Magento/GiftMessage/Block/Message/Inline.php index 4a9311c1b4ba2..475f1c2b717ae 100644 --- a/app/code/Magento/GiftMessage/Block/Message/Inline.php +++ b/app/code/Magento/GiftMessage/Block/Message/Inline.php @@ -280,7 +280,7 @@ public function countItems() /** * Call method getItemsHasMessages * - * @deprecated Misspelled method + * @deprecated 100.2.4 Misspelled method * @see getItemsHasMessages */ public function getItemsHasMesssages() diff --git a/app/code/Magento/GiftMessage/view/adminhtml/templates/popup.phtml b/app/code/Magento/GiftMessage/view/adminhtml/templates/popup.phtml index 908d6c91bfb5f..e45446450f872 100644 --- a/app/code/Magento/GiftMessage/view/adminhtml/templates/popup.phtml +++ b/app/code/Magento/GiftMessage/view/adminhtml/templates/popup.phtml @@ -3,26 +3,42 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->getChildHtml()) :?> -<div id="gift_options_configure_new" class="gift-options-popup product-configure-popup" style="display: none;"> +<?php if ($block->getChildHtml()):?> +<div id="gift_options_configure_new" class="gift-options-popup product-configure-popup no-display"> <div id="gift_options_form_contents"> <div class="content"> <?= $block->getChildHtml() ?> </div> <div class="ui-dialog-buttonset"> - <button type="button" class="action-close" id="gift_options_cancel_button"><span><?= $block->escapeHtml(__('Cancel')) ?></span></button> - <button type="button" class="action-primary" id="gift_options_ok_button"><span><?= $block->escapeHtml(__('OK')) ?></span></button> + <button type="button" class="action-close" id="gift_options_cancel_button"> + <span><?= $block->escapeHtml(__('Cancel')) ?></span> + </button> + <button type="button" class="action-primary" id="gift_options_ok_button"> + <span><?= $block->escapeHtml(__('OK')) ?></span> + </button> </div> </div> </div> + <div id="giftoptions_tooltip_window" class="gift-options-tooltip no-display"> + <div id="giftoptions_tooltip_window_content"> </div> + </div> + <?php $scriptString = <<<script + require(['jquery'], function($){ + 'use strict'; + $('div#gift_options_configure_new').css('display', 'none'); + $('div#gift_options_configure_new').removeClass('no-display'); -<div id="giftoptions_tooltip_window" class="gift-options-tooltip" style="display: none;"> - <div id="giftoptions_tooltip_window_content"> </div> -</div> - -<script> + $('div#giftoptions_tooltip_window').css('display', 'none'); + $('div#giftoptions_tooltip_window').removeClass('no-display'); + }); +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <?php $scriptString = <<<script require([ "Magento_Sales/order/create/giftmessage", "Magento_Sales/order/giftoptions_tooltip" @@ -36,5 +52,7 @@ giftOptionsTooltip.setTooltipWindow('giftoptions_tooltip_window','giftoptions_to //]]> window.giftMessageSet = giftMessageSet; }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> diff --git a/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/create/giftoptions.phtml b/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/create/giftoptions.phtml index 1833ae0d2e339..41b77ad74d148 100644 --- a/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/create/giftoptions.phtml +++ b/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/create/giftoptions.phtml @@ -3,24 +3,32 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> -<?php if ($_item) : ?> +<?php if ($_item): ?> <?php $_childHtml = trim($block->getChildHtml('', false));?> - <?php if ($_childHtml) : ?> + <?php if ($_childHtml): ?> <tr class="row-gift-options"> <td colspan="7"> - <a class="action-link" href="#" id="gift_options_link_<?= (int) $_item->getId() ?>"><?= $block->escapeHtml(__('Gift Options')) ?></a> - <script> + <a class="action-link" href="#" id="gift_options_link_<?= (int) $_item->getId() ?>"> + <?= $block->escapeHtml(__('Gift Options')) ?> + </a> + <?php $itemId = (int) ($_item->getId()); + $scriptString = <<<script + require([ "Magento_Sales/order/giftoptions_tooltip" ], function(){ - giftOptionsTooltip.addTargetLink('gift_options_link_<?= (int) $_item->getId() ?>', <?= (int) $_item->getId() ?>); + giftOptionsTooltip.addTargetLink('gift_options_link_{$itemId}', {$itemId}); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <div id="gift_options_data_<?= (int) $_item->getId() ?>"> <?= /* @noEscape */ $_childHtml ?> </div> diff --git a/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/view/giftoptions.phtml b/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/view/giftoptions.phtml index 60a6e1b222b17..1c2486d5471e4 100644 --- a/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/view/giftoptions.phtml +++ b/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/view/giftoptions.phtml @@ -3,26 +3,30 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_childHtml = trim($block->getChildHtml('', false)); ?> -<?php if ($_childHtml) : ?> +<?php if ($_childHtml): ?> <?php $_item = $block->getItem() ?> <tr> <td colspan="10" class="last"> <a class="action-link" href="#" id="gift_options_link_<?= (int) $_item->getId() ?>"> <?= $block->escapeHtml(__('Gift Options')) ?> </a> - <script> + <?php $itemId = (int) ($_item->getId()); + $scriptString = <<<script require([ "Magento_Sales/order/giftoptions_tooltip" ], function(){ giftOptionsTooltip.addTargetLink( - 'gift_options_link_<?= (int) ($_item->getId()) ?>', - <?= (int) $_item->getId() ?> + 'gift_options_link_{$itemId}', {$itemId} ); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <div id="gift_options_data_<?= (int) $_item->getId() ?>"> <?= /* @noEscape */ $_childHtml ?> </div> diff --git a/app/code/Magento/GiftMessage/view/frontend/templates/cart/gift_options.phtml b/app/code/Magento/GiftMessage/view/frontend/templates/cart/gift_options.phtml index 4d8a054e67fc5..3d814ac41302d 100644 --- a/app/code/Magento/GiftMessage/view/frontend/templates/cart/gift_options.phtml +++ b/app/code/Magento/GiftMessage/view/frontend/templates/cart/gift_options.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="gift-options-cart" data-bind="scope:'giftOptionsCart'"> <!-- ko template: getTemplate() --><!-- /ko --> @@ -13,7 +15,10 @@ } } </script> - <script> - window.giftOptionsConfig = <?= /* @noEscape */ $block->getGiftOptionsConfigJson() ?>; - </script> +<?= /* @noEscape */ $secureRenderer->renderTag( + 'script', + [], + 'window.giftOptionsConfig = '. /* @noEscape */ $block->getGiftOptionsConfigJson(), + false +) ?> </div> diff --git a/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml b/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml index cb462a630e3a6..f89657d3c5d90 100644 --- a/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml +++ b/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml @@ -3,73 +3,118 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Squiz.ControlStructures.ControlSignature -//phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace -//phpcs:disable PSR2.ControlStructures.SwitchDeclaration -//phpcs:disable Magento2.Files.LineLength.MaxExceeded + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php $_giftMessage = false; ?> -<?php switch ($block->getCheckoutType()) : case 'onepage_checkout': ?> +<?php $_giftMessage = false; +switch ($block->getCheckoutType()): + case 'onepage_checkout': + ?> <fieldset class="fieldset gift-message"> - <legend class="legend"><span><?= $block->escapeHtml(__('Do you have any gift items in your order?')) ?></span></legend><br> + <legend class="legend"> + <span><?= $block->escapeHtml(__('Do you have any gift items in your order?')) ?></span> + </legend><br> <div class="field choice" id="add-gift-options-<?= (int) $block->getEntity()->getId() ?>"> - <input type="checkbox" name="allow_gift_options" id="allow_gift_options" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-container"}'<?php if ($block->getItemsHasMesssages() || $block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options" class="label"><span><?= $block->escapeHtml(__('Add Gift Options')) ?></span></label> + <input type="checkbox" name="allow_gift_options" id="allow_gift_options" data-mage-init='{"giftOptions":{}}' + value="1" data-selector='{"id":"#allow-gift-options-container"}' + <?php if ($block->getItemsHasMesssages() || $block->getEntityHasMessage()):?> + checked="checked"<?php endif; ?> class="checkbox" /> + <label for="allow_gift_options" class="label"> + <span><?= $block->escapeHtml(__('Add Gift Options')) ?></span> + </label> </div> <dl class="options-items" id="allow-gift-options-container"> <?php if ($block->isMessagesAvailable()): ?> <dt id="add-gift-options-for-order-<?= (int) $block->getEntity()->getId() ?>" class="order-title"> <div class="field choice"> - <input type="checkbox" name="allow_gift_messages_for_order" id="allow_gift_options_for_order" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-for-order-container"}'<?php if ($block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options_for_order" class="label"><span><?= $block->escapeHtml(__('Gift Options for the Entire Order')) ?></span></label> + <input type="checkbox" name="allow_gift_messages_for_order" id="allow_gift_options_for_order" + data-mage-init='{"giftOptions":{}}' value="1" + data-selector='{"id":"#allow-gift-options-for-order-container"}' + <?php if ($block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> + class="checkbox" /> + <label for="allow_gift_options_for_order" class="label"> + <span><?= $block->escapeHtml(__('Gift Options for the Entire Order')) ?></span> + </label> </div> </dt> <dd id="allow-gift-options-for-order-container" class="order-options"> - <div class="options-order-container" id="options-order-container-<?= (int) $block->getEntity()->getId() ?>"></div> + <div class="options-order-container" + id="options-order-container-<?= (int) $block->getEntity()->getId() ?>"></div> <button class="action action-gift" - data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", "toggleContainers":"#allow-gift-messages-for-order-container"}}'> + data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", + "toggleContainers":"#allow-gift-messages-for-order-container"}}'> <span><?= $block->escapeHtml(__('Gift Message')) ?></span> </button> <div id="allow-gift-messages-for-order-container" class="gift-messages-order hidden"> <fieldset class="fieldset"> - <p><?= $block->escapeHtml(__('Leave this box blank if you don\'t want to leave a gift message for the entire order.')) ?></p> + <p><?= $block->escapeHtml(__( + 'Leave this box blank if you don\'t want to leave a gift message for the entire order.' + )) ?></p> <div class="field from"> - <label for="gift-message-whole-from" class="label"><span><?= $block->escapeHtml(__('From')) ?></span></label> + <label for="gift-message-whole-from" class="label"> + <span><?= $block->escapeHtml(__('From')) ?></span></label> <div class="control"> - <input type="text" name="giftmessage[quote][<?= (int) $block->getEntity()->getId() ?>][from]" id="gift-message-whole-from" title="<?= $block->escapeHtmlAttr(__('From')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage()->getSender(), $block->getDefaultFrom()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote][<?= (int) $block->getEntity()->getId() ?>][from]" + id="gift-message-whole-from" + title="<?= $block->escapeHtmlAttr(__('From')) ?>" + value="<?= /* @noEscape */ $block + ->getEscaped($block->getMessage()->getSender(), $block->getDefaultFrom()) + ?>" + class="input-text"> </div> </div> <div class="field to"> - <label for="gift-message-whole-to" class="label"><span><?= $block->escapeHtml(__('To')) ?></span></label> + <label for="gift-message-whole-to" class="label"> + <span><?= $block->escapeHtml(__('To')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote][<?= (int) $block->getEntity()->getId() ?>][to]" id="gift-message-whole-to" title="<?= $block->escapeHtmlAttr(__('To')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage()->getRecipient(), $block->getDefaultTo()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote][<?= (int) $block->getEntity()->getId() ?>][to]" + id="gift-message-whole-to" title="<?= $block->escapeHtmlAttr(__('To')) ?>" + value="<?= /* @noEscape */ $block + ->getEscaped($block->getMessage()->getRecipient(), $block->getDefaultTo()) + ?>" class="input-text"> </div> </div> <div class="field text"> - <label for="gift-message-whole-message" class="label"><span><?= $block->escapeHtml(__('Message')) ?></span></label> + <label for="gift-message-whole-message" class="label"> + <span><?= $block->escapeHtml(__('Message')) ?></span> + </label> <div class="control"> - <textarea id="gift-message-whole-message" class="input-text" name="giftmessage[quote][<?= (int) $block->getEntity()->getId() ?>][message]" title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="10"><?= /* @noEscape */ $block->getEscaped($block->getMessage()->getMessage()) ?></textarea> + <textarea id="gift-message-whole-message" class="input-text" + name="giftmessage[quote][<?=(int)$block->getEntity()->getId()?>][message]" + title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="10"><?= /* @noEscape */ $block->getEscaped($block->getMessage()->getMessage()) ?></textarea> </div> </div> </fieldset> - <script> + <?php $entityId = (int) $block->getEntity()->getId(); + $scriptString = <<<script require(['jquery'], function(jQuery){ - jQuery('#add-gift-options-<?= (int) $block->getEntity()->getId() ?>') - .add('#add-gift-options-for-order-<?= (int) $block->getEntity()->getId() ?>') + jQuery('#add-gift-options-{$entityId}') + .add('#add-gift-options-for-order-{$entityId}') .removeClass('hidden'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </dd> <?php endif ?> <?php if ($block->isItemsAvailable()): ?> - <dt id="add-gift-options-for-items-<?= (int) $block->getEntity()->getId() ?>" class="order-title individual"> + <dt id="add-gift-options-for-items-<?= (int) $block->getEntity()->getId()?>" class="order-title individual"> <div class="field choice"> - <input type="checkbox" name="allow_gift_options_for_items" id="allow_gift_options_for_items" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-for-items-container"}'<?php if ($block->getItemsHasMesssages()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options_for_items" class="label"><span><?= $block->escapeHtml(__('Gift Options for Individual Items')) ?></span></label> + <input type="checkbox" name="allow_gift_options_for_items" id="allow_gift_options_for_items" + data-mage-init='{"giftOptions":{}}' value="1" + data-selector='{"id":"#allow-gift-options-for-items-container"}' + <?php if ($block->getItemsHasMesssages()): ?> checked="checked"<?php endif; ?> + class="checkbox" /> + <label for="allow_gift_options_for_items" class="label"> + <span><?= $block->escapeHtml(__('Gift Options for Individual Items')) ?></span> + </label> </div> </dt> @@ -80,7 +125,11 @@ <li class="item"> <div class="product"> <div class="number"> - <?= $block->escapeHtml(__('<span>Item %1</span> of %2', $_index+1, $block->countItems()), ['span']) ?> + <?= $block->escapeHtml(__( + '<span>Item %1</span> of %2', + $_index+1, + $block->countItems() + ), ['span']) ?> </div> <div class="img photo container"> <?= $block->getImage($_product, 'gift_messages_checkout_thumbnail')->toHtml() ?> @@ -88,31 +137,64 @@ <strong class="product name"><?= $block->escapeHtml($_product->getName()) ?></strong> </div> <div class="options"> - <div class="options-items-container" id="options-items-container-<?= (int) $block->getEntity()->getId() ?>-<?= (int) $_item->getId() ?>"></div> + <div class="options-items-container" + id="options-items-container-<?= (int) $block->getEntity()->getId() + ?>-<?= (int) $_item->getId() ?>"></div> <?php if ($block->isItemMessagesAvailable($_item)): ?> <button class="action action-gift" - data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", "toggleContainers":"#gift-messages-for-item-container-<?= (int) $_item->getId() ?>"}}'> + data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", + "toggleContainers":"#gift-messages-for-item-container-<?= (int) $_item->getId() + ?>"}}'> <span><?= $block->escapeHtml(__('Gift Message')) ?></span> </button> - <div id="gift-messages-for-item-container-<?= (int) $_item->getId() ?>" class="block message hidden"> + <div id="gift-messages-for-item-container-<?= (int) $_item->getId() ?>" + class="block message hidden"> <fieldset class="fieldset"> - <p><?= $block->escapeHtml(__('Leave a box blank if you don\'t want to add a gift message for that item.')) ?></p> + <p><?= $block->escapeHtml(__( + 'Leave a box blank if you don\'t want to add a gift message for that item.' + )) ?></p> <div class="field from"> - <label for="gift-message-<?= (int) $_item->getId() ?>-from" class="label"><span><?= $block->escapeHtml(__('From')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId() ?>-from" class="label"> + <span><?= $block->escapeHtml(__('From')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_item][<?= (int) $_item->getId() ?>][from]" id="gift-message-<?= (int) $_item->getId() ?>-from" title="<?= $block->escapeHtmlAttr(__('From')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getSender(), $block->getDefaultFrom()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote_item][<?= (int) $_item->getId() ?>][from]" + id="gift-message-<?= (int) $_item->getId() ?>-from" + title="<?= $block->escapeHtmlAttr(__('From')) ?>" + value= + "<?= /* @noEscape */ + $block->getEscaped( + $block->getMessage($_item)->getSender(), + $block->getDefaultFrom() + ) ?>" class="input-text"> </div> </div> <div class="field to"> - <label for="gift-message-<?= (int) $_item->getId() ?>-to" class="label"><span><?= $block->escapeHtmlAttr(__('To')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId() ?>-to" class="label"> + <span><?= $block->escapeHtmlAttr(__('To')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_item][<?= (int) $_item->getId() ?>][to]" id="gift-message-<?= (int) $_item->getId() ?>-to" title="<?= $block->escapeHtmlAttr(__('To')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getRecipient(), $block->getDefaultTo()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote_item][<?= (int) $_item->getId() ?>][to]" + id="gift-message-<?= (int) $_item->getId() ?>-to" + title="<?= $block->escapeHtmlAttr(__('To')) ?>" + value="<?= /* @noEscape */ $block->getEscaped($block + ->getMessage($_item)->getRecipient(), $block->getDefaultTo()) ?>" + class="input-text"> </div> </div> <div class="field text"> - <label for="gift-message-<?= (int) $_item->getId() ?>-message" class="label"><span><?= $block->escapeHtml(__('Message')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId() ?>-message" class="label"> + <span><?= $block->escapeHtml(__('Message')) ?></span> + </label> <div class="control"> - <textarea id="gift-message-<?= (int) $_item->getId() ?>-message" class="input-text giftmessage-area" name="giftmessage[quote_item][<?= (int) $_item->getId() ?>][message]" title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="40"><?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getMessage()) ?></textarea> + <textarea id="gift-message-<?= (int) $_item->getId() ?>-message" + class="input-text giftmessage-area" + name="giftmessage[quote_item][<?= (int) $_item->getId() + ?>][message]" + title="<?= $block->escapeHtmlAttr(__('Message')) ?>" + rows="5" cols="40"><?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getMessage()) ?></textarea> </div> </div> </fieldset> @@ -123,15 +205,20 @@ <?php endforeach; ?> </ol> </dd> - <script> + <?php $entityId = (int) $block->getEntity()->getId(); + $scriptString = <<<script require(['jquery'], function(jQuery){ - jQuery('#add-gift-options-<?= (int) $block->getEntity()->getId() ?>') - .add('#add-gift-options-for-items-<?= (int) $block->getEntity()->getId() ?>') + jQuery('#add-gift-options-{$entityId}') + .add('#add-gift-options-for-items-{$entityId}') .removeClass('hidden'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> - <dt class="extra-options-container" id="extra-options-container-<?= (int) $block->getEntity()->getId() ?>"></dt> + <dt class="extra-options-container" + id="extra-options-container-<?= (int) $block->getEntity()->getId() ?>"> + </dt> </dl> </fieldset> <script type="text/x-magento-init"> @@ -141,52 +228,95 @@ } } </script> -<?php break; -case 'multishipping_address': ?> + <?php + break; + case 'multishipping_address': + ?> <fieldset id="add-gift-options-<?= (int) $block->getEntity()->getId() ?>" class="fieldset gift-message"> - <legend class="legend"><span><?= $block->escapeHtml(__('Do you have any gift items in your order?')) ?></span></legend><br> + <legend class="legend"> + <span><?= $block->escapeHtml(__('Do you have any gift items in your order?')) ?></span> + </legend><br> <div class="field choice" id="add-gift-options-<?= (int) $block->getEntity()->getId() ?>"> - <input type="checkbox" name="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" id="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-container-<?= (int) $block->getEntity()->getId() ?>"}'<?php if ($block->getItemsHasMesssages() || $block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" class="label"><span><?= $block->escapeHtml(__('Add Gift Options')) ?></span></label> + <input type="checkbox" name="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" + id="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" data-mage-init='{"giftOptions":{}}' + value="1" + data-selector='{"id":"#allow-gift-options-container-<?= (int) $block->getEntity()->getId() ?>"}' + <?php if ($block->getItemsHasMesssages() || $block->getEntityHasMessage()):?> checked="checked" + <?php endif; ?> class="checkbox" /> + <label for="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" class="label"> + <span><?= $block->escapeHtml(__('Add Gift Options')) ?></span> + </label> </div> <dl class="options-items" id="allow-gift-options-container-<?= (int) $block->getEntity()->getId() ?>"> <?php if ($block->isMessagesOrderAvailable() || $block->isMessagesAvailable()): ?> <dt id="add-gift-options-for-order-<?= (int) $block->getEntity()->getId() ?>" class="order-title"> <div class="field choice"> - <input type="checkbox" name="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" id="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-for-order-container-<?= (int) $block->getEntity()->getId() ?>"}'<?php if ($block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" class="label"><span><?= $block->escapeHtml(__('Add Gift Options for the Entire Order')) ?></span></label> + <input type="checkbox" name="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" + id="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" + data-mage-init='{"giftOptions":{}}' value="1" + data-selector='{"id":"#allow-gift-options-for-order-container-<?= (int) $block->getEntity() + ->getId() ?>"}' + <?php if ($block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox"/> + <label for="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" class="label"> + <span><?= $block->escapeHtml(__('Add Gift Options for the Entire Order')) ?></span> + </label> </div> </dt> - <dd id="allow-gift-options-for-order-container-<?= (int) $block->getEntity()->getId() ?>" class="order-options"> - <div class="options-order-container" id="options-order-container-<?= (int) $block->getEntity()->getId() ?>"></div> - <?php if ($block->isMessagesAvailable()): ?> - <?php $_giftMessage = true; ?> + <dd id="allow-gift-options-for-order-container-<?= (int) $block->getEntity()->getId() ?>" + class="order-options"> + <div class="options-order-container" + id="options-order-container-<?= (int) $block->getEntity()->getId() ?>"></div> + <?php if ($block->isMessagesAvailable()): ?> + <?php $_giftMessage = true; ?> <button class="action action-gift" - data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", "toggleContainers":"#gift-messages-for-order-container-<?= (int) $block->getEntity()->getId() ?>"}}'> + data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", + "toggleContainers":"#gift-messages-for-order-container-<?= (int) $block->getEntity() + ->getId() ?>"}}'> <span><?= $block->escapeHtml(__('Gift Message')) ?></span> </button> - <div id="gift-messages-for-order-container-<?= (int) $block->getEntity()->getId() ?>" class="gift-messages-order hidden"> + <div id="gift-messages-for-order-container-<?= (int) $block->getEntity()->getId() ?>" + class="gift-messages-order hidden"> <fieldset class="fieldset"> - <p><?= $block->escapeHtml(__('You can leave this box blank if you don\'t want to add a gift message for this address.')) ?></p> + <p><?= $block->escapeHtml(__('You can leave this box blank if you don\'t want to add a ' . + 'gift message for this address.')) ?></p> <div class="field from"> - <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-from" class="label"><span><?= $block->escapeHtml(__('From')) ?></span></label> + <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-from" + class="label"><span><?= $block->escapeHtml(__('From')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_address][<?= (int) $block->getEntity()->getId() ?>][from]" id="gift-message-<?= (int) $block->getEntity()->getId() ?>-from" title="<?= $block->escapeHtmlAttr(__('From')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage()->getSender(), $block->getDefaultFrom()) ?>" class="input-text"> + <input type="text" name="giftmessage[quote_address][<?= (int) $block->getEntity() + ->getId() ?>][from]" + id="gift-message-<?= (int) $block->getEntity()->getId() ?>-from" + title="<?= $block->escapeHtmlAttr(__('From')) ?>" + value="<?= /* @noEscape */ $block->getEscaped($block->getMessage() + ->getSender(), $block->getDefaultFrom()) ?>" class="input-text"> </div> </div> <div class="field to"> - <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-to" class="label"><span><?= $block->escapeHtml(__('To')) ?></span></label> + <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-to" + class="label"><span><?= $block->escapeHtml(__('To')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_address][<?= (int) $block->getEntity()->getId() ?>][to]" id="gift-message-<?= (int) $block->getEntity()->getId() ?>-to" title="<?= $block->escapeHtmlAttr(__('To')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage()->getRecipient(), $block->getDefaultTo()) ?>" class="input-text"> + <input type="text" name="giftmessage[quote_address][<?= (int) $block->getEntity() + ->getId() ?>][to]" + id="gift-message-<?= (int) $block->getEntity()->getId() ?>-to" + title="<?= $block->escapeHtmlAttr(__('To')) ?>" + value="<?= /* @noEscape */ $block->getEscaped($block->getMessage() + ->getRecipient(), $block->getDefaultTo()) ?>" class="input-text"> </div> </div> <div class="field text"> - <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-message" class="label"><span><?= $block->escapeHtml(__('Message')) ?></span></label> + <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-message" + class="label"><span><?= $block->escapeHtml(__('Message')) ?></span> + </label> <div class="control"> - <textarea id="gift-message-<?= (int) $block->getEntity()->getId() ?>-message" class="input-text" name="giftmessage[quote_address][<?= (int) $block->getEntity()->getId() ?>][message]" title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="40"><?= /* @noEscape */ $block->getEscaped($block->getMessage()->getMessage()) ?></textarea> + <textarea id="gift-message-<?= (int) $block->getEntity()->getId() ?>-message" + class="input-text" name="giftmessage[quote_address][<?= (int) $block + ->getEntity()->getId() ?>][message]" + title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="40"><?= /* @noEscape */ $block->getEscaped($block->getMessage()->getMessage()) ?></textarea> </div> </div> </fieldset> @@ -195,54 +325,103 @@ case 'multishipping_address': ?> </dd> <?php endif; ?> <?php if ($block->isItemsAvailable()): ?> - <dt id="add-gift-options-for-items-<?= (int) $block->getEntity()->getId() ?>" class="order-title individual"> + <dt id="add-gift-options-for-items-<?= (int) $block->getEntity()->getId()?>" class="order-title individual"> <div class="field choice"> - <input type="checkbox" name="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" id="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-for-items-container-<?= (int) $block->getEntity()->getId() ?>"}'<?php if ($block->getItemsHasMesssages()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" class="label"><span><?= $block->escapeHtml(__('Add Gift Options for Individual Items')) ?></span></label> + <input type="checkbox" name="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" + id="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" + data-mage-init='{"giftOptions":{}}' value="1" + data-selector='{"id":"#allow-gift-options-for-items-container-<?= (int) $block->getEntity() + ->getId() ?>"}' + <?php if ($block->getItemsHasMesssages()): ?> checked="checked"<?php endif; ?> + class="checkbox" /> + <label for="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" class="label"> + <span><?= $block->escapeHtml(__('Add Gift Options for Individual Items')) ?></span> + </label> </div> </dt> - <dd id="allow-gift-options-for-items-container-<?= (int) $block->getEntity()->getId() ?>" class="order-options individual"> + <dd id="allow-gift-options-for-items-container-<?= (int) $block->getEntity()->getId() ?>" + class="order-options individual"> <ol class="items"> <?php foreach ($block->getItems() as $_index => $_item): ?> <?php $_product = $_item->getProduct() ?> <li class="item"> <div class="product"> - <div class="number"><?= $block->escapeHtml(__('<span>Item %1</span> of %2', $_index+1, $block->countItems()), ['span']) ?></div> + <div class="number"> + <?= $block->escapeHtml( + __('<span>Item %1</span> of %2', $_index+1, $block->countItems()), + ['span'] + ) ?></div> <div class="img photo container"> <?= $block->getImage($_product, 'gift_messages_checkout_thumbnail')->toHtml() ?> </div> <strong class="product-name"><?= $block->escapeHtml($_product->getName()) ?></strong> </div> <div class="options"> - <div class="options-items-container" id="options-items-container-<?= (int) $block->getEntity()->getId() ?>-<?= (int) $_item->getId() ?>"></div> - <input type="hidden" name="giftoptions[quote_address_item][<?= (int) $_item->getId() ?>][address]" value="<?= (int) $block->getEntity()->getId() ?>" /> + <div class="options-items-container" + id="options-items-container-<?= (int) $block->getEntity()->getId()?>-<?= (int)$_item + ->getId() ?>"> + </div> + <input type="hidden" + name="giftoptions[quote_address_item][<?= (int) $_item->getId() ?>][address]" + value="<?= (int) $block->getEntity()->getId() ?>" /> <?php if ($block->isItemMessagesAvailable($_item)): ?> <?php $_giftMessage = true; ?> <button class="action action-gift" - data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", "toggleContainers":"#gift-messages-for-item-container-<?= (int) $_item->getId() ?>"}}'> + data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", + "toggleContainers":"#gift-messages-for-item-container-<?= (int) $_item->getId() + ?>"}}'> <span><?= $block->escapeHtml(__('Gift Message')) ?></span> </button> - <div id="gift-messages-for-item-container-<?= (int) $_item->getId() ?>" class="block message hidden"> + <div id="gift-messages-for-item-container-<?= (int) $_item->getId() ?>" + class="block message hidden"> <fieldset class="fieldset"> - <p><?= $block->escapeHtml(__('You can leave this box blank if you don\'t want to add a gift message for the item.')) ?></p> - <input type="hidden" name="giftmessage[quote_address_item][<?= (int) $_item->getId() ?>][address]" value="<?= (int) $block->getEntity()->getId() ?>" /> + <p><?= $block->escapeHtml(__( + 'You can leave this box blank if you don\'t want to add a gift message ' . + 'for the item.' + )) ?></p> + <input type="hidden" name="giftmessage[quote_address_item][<?= (int) $_item + ->getId() ?>][address]" value="<?= (int) $block->getEntity()->getId() ?>" /> <div class="field from"> - <label for="gift-message-<?= (int) $_item->getId() ?>-from" class="label"><span><?= $block->escapeHtml(__('From')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId() ?>-from" class="label"> + <span><?= $block->escapeHtml(__('From')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_address_item][<?= (int) $_item->getId() ?>][from]" id="gift-message-<?= (int) $_item->getId() ?>-from" title="<?= $block->escapeHtmlAttr(__('From')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getSender(), $block->getDefaultFrom()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote_address_item][<?= (int) $_item->getId() + ?>][from]" id="gift-message-<?= (int) $_item->getId() ?>-from" + title="<?= $block->escapeHtmlAttr(__('From')) ?>" + value="<?= /* @noEscape */ $block->getEscaped($block + ->getMessage($_item)->getSender(), $block->getDefaultFrom()) + ?>" class="input-text"> </div> </div> <div class="field to"> - <label for="gift-message-<?= (int) $_item->getId() ?>-to" class="label"><span><?= $block->escapeHtml(__('To')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId() ?>-to" class="label"> + <span><?= $block->escapeHtml(__('To')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_address_item][<?= (int) $_item->getId() ?>][to]" id="gift-message-<?= (int) $_item->getId() ?>-to" title="<?= $block->escapeHtmlAttr(__('To')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getRecipient(), $block->getDefaultTo()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote_address_item][<?= (int) $_item->getId() + ?>][to]" id="gift-message-<?= (int) $_item->getId() ?>-to" + title="<?= $block->escapeHtmlAttr(__('To')) ?>" + value= + "<?= /* @noEscape */ $block->getEscaped($block + ->getMessage($_item)->getRecipient(), $block->getDefaultTo()) + ?>" class="input-text"> </div> </div> <div class="field text"> - <label for="gift-message-<?= (int) $_item->getId() ?>-message" class="label"><span><?= $block->escapeHtml(__('Message')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId()?>-message" class="label"> + <span><?= $block->escapeHtml(__('Message')) ?></span> + </label> <div class="control"> - <textarea id="gift-message-<?= (int) $_item->getId() ?>-message" class="input-text giftmessage-area" name="giftmessage[quote_address_item][<?= (int) $_item->getId() ?>][message]" title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="10"><?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getMessage()) ?></textarea> + <textarea id="gift-message-<?= (int) $_item->getId() ?>-message" + class="input-text giftmessage-area" + name="giftmessage[quote_address_item][<?= (int) $_item + ->getId() ?>][message]" + title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" + cols="10"><?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getMessage()) ?></textarea> </div> </div> </fieldset> @@ -254,19 +433,22 @@ case 'multishipping_address': ?> </ol> </dd> <?php endif; ?> - <dt class="extra-options-container" id="extra-options-container-<?= (int) $block->getEntity()->getId() ?>"></dt> + <dt class="extra-options-container" id="extra-options-container-<?= (int) $block->getEntity()->getId() ?>"> + </dt> </dl> </fieldset> + <?php $entityId = (int) $block->getEntity()->getId(); ?> <script type="text/x-magento-init"> { - "#allow_gift_options_<?= (int) $block->getEntity()->getId() ?>, #allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>, #allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>": { + "#allow_gift_options_<?= /* @noEscape */ $entityId ?>, #allow_gift_options_for_order_<?= /* @noEscape */ $entityId ?>, #allow_gift_options_for_items_<?= /* @noEscape */ $entityId ?>": { "giftOptions": {} } } </script> - <?php break; ?> -<?php endswitch ?> -<?php if ($_giftMessage): ?> + <?php + break; + endswitch; +if ($_giftMessage): ?> <script type="text/x-magento-init"> { "#shipping_method_form": { diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php new file mode 100644 index 0000000000000..2ce51c8bbf19d --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GiftMessageGraphQl\Model\Resolver\Cart; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GiftMessage\Api\CartRepositoryInterface; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; + +/** + * Class provides ability to get GiftMessage for cart + */ +class GiftMessage implements ResolverInterface +{ + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var GiftMessageHelper + */ + private $giftMessageHelper; + + /** + * @param CartRepositoryInterface $cartRepository + * @param GiftMessageHelper $giftMessageHelper + */ + public function __construct( + CartRepositoryInterface $cartRepository, + GiftMessageHelper $giftMessageHelper + ) { + $this->cartRepository = $cartRepository; + $this->giftMessageHelper = $giftMessageHelper; + } + + /** + * Return information about Gift message of cart + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('"model" value must be specified')); + } + + $cart = $value['model']; + + if (!$this->giftMessageHelper->isMessagesAllowed('order', $cart)) { + return null; + } + + try { + $giftCartMessage = $this->cartRepository->get($cart->getId()); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__('Can\'t load cart.')); + } + + if (!isset($giftCartMessage)) { + return null; + } + + return [ + 'to' => $giftCartMessage->getRecipient() ?? '', + 'from' => $giftCartMessage->getSender() ?? '', + 'message'=> $giftCartMessage->getMessage() ?? '' + ]; + } +} diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php new file mode 100644 index 0000000000000..a9a8e682612cc --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GiftMessageGraphQl\Model\Resolver\Cart\Item; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GiftMessage\Api\ItemRepositoryInterface; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; + +/** + * Class provides ability to get GiftMessage for cart item + */ +class GiftMessage implements ResolverInterface +{ + /** + * @var ItemRepositoryInterface + */ + private $itemRepository; + + /** + * @var GiftMessageHelper + */ + private $giftMessageHelper; + + /** + * @param ItemRepositoryInterface $itemRepository + * @param GiftMessageHelper $giftMessageHelper + */ + public function __construct( + ItemRepositoryInterface $itemRepository, + GiftMessageHelper $giftMessageHelper + ) { + $this->itemRepository = $itemRepository; + $this->giftMessageHelper = $giftMessageHelper; + } + + /** + * Return information about Gift message for item cart + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('"model" value must be specified')); + } + + $quoteItem = $value['model']; + + if (!$this->giftMessageHelper->isMessagesAllowed('items', $quoteItem)) { + return null; + } + + if (!$this->giftMessageHelper->isMessagesAllowed('item', $quoteItem)) { + return null; + } + + try { + $giftItemMessage = $this->itemRepository->get($quoteItem->getQuoteId(), $quoteItem->getItemId()); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__('Can\'t load cart item')); + } + + if (!isset($giftItemMessage)) { + return null; + } + + return [ + 'to' => $giftItemMessage->getRecipient() ?? '', + 'from' => $giftItemMessage->getSender() ?? '', + 'message'=> $giftItemMessage->getMessage() ?? '' + ]; + } +} diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php new file mode 100644 index 0000000000000..aae0e3709d87f --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GiftMessageGraphQl\Model\Resolver\Order; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GiftMessage\Api\OrderRepositoryInterface; + +/** + * Class for getting GiftMessage from CustomerOrder + */ +class GiftMessage implements ResolverInterface +{ + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @param OrderRepositoryInterface $orderRepository + */ + public function __construct( + OrderRepositoryInterface $orderRepository + ) { + $this->orderRepository = $orderRepository; + } + + /** + * Return information about gift message for order + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['id'])) { + throw new GraphQlInputException(__('"id" value should be specified')); + } + + try { + $orderGiftMessage = $this->orderRepository->get($value['id']); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__('Can\'t load gift message for order')); + } + + if (!isset($orderGiftMessage)) { + return null; + } + + return [ + 'to' => $orderGiftMessage->getRecipient() ?? '', + 'from' => $orderGiftMessage->getSender() ?? '', + 'message'=> $orderGiftMessage->getMessage() ?? '' + ]; + } +} diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md new file mode 100644 index 0000000000000..d73a058e0db9c --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/README.md @@ -0,0 +1,3 @@ +# GiftMessageGraphQl + +**GiftMessageGraphQl** provides information about gift messages for carts, cart items, orders and order items. diff --git a/app/code/Magento/GiftMessageGraphQl/composer.json b/app/code/Magento/GiftMessageGraphQl/composer.json new file mode 100644 index 0000000000000..48088f2a48a32 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-gift-message-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-gift-message": "*" + }, + "suggest": { + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\GiftMessageGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..bce5b7063e6b9 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="allow_order" xsi:type="string">sales/gift_options/allow_order</item> + <item name="allow_items" xsi:type="string">sales/gift_options/allow_items</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/GiftMessageGraphQl/etc/module.xml b/app/code/Magento/GiftMessageGraphQl/etc/module.xml new file mode 100644 index 0000000000000..5eaaae0b0b988 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/etc/module.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_GiftMessageGraphQl"> + <sequence> + <module name="Magento_GiftMessage"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..ad18054abca13 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls @@ -0,0 +1,47 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type StoreConfig { + allow_order : String @doc(description: "The value of the Allow Gift Messages on Order Level option") + allow_items : String @doc(description: "The value of the Allow Gift Messages for Order Items option") +} + +type Cart { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\GiftMessage") @doc(description: "The entered gift message for the cart") +} + +type SimpleCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") +} + +type ConfigurableCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") +} + +type BundleCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") +} + +type GiftMessage @doc(description: "Contains the text of a gift message, its sender, and recipient") { + to: String! @doc(description: "Recipient name") + from: String! @doc(description: "Sender name") + message: String! @doc(description: "Gift message text") +} + +input CartItemUpdateInput { + gift_message: GiftMessageInput @doc(description: "Gift message details for the cart item") +} + +input GiftMessageInput @doc(description: "Contains the text of a gift message, its sender, and recipient") { + to: String! @doc(description: "Recipient name") + from: String! @doc(description: "Sender name") + message: String! @doc(description: "Gift message text") +} + +type SalesItemInterface { + gift_message: GiftMessage @doc(description: "The entered gift message for the order item") +} + +type CustomerOrder { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order") +} diff --git a/app/code/Magento/GiftMessageGraphQl/registration.php b/app/code/Magento/GiftMessageGraphQl/registration.php new file mode 100644 index 0000000000000..bb260c23b0177 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_GiftMessageGraphQl', + __DIR__ +); diff --git a/app/code/Magento/GoogleAdwords/Helper/Data.php b/app/code/Magento/GoogleAdwords/Helper/Data.php index 0e95859193d42..e3b85822059d8 100644 --- a/app/code/Magento/GoogleAdwords/Helper/Data.php +++ b/app/code/Magento/GoogleAdwords/Helper/Data.php @@ -280,6 +280,7 @@ public function getConversionValue() * Get send order currency to Google Adwords * * @return boolean + * @since 100.3.0 */ public function hasSendConversionValueCurrency() { @@ -293,6 +294,7 @@ public function hasSendConversionValueCurrency() * Get Google AdWords conversion value currency * * @return string|false + * @since 100.3.0 */ public function getConversionValueCurrency() { diff --git a/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml b/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..d700b0e9e7668 --- /dev/null +++ b/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml @@ -0,0 +1,24 @@ +<?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="script-src"> + <values> + <value id="google_ad_services" type="host">www.googleadservices.com</value> + <value id="google_analytics" type="host">www.google-analytics.com</value> + </values> + </policy> + <policy id="img-src"> + <values> + <value id="google_ad_services" type="host">www.googleadservices.com</value> + <value id="google_analytics" type="host">www.google-analytics.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/GoogleAdwords/view/frontend/templates/code.phtml b/app/code/Magento/GoogleAdwords/view/frontend/templates/code.phtml index e3c46bc27834c..0de78ca8b62c2 100644 --- a/app/code/Magento/GoogleAdwords/view/frontend/templates/code.phtml +++ b/app/code/Magento/GoogleAdwords/view/frontend/templates/code.phtml @@ -5,27 +5,39 @@ */ ?> <?php -/** @var $block \Magento\GoogleAdwords\Block\Code */ +/** + * @var $block \Magento\GoogleAdwords\Block\Code + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <!-- Google Code for Sale Conversion Page --> -<script> +<?php +/** @var \Magento\GoogleAdwords\Helper\Data $helper */ +$helper = $block->getHelper(); +$scriptString = <<<script /* <![CDATA[ */ - var google_conversion_id = <?= $block->escapeJs($block->getHelper()->getConversionId()) ?>; - var google_conversion_language = "<?= $block->escapeJs($block->getHelper()->getConversionLanguage()) ?>"; - var google_conversion_format = "<?= $block->escapeJs($block->getHelper()->getConversionFormat()) ?>"; - var google_conversion_color = "<?= $block->escapeJs($block->getHelper()->getConversionColor()) ?>"; - var google_conversion_label = "<?= $block->escapeJs($block->getHelper()->getConversionLabel()) ?>"; - var google_conversion_value = <?= $block->escapeJs($block->getHelper()->getConversionValue()) ?>; - <?php if ($block->getHelper()->hasSendConversionValueCurrency() && $block->getHelper()->getConversionValueCurrency()) : ?> - var google_conversion_currency = "<?= $block->escapeJs($block->getHelper()->getConversionValueCurrency()) ?>"; - <?php endif; ?> + var google_conversion_id = {$block->escapeJs($helper->getConversionId())}; + var google_conversion_language = "{$block->escapeJs($helper->getConversionLanguage())}"; + var google_conversion_format = "{$block->escapeJs($helper->getConversionFormat())}"; + var google_conversion_color = "{$block->escapeJs($helper->getConversionColor())}"; + var google_conversion_label = "{$block->escapeJs($helper->getConversionLabel())}"; + var google_conversion_value = {$block->escapeJs($helper->getConversionValue())}; +script; +if ($helper->hasSendConversionValueCurrency() && $helper->getConversionValueCurrency()): + $scriptString .= <<<script + var google_conversion_currency = "{$block->escapeJs($helper->getConversionValueCurrency())}"; +script; +endif; +$scriptString .= <<<script /* ]]> */ -</script> -<script src="<?= $block->escapeHtmlAttr($block->getHelper()->getConversionJsSrc()) ?>"></script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<script src="<?= $block->escapeHtmlAttr($helper->getConversionJsSrc()) ?>"></script> <noscript> <div style="display:inline;"> <img height="1" width="1" style="border-style:none;" alt="" - src="<?= $block->escapeHtmlAttr($block->getHelper()->getConversionImgSrc()) ?>"/> + src="<?= $block->escapeHtmlAttr($helper->getConversionImgSrc()) ?>"/> </div> </noscript> <!-- END Google Code for Sale Conversion Page --> diff --git a/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php b/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php index 135c8c92c6aa9..975788abe52e4 100644 --- a/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php +++ b/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php @@ -5,12 +5,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\GoogleOptimizer\Observer; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; /** + * Abstract entity for saving codes + * * @api * @since 100.0.2 */ @@ -96,7 +100,9 @@ protected function _processCode() $this->_initRequestParams(); if ($this->_isNewCode()) { - $this->_saveCode(); + if (!$this->_isEmptyCode()) { + $this->_saveCode(); + } } else { $this->_loadCode(); if ($this->_isEmptyCode()) { @@ -185,6 +191,8 @@ protected function _deleteCode() } /** + * Check data availability + * * @return bool */ private function isDataAvailable() @@ -194,6 +202,8 @@ private function isDataAvailable() } /** + * Get request data + * * @return mixed */ private function getRequestData() diff --git a/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php b/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php index 8a5c247369657..c6d02957c4be9 100644 --- a/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php +++ b/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php @@ -127,6 +127,39 @@ public function testCreatingCodeIfRequestIsValid() $this->_modelObserver->execute($this->_eventObserverMock); } + /** + * Test that code is not saving when request is empty + * + * @return void + */ + public function testCreatingCodeIfRequestIsEmpty(): void + { + $this->_helperMock->expects( + $this->once() + )->method( + 'isGoogleExperimentActive' + )->with( + $this->_storeId + )->willReturn( + true + ); + + $this->_requestMock->expects( + $this->exactly(3) + )->method( + 'getParam' + )->with( + 'google_experiment' + )->willReturn( + ['code_id' => '', 'experiment_script' => ''] + ); + + $this->_codeMock->expects($this->never())->method('addData'); + $this->_codeMock->expects($this->never())->method('save'); + + $this->_modelObserver->execute($this->_eventObserverMock); + } + /** * @param array $params * @dataProvider dataProviderWrongRequestForCreating diff --git a/app/code/Magento/GraphQl/Controller/GraphQl.php b/app/code/Magento/GraphQl/Controller/GraphQl.php index 2d72fde91b031..34dbeaa2ed0a2 100644 --- a/app/code/Magento/GraphQl/Controller/GraphQl.php +++ b/app/code/Magento/GraphQl/Controller/GraphQl.php @@ -28,12 +28,13 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.3.0 */ class GraphQl implements FrontControllerInterface { /** * @var \Magento\Framework\Webapi\Response - * @deprecated + * @deprecated 100.3.2 */ private $response; @@ -59,7 +60,7 @@ class GraphQl implements FrontControllerInterface /** * @var ContextInterface - * @deprecated $contextFactory is used for creating Context object + * @deprecated 100.3.3 $contextFactory is used for creating Context object */ private $resolverContext; @@ -133,6 +134,7 @@ public function __construct( * * @param RequestInterface $request * @return ResponseInterface + * @since 100.3.0 */ public function dispatch(RequestInterface $request) : ResponseInterface { diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php new file mode 100644 index 0000000000000..ba2e995d4f704 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Credentials header if CORS is enabled + */ +class CorsAllowCredentialsHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Get value for header + * + * @return string + */ + public function getValue(): string + { + return "1"; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->corsConfiguration->isCredentialsAllowed(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php new file mode 100644 index 0000000000000..68760de543daa --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Headers header if CORS is enabled + */ +class CorsAllowHeadersHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedHeaders(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php new file mode 100644 index 0000000000000..233839b9deb74 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Methods header if CORS is enabled + */ +class CorsAllowMethodsHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedMethods(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php new file mode 100644 index 0000000000000..21850f18db1f2 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Origin header if CORS is enabled + */ +class CorsAllowOriginHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedOrigins(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php new file mode 100644 index 0000000000000..e30209ae25e68 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Max-Age header if CORS is enabled + */ +class CorsMaxAgeHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return (string) $this->corsConfiguration->getMaxAge(); + } +} diff --git a/app/code/Magento/GraphQl/Model/Cors/Configuration.php b/app/code/Magento/GraphQl/Model/Cors/Configuration.php new file mode 100644 index 0000000000000..dd5a0b426e22d --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Cors/Configuration.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Cors; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Configuration provider for GraphQL CORS settings + */ +class Configuration implements ConfigurationInterface +{ + public const XML_PATH_CORS_HEADERS_ENABLED = 'graphql/cors/enabled'; + public const XML_PATH_CORS_ALLOWED_ORIGINS = 'graphql/cors/allowed_origins'; + public const XML_PATH_CORS_ALLOWED_HEADERS = 'graphql/cors/allowed_headers'; + public const XML_PATH_CORS_ALLOWED_METHODS = 'graphql/cors/allowed_methods'; + public const XML_PATH_CORS_MAX_AGE = 'graphql/cors/max_age'; + public const XML_PATH_CORS_ALLOW_CREDENTIALS = 'graphql/cors/allow_credentials'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Are CORS headers enabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_HEADERS_ENABLED); + } + + /** + * Get allowed origins or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedOrigins(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_ORIGINS); + } + + /** + * Get allowed headers or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedHeaders(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_HEADERS); + } + + /** + * Get allowed methods or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedMethods(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_METHODS); + } + + /** + * Get max age header value + * + * @return int + */ + public function getMaxAge(): int + { + return (int) $this->scopeConfig->getValue(self::XML_PATH_CORS_MAX_AGE); + } + + /** + * Are credentials allowed + * + * @return bool + */ + public function isCredentialsAllowed(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_ALLOW_CREDENTIALS); + } +} diff --git a/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php new file mode 100644 index 0000000000000..b40b64f48e51f --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Cors; + +/** + * Interface for configuration provider for GraphQL CORS settings + */ +interface ConfigurationInterface +{ + /** + * Are CORS headers enabled + * + * @return bool + */ + public function isEnabled(): bool; + + /** + * Get allowed origins or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedOrigins(): ?string; + + /** + * Get allowed headers or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedHeaders(): ?string; + + /** + * Get allowed methods or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedMethods(): ?string; + + /** + * Get max age header value + * + * @return int + */ + public function getMaxAge(): int; + + /** + * Are credentials allowed + * + * @return bool + */ + public function isCredentialsAllowed() : bool; +} diff --git a/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php b/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php index 2b8e3fabd6863..9403ccaf07099 100644 --- a/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php +++ b/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php @@ -12,7 +12,7 @@ /** * Do not use this class. It was kept for backward compatibility. * - * @deprecated \Magento\GraphQl\Model\Query\Context is used instead of this + * @deprecated 100.3.3 \Magento\GraphQl\Model\Query\Context is used instead of this */ class Context extends \Magento\Framework\Model\AbstractExtensibleModel implements ContextInterface { diff --git a/app/code/Magento/GraphQl/composer.json b/app/code/Magento/GraphQl/composer.json index 904d41c97953e..401e77a787acf 100644 --- a/app/code/Magento/GraphQl/composer.json +++ b/app/code/Magento/GraphQl/composer.json @@ -5,10 +5,10 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/module-eav": "*", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-webapi": "*" }, "suggest": { - "magento/module-webapi": "*", "magento/module-graph-ql-cache": "*" }, "license": [ diff --git a/app/code/Magento/GraphQl/etc/adminhtml/system.xml b/app/code/Magento/GraphQl/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..ddee7596eca3e --- /dev/null +++ b/app/code/Magento/GraphQl/etc/adminhtml/system.xml @@ -0,0 +1,65 @@ +<?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="graphql" translate="label" type="text" sortOrder="300" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>GraphQL</label> + <tab>service</tab> + <resource>Magento_Integration::config_oauth</resource> + <group id="cors" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="1"> + <label>CORS Settings</label> + <field id="enabled" translate="label" type="select" sortOrder="1" showInDefault="1" canRestore="1"> + <label>CORS Headers Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + + <field id="allowed_origins" translate="label" type="text" sortOrder="10" showInDefault="1" canRestore="1"> + <label>Allowed origins</label> + <comment>The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin. Fill this field with one or more origins (comma separated) or use '*' to allow access from all origins.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allowed_methods" translate="label" type="text" sortOrder="20" showInDefault="1" canRestore="1"> + <label>Allowed methods</label> + <comment>The Access-Control-Allow-Methods response header specifies the method or methods allowed when accessing the resource in response to a preflight request. Use comma separated methods (e.g. GET,POST)</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allowed_headers" translate="label" type="text" sortOrder="30" showInDefault="1" canRestore="1"> + <label>Allowed headers</label> + <comment>The Access-Control-Allow-Headers response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request. Use comma separated headers.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="max_age" translate="label" type="text" sortOrder="40" showInDefault="1" canRestore="1"> + <label>Max Age</label> + <validate>validate-digits</validate> + <comment>The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allow_credentials" translate="label" type="select" sortOrder="50" showInDefault="1" canRestore="1"> + <label>Credentials Allowed</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to frontend code when the request's credentials mode is include.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/GraphQl/etc/config.xml b/app/code/Magento/GraphQl/etc/config.xml new file mode 100644 index 0000000000000..39caacbec42d2 --- /dev/null +++ b/app/code/Magento/GraphQl/etc/config.xml @@ -0,0 +1,22 @@ +<?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> + <graphql> + <cors> + <enabled>0</enabled> + <allowed_origins></allowed_origins> + <allowed_methods></allowed_methods> + <allowed_headers></allowed_headers> + <max_age>86400</max_age> + <allow_credentials>0</allow_credentials> + </cors> + </graphql> + </default> +</config> diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index b356f33c4f4bf..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> @@ -98,4 +102,31 @@ <argument name="queryComplexity" xsi:type="number">300</argument> </arguments> </type> + + <preference for="Magento\GraphQl\Model\Cors\ConfigurationInterface" type="Magento\GraphQl\Model\Cors\Configuration" /> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Max-Age</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Credentials</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Headers</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Methods</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Origin</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GraphQl/etc/graphql/di.xml b/app/code/Magento/GraphQl/etc/graphql/di.xml index 2bcd44e9ae410..23d49124d1a02 100644 --- a/app/code/Magento/GraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GraphQl/etc/graphql/di.xml @@ -15,10 +15,6 @@ <item name="type" xsi:type="object">Magento\Webapi\Model\Authorization\TokenUserContext</item> <item name="sortOrder" xsi:type="string">10</item> </item> - <item name="oauthUserContext" xsi:type="array"> - <item name="type" xsi:type="object">Magento\Webapi\Model\Authorization\OauthUserContext</item> - <item name="sortOrder" xsi:type="string">40</item> - </item> <item name="guestUserContext" xsi:type="array"> <item name="type" xsi:type="object">Magento\Webapi\Model\Authorization\GuestUserContext</item> <item name="sortOrder" xsi:type="string">100</item> @@ -34,4 +30,15 @@ </argument> </arguments> </type> + <type name="Magento\Framework\App\Response\HeaderManager"> + <arguments> + <argument name="headerProviderList" xsi:type="array"> + <item name="CorsAllowOrigins" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider</item> + <item name="CorsAllowHeaders" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider</item> + <item name="CorsAllowMethods" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider</item> + <item name="CorsAllowCredentials" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider</item> + <item name="CorsMaxAge" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index fccde015c3388..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 @@ -79,6 +79,12 @@ input FilterMatchTypeInput @doc(description: "Defines a filter that performs a f match: String @doc(description: "One or more words to filter on") } +input FilterStringTypeInput @doc(description: "Defines a filter for an input string.") { + in: [String] @doc(description: "Filters items that are exactly the same as entries specified in an array of strings.") + eq: String @doc(description: "Filters items that are exactly the same as the specified string.") + match: String @doc(description: "Defines a filter that performs a fuzzy search using the specified string.") +} + type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") { page_size: Int @doc(description: "Specifies the maximum number of items to return") current_page: Int @doc(description: "Specifies which page of results to return") @@ -271,3 +277,8 @@ enum CurrencyEnum @doc(description: "The list of available currency codes") { TRL XPF } + +input EnteredOptionInput @doc(description: "Defines a customer-entered option") { + uid: ID! @doc(description: "An encoded ID") + value: String! @doc(description: "Text the customer entered") +} diff --git a/app/code/Magento/GroupedProduct/Block/Order/Email/Items/CreditMemo/Grouped.php b/app/code/Magento/GroupedProduct/Block/Order/Email/Items/CreditMemo/Grouped.php index 6df890b3e94dc..2b4ee3a7d27da 100644 --- a/app/code/Magento/GroupedProduct/Block/Order/Email/Items/CreditMemo/Grouped.php +++ b/app/code/Magento/GroupedProduct/Block/Order/Email/Items/CreditMemo/Grouped.php @@ -13,6 +13,7 @@ * Class renders grouped product(s) in the CreditMemo email * * @api + * @since 100.4.0 */ class Grouped extends DefaultItems { @@ -22,6 +23,7 @@ class Grouped extends DefaultItems * This method uses renderer for real product type * * @return string + * @since 100.4.0 */ protected function _toHtml() { diff --git a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php index e1599dc772c2c..c5f0316feb126 100644 --- a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php @@ -20,6 +20,7 @@ /** * Calculate minimal and maximal prices for Grouped products + * * Use calculated price for relation products * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -85,10 +86,7 @@ public function __construct( } /** - * {@inheritdoc} - * @param array $dimensions - * @param \Traversable $entityIds - * @throws \Exception + * @inheritDoc */ public function executeByDimensions(array $dimensions, \Traversable $entityIds) { @@ -105,9 +103,8 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) 'maxPriceField' => 'max_price', 'tierPriceField' => 'tier_price', ]); - $query = $this->prepareGroupedProductPriceDataSelect($dimensions, iterator_to_array($entityIds)) - ->insertFromSelect($temporaryPriceTable->getTableName()); - $this->getConnection()->query($query); + $select = $this->prepareGroupedProductPriceDataSelect($dimensions, iterator_to_array($entityIds)); + $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []); } /** @@ -186,13 +183,13 @@ private function getMainTable($dimensions) if ($this->fullReindexAction) { return $this->tableMaintainer->getMainReplicaTable($dimensions); } - return $this->tableMaintainer->getMainTable($dimensions); + return $this->tableMaintainer->getMainTableByDimensions($dimensions); } /** * Get connection * - * return \Magento\Framework\DB\Adapter\AdapterInterface + * @return \Magento\Framework\DB\Adapter\AdapterInterface * @throws \DomainException */ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface diff --git a/app/code/Magento/GroupedProduct/Model/Wishlist/Product/Item.php b/app/code/Magento/GroupedProduct/Model/Wishlist/Product/Item.php index d84df510195f3..48e1a5053ac69 100644 --- a/app/code/Magento/GroupedProduct/Model/Wishlist/Product/Item.php +++ b/app/code/Magento/GroupedProduct/Model/Wishlist/Product/Item.php @@ -7,6 +7,7 @@ namespace Magento\GroupedProduct\Model\Wishlist\Product; +use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; use Magento\Wishlist\Model\Item as WishlistItem; use Magento\GroupedProduct\Model\Product\Type\Grouped as TypeGrouped; use Magento\Catalog\Model\Product; @@ -36,7 +37,7 @@ public function beforeRepresentProduct( $diff = array_diff_key($itemOptions, $productOptions); - if (!$diff) { + if (!$diff && $this->isAddAction($productOptions['info_buyRequest'])) { $buyRequest = $subject->getBuyRequest(); $superGroupInfo = $buyRequest->getData('super_group'); @@ -78,10 +79,13 @@ public function beforeCompareOptions( array $options2 ): array { $diff = array_diff_key($options1, $options2); + $productOptions = isset($options1['info_buyRequest']['product']) ? $options1 : $options2; if (!$diff) { foreach (array_keys($options1) as $key) { - if (preg_match('/associated_product_\d+/', $key)) { + if (preg_match('/associated_product_\d+/', $key) + && $this->isAddAction($productOptions['info_buyRequest']) + ) { unset($options1[$key]); unset($options2[$key]); } @@ -90,4 +94,18 @@ public function beforeCompareOptions( return [$options1, $options2]; } + + /** + * Check that current request belongs to add to wishlist action. + * + * @param OptionInterface $buyRequest + * + * @return bool + */ + private function isAddAction(OptionInterface $buyRequest): bool + { + $requestValue = json_decode($buyRequest->getValue(), true); + + return $requestValue['action'] === 'add'; + } } diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml index 04b704b9193ca..23b1499c2d4e8 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml @@ -32,15 +32,14 @@ <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteGroupedProduct"> <argument name="sku" value="{{GroupedProduct.sku}}"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> <!-- Create product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="GroupedProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml index bd6785eb5e41b..a909582c32b3d 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml @@ -53,7 +53,9 @@ </actionGroup> <!-- Reindex --> - <magentoCLI command="indexer:reindex" stepKey="reindexAllIndexes"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAllIndexes"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml index b88f909d977ab..a00a341c4e6af 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml @@ -42,10 +42,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createGroupedProduct.name$$)}}" stepKey="amOnGroupedProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!--Search for the product by sku--> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createGroupedProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createGroupedProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createGroupedProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml index 6514b5ddc5f78..ef1665d965200 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml @@ -37,8 +37,7 @@ </after> <!-- Create product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="adminProductIndexPageAdd"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="adminProductIndexPageAdd"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProductPage"> <argument name="product" value="GroupedProduct"/> </actionGroup> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml index 053949fa20fb2..0dc622a82aaae 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml @@ -29,15 +29,14 @@ </createData> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> <!-- Create product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="GroupedProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml index 7f03765720069..fbe77270a6177 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml @@ -127,8 +127,7 @@ </after> <!--Create grouped Product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="GroupedProduct"/> @@ -156,8 +155,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Open created Product group--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfExist"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGridForm"> <argument name="keyword" value="GroupedProduct.name"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByDescriptionTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByDescriptionTest.xml index 599736e7e817e..4aa4e99e79489 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByDescriptionTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByDescriptionTest.xml @@ -30,8 +30,12 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple2"/> </updateData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByNameTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByNameTest.xml index 853304c557c8f..06dde74de20f9 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByNameTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByNameTest.xml @@ -30,8 +30,12 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple2"/> </updateData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByPriceTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByPriceTest.xml index 4e67f2bd50439..66e1b60331e97 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByPriceTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByPriceTest.xml @@ -39,8 +39,12 @@ <getData entity="GetProduct" stepKey="arg3"> <requiredEntity createDataKey="simple2"/> </getData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByShortDescriptionTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByShortDescriptionTest.xml index 4b86a2c085003..79b465abe2ac6 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByShortDescriptionTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByShortDescriptionTest.xml @@ -30,8 +30,12 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple2"/> </updateData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml index 6e67e41fa447b..c196abbce99ed 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml @@ -29,8 +29,12 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple2"/> </updateData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/NewProductsListWidgetGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/NewProductsListWidgetGroupedProductTest.xml index d563796b21da9..b783525e9fe18 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/NewProductsListWidgetGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/NewProductsListWidgetGroupedProductTest.xml @@ -40,8 +40,7 @@ <!-- Create a grouped product to appear in the widget --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="toggleAddProductButton"/> <click selector="{{AdminProductGridActionSection.addGroupedProduct}}" stepKey="clickAddGroupedProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> @@ -58,7 +57,7 @@ <checkOption selector="{{AdminAddProductsToGroupPanel.nThCheckbox('0')}}" stepKey="checkFilterResult1"/> <checkOption selector="{{AdminAddProductsToGroupPanel.nThCheckbox('1')}}" stepKey="checkFilterResult2"/> <click selector="{{AdminAddProductsToGroupPanel.addSelectedProducts}}" stepKey="clickAddSelectedGroupProducts"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here, via merge --> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml index aaa9cf5b2f925..c141c179330f5 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml @@ -21,7 +21,6 @@ <skip> <issueId value="MC-34217"/> </skip> - </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -35,8 +34,12 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple2"/> </updateData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/Wishlist/Product/ItemTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/Wishlist/Product/ItemTest.php index 7dc25c3de1245..6006ed639c92e 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Model/Wishlist/Product/ItemTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/Wishlist/Product/ItemTest.php @@ -115,13 +115,29 @@ public function testBeforeRepresentProduct() */ public function testBeforeCompareOptionsSameKeys() { - $options1 = ['associated_product_34' => 3]; - $options2 = ['associated_product_34' => 2]; + $infoBuyRequestMock = $this->createPartialMock( + \Magento\Catalog\Model\Product\Configuration\Item\Option::class, + [ + 'getValue', + ] + ); + + $infoBuyRequestMock->expects($this->atLeastOnce()) + ->method('getValue') + ->willReturn('{"product":"3","action":"add"}'); + $options1 = [ + 'associated_product_34' => 3, + 'info_buyRequest' => $infoBuyRequestMock, + ]; + $options2 = [ + 'associated_product_34' => 3, + 'info_buyRequest' => $infoBuyRequestMock, + ]; $res = $this->model->beforeCompareOptions($this->subjectMock, $options1, $options2); - $this->assertEquals([], $res[0]); - $this->assertEquals([], $res[1]); + $this->assertEquals(['info_buyRequest' => $infoBuyRequestMock], $res[0]); + $this->assertEquals(['info_buyRequest' => $infoBuyRequestMock], $res[1]); } /** @@ -175,16 +191,26 @@ private function getProductAssocOption($initVal, $prodId) { $items = []; - $optionMock = $this->createPartialMock( + $associatedProductMock = $this->createPartialMock( + \Magento\Catalog\Model\Product\Configuration\Item\Option::class, + [ + 'getValue', + ] + ); + $infoBuyRequestMock = $this->createPartialMock( \Magento\Catalog\Model\Product\Configuration\Item\Option::class, [ 'getValue', ] ); - $optionMock->expects($this->once())->method('getValue')->willReturn($initVal); + $associatedProductMock->expects($this->once())->method('getValue')->willReturn($initVal); + $infoBuyRequestMock->expects($this->once()) + ->method('getValue') + ->willReturn('{"product":"'. $prodId . '","action":"add"}'); - $items['associated_product_' . $prodId] = $optionMock; + $items['associated_product_' . $prodId] = $associatedProductMock; + $items['info_buyRequest'] = $infoBuyRequestMock; return $items; } diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/templates/product/stock/disabler.phtml b/app/code/Magento/GroupedProduct/view/adminhtml/templates/product/stock/disabler.phtml index 991ef2b5f4c7c..4e62b5539549f 100644 --- a/app/code/Magento/GroupedProduct/view/adminhtml/templates/product/stock/disabler.phtml +++ b/app/code/Magento/GroupedProduct/view/adminhtml/templates/product/stock/disabler.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require(['jquery'], function($){ $('[data-tab-panel=product-details]').on('stockbeforedisable', function(e) { if (e.productType === 'grouped') { @@ -13,4 +16,6 @@ require(['jquery'], function($){ } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php index 92cfb375fea41..29fa2bffabb3b 100644 --- a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php +++ b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php @@ -10,7 +10,7 @@ use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class GroupedProductLinksTypeResolver implements TypeResolverInterface { @@ -20,14 +20,14 @@ class GroupedProductLinksTypeResolver implements TypeResolverInterface private $linkTypes = ['associated']; /** - * {@inheritdoc} + * @inheritdoc */ - public function resolveType(array $data) : string + public function resolveType(array $data): string { if (isset($data['link_type'])) { $linkType = $data['link_type']; if (in_array($linkType, $this->linkTypes)) { - return 'GroupedProductLinks'; + return 'ProductLinks'; } } return ''; diff --git a/app/code/Magento/GroupedProductGraphQl/etc/di.xml b/app/code/Magento/GroupedProductGraphQl/etc/di.xml index 35b63370baf2f..717bc14826f70 100644 --- a/app/code/Magento/GroupedProductGraphQl/etc/di.xml +++ b/app/code/Magento/GroupedProductGraphQl/etc/di.xml @@ -13,4 +13,11 @@ </argument> </arguments> </type> + <type name="\Magento\CatalogGraphQl\Model\Resolver\Product\BatchProductLinks"> + <arguments> + <argument name="linkTypes" xsi:type="array"> + <item name="associated" xsi:type="string">associated</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php b/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php index 01c41e35fc4eb..f670d97626725 100644 --- a/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php +++ b/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php @@ -10,6 +10,7 @@ /** * Basic interface with data needed for export operation. * @api + * @since 100.3.2 */ interface ExportInfoInterface { @@ -17,6 +18,7 @@ interface ExportInfoInterface * Return filename. * * @return string + * @since 100.3.2 */ public function getFileName(); @@ -25,6 +27,7 @@ public function getFileName(); * * @param string $fileName * @return void + * @since 100.3.2 */ public function setFileName($fileName); @@ -32,6 +35,7 @@ public function setFileName($fileName); * Override standard entity getter. * * @return string + * @since 100.3.2 */ public function getFileFormat(); @@ -40,6 +44,7 @@ public function getFileFormat(); * * @param string $fileFormat * @return void + * @since 100.3.2 */ public function setFileFormat($fileFormat); @@ -47,6 +52,7 @@ public function setFileFormat($fileFormat); * Return content type. * * @return string + * @since 100.3.2 */ public function getContentType(); @@ -55,6 +61,7 @@ public function getContentType(); * * @param string $contentType * @return void + * @since 100.3.2 */ public function setContentType($contentType); @@ -62,6 +69,7 @@ public function setContentType($contentType); * Returns entity. * * @return string + * @since 100.3.2 */ public function getEntity(); @@ -70,6 +78,7 @@ public function getEntity(); * * @param string $entity * @return void + * @since 100.3.2 */ public function setEntity($entity); @@ -77,6 +86,7 @@ public function setEntity($entity); * Returns export filter. * * @return string + * @since 100.3.2 */ public function getExportFilter(); @@ -85,6 +95,7 @@ public function getExportFilter(); * * @param string $exportFilter * @return void + * @since 100.3.2 */ public function setExportFilter($exportFilter); } diff --git a/app/code/Magento/ImportExport/Api/ExportManagementInterface.php b/app/code/Magento/ImportExport/Api/ExportManagementInterface.php index 39bb89b43c838..0383b2a43b45e 100644 --- a/app/code/Magento/ImportExport/Api/ExportManagementInterface.php +++ b/app/code/Magento/ImportExport/Api/ExportManagementInterface.php @@ -12,6 +12,7 @@ /** * Describes how to do export operation with data interface. * @api + * @since 100.3.2 */ interface ExportManagementInterface { @@ -20,6 +21,7 @@ interface ExportManagementInterface * * @param ExportInfoInterface $exportInfo * @return string + * @since 100.3.2 */ public function export(ExportInfoInterface $exportInfo); } diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php index 81fa9767fbaa4..5a9e3a2a8504a 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php @@ -20,9 +20,9 @@ class Download extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Text */ public function _getValue(\Magento\Framework\DataObject $row) { - return '<p> ' . $row->getData('imported_file') . '</p><a href="' + return '<p> ' . $this->escapeHtml($row->getData('imported_file')) . '</p><a href="' . $this->getUrl('*/*/download', ['filename' => $row->getData('imported_file')]) . '">' - . __('Download') + . $this->escapeHtml(__('Download')) . '</a>'; } } diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php index 527bc5025d139..d493fc3fd9531 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php @@ -22,9 +22,9 @@ public function _getValue(\Magento\Framework\DataObject $row) { $result = ''; if ($row->getData('error_file') != '') { - $result = '<p> ' . $row->getData('error_file') . '</p><a href="' + $result = '<p> ' . $this->escapeHtml($row->getData('error_file')) . '</p><a href="' . $this->getUrl('*/*/download', ['filename' => $row->getData('error_file')]) . '">' - . __('Download') + . $this->escapeHtml(__('Download')) . '</a>'; } return $result; 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/File/Delete.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php index 1b7fdc2881073..e31f36e627a66 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php @@ -11,9 +11,11 @@ use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\ValidatorException; use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Filesystem\Directory\WriteFactory; /** * Controller that delete file by name. @@ -31,54 +33,58 @@ class Delete extends ExportController implements HttpPostActionInterface private $filesystem; /** - * @var DriverInterface + * @var WriteFactory */ - private $file; + private $writeFactory; /** * Delete constructor. + * * @param Action\Context $context * @param Filesystem $filesystem - * @param DriverInterface $file + * @param WriteFactory $writeFactory */ public function __construct( Action\Context $context, Filesystem $filesystem, - DriverInterface $file + WriteFactory $writeFactory ) { $this->filesystem = $filesystem; - $this->file = $file; + $this->writeFactory = $writeFactory; parent::__construct($context); } /** * Controller basic method implementation. * - * @return \Magento\Framework\Controller\ResultInterface + * @return ResultInterface */ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath('adminhtml/export/index'); - $fileName = $this->getRequest()->getParam('filename'); - if (empty($fileName) || preg_match('/\.\.(\\\|\/)/', $fileName) !== 0) { - $this->messageManager->addErrorMessage(__('Please provide valid export file name')); - - return $resultRedirect; - } try { - $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); - $path = $directory->getAbsolutePath() . 'export/' . $fileName; + if (empty($fileName = $this->getRequest()->getParam('filename'))) { + $this->messageManager->addErrorMessage(__('Please provide valid export file name')); - if ($directory->isFile($path)) { - $this->file->deleteFile($path); + return $resultRedirect; + } + $directoryWrite = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_EXPORT); + try { + $directoryWrite->delete($directoryWrite->getAbsolutePath($fileName)); $this->messageManager->addSuccessMessage(__('File %1 deleted', $fileName)); - } else { - $this->messageManager->addErrorMessage(__('%1 is not a valid file', $fileName)); + } catch (ValidatorException $exception) { + $this->messageManager->addErrorMessage( + __('Sorry, but the data is invalid or the file is not uploaded.') + ); + } catch (FileSystemException $exception) { + $this->messageManager->addErrorMessage( + __('Sorry, but the data is invalid or the file is not uploaded.') + ); } } catch (FileSystemException $exception) { - $this->messageManager->addErrorMessage($exception->getMessage()); + $this->messageManager->addErrorMessage(__('There are no export file with such name %1', $fileName)); } return $resultRedirect; 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/Helper/Report.php b/app/code/Magento/ImportExport/Helper/Report.php index 02bc4d8b8a047..29d1928d837d6 100644 --- a/app/code/Magento/ImportExport/Helper/Report.php +++ b/app/code/Magento/ImportExport/Helper/Report.php @@ -140,6 +140,7 @@ protected function getFilePath($filename) * Get csv delimiter from request. * * @return string + * @since 100.2.2 */ public function getDelimiter() { diff --git a/app/code/Magento/ImportExport/Model/Export.php b/app/code/Magento/ImportExport/Model/Export.php index 850ded7c8f256..3d7a190919525 100644 --- a/app/code/Magento/ImportExport/Model/Export.php +++ b/app/code/Magento/ImportExport/Model/Export.php @@ -13,7 +13,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 - * @deprecated + * @deprecated 100.3.2 */ class Export extends \Magento\ImportExport\Model\AbstractModel { diff --git a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php index 09b17371ae4e8..085d24ca3a572 100644 --- a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php +++ b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php @@ -39,6 +39,7 @@ class Csv extends AbstractAdapter /** * Object destructor + * @since 100.3.5 */ public function __destruct() { @@ -54,6 +55,19 @@ public function destruct() { if (is_object($this->_fileHandler)) { $this->_fileHandler->close(); + $this->resolveDestination(); + } + } + + /** + * Remove temporary destination + * + * @return void + */ + private function resolveDestination(): void + { + // only temporary file located directly in var folder + if (strpos($this->_destination, '/') === false) { $this->_directoryHandle->delete($this->_destination); } } diff --git a/app/code/Magento/ImportExport/Model/History.php b/app/code/Magento/ImportExport/Model/History.php index b85bf7da81a35..9a97367ba8453 100644 --- a/app/code/Magento/ImportExport/Model/History.php +++ b/app/code/Magento/ImportExport/Model/History.php @@ -44,6 +44,7 @@ class History extends \Magento\Framework\Model\AbstractModel /** * @var \Magento\Backend\Model\Auth\Session + * @since 100.3.1 */ protected $session; diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index cf20001882c0d..fba7c6860bbb5 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -614,6 +614,7 @@ public function uploadSource() * * @return Import\AbstractSource * @throws LocalizedException + * @since 100.2.7 */ public function uploadFileAndGetSource() { diff --git a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php index 5bd956c1bc322..9bf5b945c8fbd 100644 --- a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php @@ -15,6 +15,7 @@ /** * Import entity abstract model * + * phpcs:disable Magento2.Classes.AbstractApi * @api * * @SuppressWarnings(PHPMD.TooManyFields) @@ -335,6 +336,8 @@ public function __construct( } /** + * Returns Error aggregator + * * @return ProcessingErrorAggregatorInterface */ public function getErrorAggregator() @@ -413,7 +416,7 @@ protected function _saveValidatedBunches() $source->rewind(); $this->_dataSourceModel->cleanBunches(); - $masterAttributeCode = $this->getMasterAttributeCode(); + $mainAttributeCode = $this->getMasterAttributeCode(); while ($source->valid() || count($bunchRows) || isset($entityGroup)) { if ($startNewBunch || !$source->valid()) { @@ -453,7 +456,7 @@ protected function _saveValidatedBunches() continue; } - if (isset($rowData[$masterAttributeCode]) && trim($rowData[$masterAttributeCode])) { + if (isset($rowData[$mainAttributeCode]) && trim($rowData[$mainAttributeCode])) { /* Add entity group that passed validation to bunch */ if (isset($entityGroup)) { foreach ($entityGroup as $key => $value) { @@ -590,6 +593,7 @@ public function getBehavior(array $rowData = null) * Get default import behavior * * @return string + * phpcs:disable Magento2.Functions.StaticFunction */ public static function getDefaultBehavior() { @@ -652,7 +656,9 @@ public function isAttributeParticular($attributeCode) } /** - * @return string the master attribute code to use in an import + * Returns the master attribute code to use in an import + * + * @return string */ public function getMasterAttributeCode() { diff --git a/app/code/Magento/ImportExport/Model/Report/Csv.php b/app/code/Magento/ImportExport/Model/Report/Csv.php index 7279092265cbb..e7ddef1008444 100644 --- a/app/code/Magento/ImportExport/Model/Report/Csv.php +++ b/app/code/Magento/ImportExport/Model/Report/Csv.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\ImportExport\Model\Report; @@ -60,22 +61,16 @@ public function __construct( } /** - * @param string $originalFileName - * @param ProcessingErrorAggregatorInterface $errorAggregator - * @param bool $writeOnlyErrorItems - * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @inheritDoc */ public function createReport( $originalFileName, ProcessingErrorAggregatorInterface $errorAggregator, $writeOnlyErrorItems = false ) { - $sourceCsv = $this->createSourceCsvModel($originalFileName); - - $outputFileName = $this->generateOutputFileName($originalFileName); - $outputCsv = $this->createOutputCsvModel($outputFileName); + $outputCsv = $this->outputCsvFactory->create(); + $sourceCsv = $this->createSourceCsvModel($originalFileName); $columnsName = $sourceCsv->getColNames(); $columnsName[] = self::REPORT_ERROR_COLUMN_NAME; $outputCsv->setHeaderCols($columnsName); @@ -88,10 +83,16 @@ public function createReport( } } + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $outputFileName = $this->generateOutputFileName($originalFileName); + $directory->writeFile(Import::IMPORT_HISTORY_DIR . $outputFileName, $outputCsv->getContents()); + return $outputFileName; } /** + * Retrieve error messages + * * @param int $rowNumber * @param ProcessingErrorAggregatorInterface $errorAggregator * @return string @@ -112,16 +113,21 @@ public function retrieveErrorMessagesByRowNumber($rowNumber, ProcessingErrorAggr } /** + * Generate output filename based on source filename + * * @param string $sourceFile * @return string */ protected function generateOutputFileName($sourceFile) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $fileName = basename($sourceFile, self::ERROR_REPORT_FILE_EXTENSION); return $fileName . self::ERROR_REPORT_FILE_SUFFIX . self::ERROR_REPORT_FILE_EXTENSION; } /** + * Create source CSV model + * * @param string $sourceFile * @return \Magento\ImportExport\Model\Import\Source\Csv */ @@ -135,18 +141,4 @@ protected function createSourceCsvModel($sourceFile) ] ); } - - /** - * @param string $outputFileName - * @return \Magento\ImportExport\Model\Export\Adapter\Csv - */ - protected function createOutputCsvModel($outputFileName) - { - return $this->outputCsvFactory->create( - [ - 'destination' => Import::IMPORT_HISTORY_DIR . $outputFileName, - 'destinationDirectoryCode' => DirectoryList::VAR_DIR, - ] - ); - } } diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml index a45783767e6a2..25331ae3cd058 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml @@ -33,8 +33,7 @@ </before> <after> <!--Delete Product and Category--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> <argument name="productName" value="simpleProductWithShortNameAndSku.name"/> </actionGroup> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml index 99622caf0697e..fe5f4358bfca3 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml @@ -44,6 +44,8 @@ <waitForPageLoad stepKey="waitForProductPageLoad"/> <seeInCurrentUrl url="{{StorefrontProductPage.url('simpleprod')}}" stepKey="seeUpdatedUrl"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createProduct.name$$" stepKey="assertProductName"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createProduct.sku$$" stepKey="assertProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="assertProductSku"> + <argument name="productSku" value="$$createProduct.sku$$"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/DownloadTest.php b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/DownloadTest.php index 93bc4be3bb423..d9a00c16cb0af 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/DownloadTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/DownloadTest.php @@ -10,14 +10,19 @@ use Magento\Backend\Block\Context; use Magento\Backend\Model\Url; use Magento\Framework\DataObject; +use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\ImportExport\Block\Adminhtml\Grid\Column\Renderer\Download; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\ImportExport\Block\Adminhtml\Grid\Column\Renderer\Download class. + */ class DownloadTest extends TestCase { /** - * @var Context + * @var Context|MockObject */ protected $context; @@ -32,14 +37,21 @@ class DownloadTest extends TestCase protected $download; /** - * Set up + * @var Escaper|MockObject + */ + private $escaperMock; + + /** + * @inheritdoc */ protected function setUp(): void { + $this->escaperMock = $this->createMock(Escaper::class); $urlModel = $this->createPartialMock(Url::class, ['getUrl']); $urlModel->expects($this->any())->method('getUrl')->willReturn('url'); - $this->context = $this->createPartialMock(Context::class, ['getUrlBuilder']); + $this->context = $this->createPartialMock(Context::class, ['getUrlBuilder', 'getEscaper']); $this->context->expects($this->any())->method('getUrlBuilder')->willReturn($urlModel); + $this->context->expects($this->any())->method('getEscaper')->willReturn($this->escaperMock); $data = []; $this->objectManagerHelper = new ObjectManagerHelper($this); @@ -47,7 +59,7 @@ protected function setUp(): void Download::class, [ 'context' => $this->context, - 'data' => $data + 'data' => $data, ] ); } @@ -59,6 +71,14 @@ public function testGetValue() { $data = ['imported_file' => 'file.csv']; $row = new DataObject($data); + $this->escaperMock->expects($this->at(0)) + ->method('escapeHtml') + ->with('file.csv') + ->willReturn('file.csv'); + $this->escaperMock->expects($this->at(1)) + ->method('escapeHtml') + ->with('Download') + ->willReturn('Download'); $this->assertEquals('<p> file.csv</p><a href="url">Download</a>', $this->download->_getValue($row)); } } diff --git a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Export/File/DeleteTest.php b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Export/File/DeleteTest.php deleted file mode 100644 index 5796f6d3bf4be..0000000000000 --- a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Export/File/DeleteTest.php +++ /dev/null @@ -1,198 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\ImportExport\Test\Unit\Controller\Adminhtml\Export\File; - -use Magento\Backend\App\Action\Context; -use Magento\Backend\Model\View\Result\Redirect; -use Magento\Framework\App\Request\Http; -use Magento\Framework\Controller\Result\Raw; -use Magento\Framework\Controller\Result\RedirectFactory; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\ReadInterface; -use Magento\Framework\Filesystem\DriverInterface; -use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\ImportExport\Controller\Adminhtml\Export\File\Delete; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class DeleteTest extends TestCase -{ - /** - * @var Context|MockObject - */ - private $contextMock; - - /** - * @var ObjectManagerHelper - */ - private $objectManagerHelper; - - /** - * @var Http|MockObject - */ - private $requestMock; - - /** - * @var Raw|MockObject - */ - private $redirectMock; - - /** - * @var RedirectFactory|MockObject - */ - private $resultRedirectFactoryMock; - - /** - * @var Filesystem|MockObject - */ - private $fileSystemMock; - - /** - * @var DriverInterface|MockObject - */ - private $fileMock; - - /** - * @var Delete|MockObject - */ - private $deleteControllerMock; - - /** - * @var ManagerInterface|MockObject - */ - private $messageManagerMock; - - /** - * @var ReadInterface|MockObject - */ - private $directoryMock; - - /** - * Set up - */ - protected function setUp(): void - { - $this->requestMock = $this->getMockBuilder(Http::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->fileSystemMock = $this->getMockBuilder(Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->directoryMock = $this->getMockBuilder(ReadInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->fileMock = $this->getMockBuilder(DriverInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->contextMock = $this->createPartialMock( - Context::class, - ['getRequest', 'getResultRedirectFactory', 'getMessageManager'] - ); - - $this->redirectMock = $this->createPartialMock(Redirect::class, ['setPath']); - - $this->resultRedirectFactoryMock = $this->createPartialMock( - RedirectFactory::class, - ['create'] - ); - $this->resultRedirectFactoryMock->expects($this->any())->method('create')->willReturn($this->redirectMock); - $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->any()) - ->method('getResultRedirectFactory') - ->willReturn($this->resultRedirectFactoryMock); - - $this->contextMock->expects($this->any()) - ->method('getMessageManager') - ->willReturn($this->messageManagerMock); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->deleteControllerMock = $this->objectManagerHelper->getObject( - Delete::class, - [ - 'context' => $this->contextMock, - 'filesystem' => $this->fileSystemMock, - 'file' => $this->fileMock - ] - ); - } - - /** - * Tests download controller with different file names in request. - */ - public function testExecuteSuccess() - { - $this->requestMock->method('getParam') - ->with('filename') - ->willReturn('sampleFile'); - - $this->fileSystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->willReturn($this->directoryMock); - $this->directoryMock->expects($this->once())->method('isFile')->willReturn(true); - $this->fileMock->expects($this->once())->method('deleteFile')->willReturn(true); - $this->messageManagerMock->expects($this->once())->method('addSuccessMessage'); - - $this->deleteControllerMock->execute(); - } - - /** - * Tests download controller with different file names in request. - */ - public function testExecuteFileDoesntExists() - { - $this->requestMock->method('getParam') - ->with('filename') - ->willReturn('sampleFile'); - - $this->fileSystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->willReturn($this->directoryMock); - $this->directoryMock->expects($this->once())->method('isFile')->willReturn(false); - $this->messageManagerMock->expects($this->once())->method('addErrorMessage'); - - $this->deleteControllerMock->execute(); - } - - /** - * Test execute() with invalid file name - * @param string $requestFilename - * @dataProvider invalidFileDataProvider - */ - public function testExecuteInvalidFileName($requestFilename) - { - $this->requestMock->method('getParam')->with('filename')->willReturn($requestFilename); - $this->messageManagerMock->expects($this->once())->method('addErrorMessage'); - - $this->deleteControllerMock->execute(); - } - - /** - * Data provider to test possible invalid filenames - * @return array - */ - public function invalidFileDataProvider() - { - return [ - 'Relative file name' => ['../.htaccess'], - 'Empty file name' => [''], - 'Null file name' => [null], - ]; - } -} 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/ImportExport/Test/Unit/Model/Report/CsvTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php deleted file mode 100644 index 9ca7f2bcbc9c7..0000000000000 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php +++ /dev/null @@ -1,124 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\ImportExport\Test\Unit\Model\Report; - -use Magento\Framework\Filesystem; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\ImportExport\Helper\Report; -use Magento\ImportExport\Model\Export\Adapter\Csv; -use Magento\ImportExport\Model\Export\Adapter\CsvFactory; -use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; -use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class CsvTest extends TestCase -{ - /** - * @var Report|MockObject - */ - protected $reportHelperMock; - - /** - * @var CsvFactory|MockObject - */ - protected $outputCsvFactoryMock; - - /** - * @var Csv|MockObject - */ - protected $outputCsvMock; - - /** - * @var \Magento\ImportExport\Model\Import\Source\CsvFactory|MockObject - */ - protected $sourceCsvFactoryMock; - - /** - * @var Csv|MockObject - */ - protected $sourceCsvMock; - - /** - * @var Filesystem|MockObject - */ - protected $filesystemMock; - - /** - * @var \Magento\ImportExport\Model\Report\Csv|ObjectManager - */ - protected $csvModel; - - protected function setUp(): void - { - $objectManager = new ObjectManager($this); - $testDelimiter = 'some_delimiter'; - - $this->reportHelperMock = $this->createMock(Report::class); - $this->reportHelperMock->expects($this->any())->method('getDelimiter')->willReturn($testDelimiter); - - $this->outputCsvFactoryMock = $this->createPartialMock( - CsvFactory::class, - ['create'] - ); - $this->outputCsvMock = $this->createMock(Csv::class); - $this->outputCsvFactoryMock->expects($this->any())->method('create')->willReturn($this->outputCsvMock); - - $this->sourceCsvFactoryMock = $this->createPartialMock( - \Magento\ImportExport\Model\Import\Source\CsvFactory::class, - ['create'] - ); - $this->sourceCsvMock = $this->createMock(\Magento\ImportExport\Model\Import\Source\Csv::class); - $this->sourceCsvMock->expects($this->any())->method('valid')->willReturnOnConsecutiveCalls(true, true, false); - $this->sourceCsvMock->expects($this->any())->method('current')->willReturnOnConsecutiveCalls( - [23 => 'first error'], - [27 => 'second error'] - ); - $this->sourceCsvFactoryMock - ->expects($this->any()) - ->method('create') - ->with( - [ - 'file' => 'some_file_name', - 'directory' => null, - 'delimiter' => $testDelimiter - ] - ) - ->willReturn($this->sourceCsvMock); - - $this->filesystemMock = $this->createMock(Filesystem::class); - - $this->csvModel = $objectManager->getObject( - \Magento\ImportExport\Model\Report\Csv::class, - [ - 'reportHelper' => $this->reportHelperMock, - 'sourceCsvFactory' => $this->sourceCsvFactoryMock, - 'outputCsvFactory' => $this->outputCsvFactoryMock, - 'filesystem' => $this->filesystemMock - ] - ); - } - - public function testCreateReport() - { - $errorAggregatorMock = $this->createMock( - ProcessingErrorAggregator::class - ); - $errorProcessingMock = $this->createPartialMock( - ProcessingError::class, - ['getErrorMessage'] - ); - $errorProcessingMock->expects($this->any())->method('getErrorMessage')->willReturn('some_error_message'); - $errorAggregatorMock->expects($this->any())->method('getErrorByRowNumber')->willReturn([$errorProcessingMock]); - $this->sourceCsvMock->expects($this->any())->method('getColNames')->willReturn([]); - - $name = $this->csvModel->createReport('some_file_name', $errorAggregatorMock, true); - - $this->assertEquals($name, 'some_file_name_error_report.csv'); - } -} diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/after.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/after.phtml index 150f7dbeb1046..784e140041004 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/after.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/after.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\ImportExport\Block\Adminhtml\Form\After $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<fieldset class="admin__fieldset" id="export_filter_container" style="display:none;"> +<fieldset class="admin__fieldset" id="export_filter_container"> <legend class="admin__legend"> <span><?= $block->escapeHtml(__('Entity Attributes')) ?></span> </legend> @@ -13,11 +16,17 @@ <input name="form_key" type="hidden" value="<?= /* @noEscape */ $block->getFormKey() ?>" /> <div id="export_filter_grid_container"><!-- --></div> </form> - <button class="action- scalable" type="button" onclick="getFile();"><span><?= - $block->escapeHtml(__('Continue')) - ?></span></button> + <button class="action- scalable" type="button"> + <span><?= $block->escapeHtml(__('Continue')) ?></span> + </button> </fieldset> -<script> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'fieldset#export_filter_container') ?> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "getFile();", + 'fieldset#export_filter_container button' +) ?> +<?php $scriptString = <<<script require(['prototype'], function(){ //<![CDATA[ @@ -25,4 +34,6 @@ require(['prototype'], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml index 3e7a19a0c0d82..b569518d9d239 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Backend\Block\Template $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require([ 'Magento_Ui/js/modal/alert', 'prototype' @@ -26,14 +30,14 @@ require([ * Handle value change in entity type selector */ modifyFilterGrid: function() { - if ($('entity') && $F('entity') && $F('entity') != 'catalog_product') { - $$('col:first-child').each(function(el) { + if ($('entity') && \$F('entity') && \$F('entity') != 'catalog_product') { + \$$('col:first-child').each(function(el) { el.show(); }); - $$('th.no-link:first-child').each(function(el) { + \$$('th.no-link:first-child').each(function(el) { el.show(); }); - $$('td.a-center').each(function(el) { + \$$('td.a-center').each(function(el) { el.show(); }); } @@ -43,9 +47,9 @@ require([ * Post form data and process response via AJAX */ getFilter: function() { - if ($('entity') && $F('entity')) { - var url = "<?= $block->escapeJs($block->escapeUrl($block->getUrl('*/*/getFilter'))) ?>"; - var entity = $F('entity'); + if ($('entity') && \$F('entity')) { + var url = "{$block->escapeJs($block->getUrl('*/*/getFilter'))}"; + var entity = \$F('entity'); if (entity != this.previousGridEntity) { this.previousGridEntity = entity; url += ((url.slice(-1) != '/') ? '/' : '') + 'entity/' + entity; @@ -76,20 +80,20 @@ require([ * return void */ getFile = function() { - if ($('entity') && $F('entity')) { + if ($('entity') && \$F('entity')) { var form = $('export_filter_form'); var oldAction = form.action; - var url = oldAction + ((oldAction.slice(-1) != '/') ? '/' : '') + 'entity/' + $F('entity') - + '/file_format/' + $F('file_format'); - if ($F('fields_enclosure')) { - url += '/fields_enclosure/' + $F('fields_enclosure'); + var url = oldAction + ((oldAction.slice(-1) != '/') ? '/' : '') + 'entity/' + \$F('entity') + + '/file_format/' + \$F('file_format'); + if (\$F('fields_enclosure')) { + url += '/fields_enclosure/' + \$F('fields_enclosure'); } form.action = url; form.submit(); form.action = oldAction; } else { alert({ - content: '<?= $block->escapeHtml(__('Invalid data')); ?>' + content: '{$block->escapeHtml(__('Invalid data'))}' }); } }; @@ -98,4 +102,6 @@ require([ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/filter/after.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/filter/after.phtml index 704b88b0c0f69..a34eaf09c0058 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/filter/after.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/filter/after.phtml @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Backend\Block\Template $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require([ 'mage/adminhtml/grid' ], function(){ @@ -17,4 +21,6 @@ require([ }; } }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/after.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/after.phtml index f629e6c9e9f59..5a59ffca17cb5 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/after.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/after.phtml @@ -3,19 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\ImportExport\Block\Adminhtml\Form\After $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div class="entry-edit fieldset" id="import_validation_container" style="display:none;"> + +<div class="entry-edit fieldset" id="import_validation_container"> <div class="entry-edit-head legend"> <span class="icon-head head-edit-form fieldset-legend" id="import_validation_container_header"><?= $block->escapeHtml(__('Validation Results')) ?></span> </div><br> <div id="import_validation_messages" class="fieldset"><!-- --></div> </div> -<script> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'div#import_validation_container') ?> +<?php $scriptString = <<<script require(['jquery', 'Magento_Ui/js/modal/alert', 'prototype'], function(jQuery){ //<![CDATA[ varienImport.resetSelectIndex('entity'); // forced resetting entity selector after page refresh //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml index bd88ec419d848..69779baba381d 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml @@ -6,8 +6,10 @@ ?> <?php /** @var $block \Magento\ImportExport\Block\Adminhtml\Import\Edit\Before */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require([ 'jquery', 'Magento_Ui/js/modal/alert', @@ -27,27 +29,25 @@ require([ * List of existing behavior sets * @type {Array} */ - uniqueBehaviors: <?= /* @noEscape */ $block->getUniqueBehaviors() ?>, + uniqueBehaviors: {$block->getUniqueBehaviors()}, /** * Behaviour codes for import entities * @type {Array} */ - entityBehaviors: <?= /* @noEscape */ $block->getEntityBehaviors() ?>, + entityBehaviors: {$block->getEntityBehaviors()}, /** * Behaviour notes for import entities * @type {Array} */ - entityBehaviorsNotes: <?= /* @noEscape */ $block->getEntityBehaviorsNotes() ?>, + entityBehaviorsNotes: {$block->getEntityBehaviorsNotes()}, /** * Base url * @type {string} */ - sampleFilesBaseUrl: '<?= $block->escapeJs( - $block->escapeUrl($block->getUrl('*/*/download/', ['filename' => 'entity-name'])) - ) ?>', + sampleFilesBaseUrl: '{$block->escapeJs($block->getUrl('*/*/download/', ['filename' => 'entity-name']))}', /** * Reset selected index @@ -168,8 +168,8 @@ require([ */ postToFrame: function(newActionUrl) { if (!jQuery('[name="' + this.ifrElemName + '"]').length) { - jQuery('body').append('<iframe name="' + this.ifrElemName + '" id="' + this.ifrElemName - + '" style="display:none;"/>'); + jQuery('body').append('<iframe name="' + this.ifrElemName + '" id="' + this.ifrElemName + '"/>'); + jQuery('iframe#' + this.ifrElemName).attr('display', 'none'); } jQuery('body') .loader({ @@ -209,17 +209,17 @@ require([ postToFrameProcessResponse: function(response) { if ('object' != typeof(response)) { alert({ - content: '<?= $block->escapeHtml(__('Invalid response')); ?>' + content: '{$block->escapeHtml(__('Invalid response'))}' }); return false; } - $H(response).each(function(pair) { + \$H(response).each(function(pair) { switch (pair.key) { case 'show': case 'clear': case 'hide': - $H(pair.value).each(function(val) { + \$H(pair.value).each(function(val) { if ($(val.value)) { $(val.value)[pair.key](); } @@ -227,7 +227,7 @@ require([ break; case 'innerHTML': case 'value': - $H(pair.value).each(function(val) { + \$H(pair.value).each(function(val) { var el = $(val.key); if (el) { el[pair.key] = val.value; @@ -238,7 +238,7 @@ require([ break; case 'removeClassName': case 'addClassName': - $H(pair.value).each(function(val) { + \$H(pair.value).each(function(val) { if ($(val.key)) $(val.key)[pair.key](val.value); }); break; @@ -265,4 +265,6 @@ require([ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/import/frame/result.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/import/frame/result.phtml index 57f521fba946f..08b8b0414e81e 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/import/frame/result.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/import/frame/result.phtml @@ -3,9 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script type='text/javascript'> +<?php $scriptString = <<<script + //<![CDATA[ - top.varienImport.postToFrameComplete(<?= /* @noEscape */ $block->getResponseJson() ?>); + top.varienImport.postToFrameComplete({$block->getResponseJson()}); //]]> -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> 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/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Indexer/Model/ResourceModel/AbstractResource.php index 7032c9258cae6..d86b15134283b 100644 --- a/app/code/Magento/Indexer/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Indexer/Model/ResourceModel/AbstractResource.php @@ -11,6 +11,7 @@ /** * Abstract resource model. Can be used as base for indexer resources * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 */ @@ -120,8 +121,7 @@ public function insertFromTable($sourceTable, $destTable, $readToIndex = true) } /** - * Insert data from select statement of read adapter to - * destination table related with index adapter + * Insert data from select statement of read adapter to destination table related with index adapter * * @param Select $select * @param string $destTable @@ -141,6 +141,9 @@ public function insertFromSelect($select, $destTable, array $columns, $readToInd if ($from === $to) { $query = $select->insertFromSelect($destTable, $columns); + if ($to->getTransactionLevel() === 0) { + $to->query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED;'); + } $to->query($query); } else { $stmt = $from->query($select); 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/Mftf/ActionGroup/IndexerActionGroup/UpdateIndexerOnSaveActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/IndexerActionGroup/UpdateIndexerOnSaveActionGroup.xml deleted file mode 100644 index efa6291d5de63..0000000000000 --- a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/IndexerActionGroup/UpdateIndexerOnSaveActionGroup.xml +++ /dev/null @@ -1,11 +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="updateIndexerOnSave" extends="AdminIndexerSetUpdateOnSaveActionGroup" deprecated="Use AdminIndexerSetUpdateOnSaveActionGroup"/> -</actionGroups> diff --git a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementGridChangesTest.xml b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementGridChangesTest.xml index 84619a5213128..51cf4aa26a1b1 100644 --- a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementGridChangesTest.xml +++ b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementGridChangesTest.xml @@ -23,15 +23,23 @@ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI command="indexer:set-mode" arguments="schedule" stepKey="setIndexerModeSchedule"/> - <magentoCLI command="indexer:reindex" stepKey="indexerReindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/></before> <after> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setIndexerModeRealTime"/> - <magentoCLI command="indexer:reindex" stepKey="indexerReindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> 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/InstantPurchase/Block/Button.php b/app/code/Magento/InstantPurchase/Block/Button.php index e6ea50073ed48..d4f19918afaf3 100644 --- a/app/code/Magento/InstantPurchase/Block/Button.php +++ b/app/code/Magento/InstantPurchase/Block/Button.php @@ -13,6 +13,7 @@ * Configuration for JavaScript instant purchase button component. * * @api + * @since 100.2.0 */ class Button extends Template { @@ -40,6 +41,7 @@ public function __construct( * Checks if button enabled. * * @return bool + * @since 100.2.0 */ public function isEnabled(): bool { @@ -48,6 +50,7 @@ public function isEnabled(): bool /** * @inheritdoc + * @since 100.2.0 */ public function getJsLayout(): string { diff --git a/app/code/Magento/InstantPurchase/Model/BillingAddressChoose/BillingAddressChooserInterface.php b/app/code/Magento/InstantPurchase/Model/BillingAddressChoose/BillingAddressChooserInterface.php index cfed97cfefd36..91eb91e2cd33c 100644 --- a/app/code/Magento/InstantPurchase/Model/BillingAddressChoose/BillingAddressChooserInterface.php +++ b/app/code/Magento/InstantPurchase/Model/BillingAddressChoose/BillingAddressChooserInterface.php @@ -12,12 +12,14 @@ * Interface to choose billing address for a customer if available. * * @api + * @since 100.2.0 */ interface BillingAddressChooserInterface { /** * @param Customer $customer * @return Address|null + * @since 100.2.0 */ public function choose(Customer $customer); } diff --git a/app/code/Magento/InstantPurchase/Model/InstantPurchaseInterface.php b/app/code/Magento/InstantPurchase/Model/InstantPurchaseInterface.php index b205ccab5067d..24a9569128064 100644 --- a/app/code/Magento/InstantPurchase/Model/InstantPurchaseInterface.php +++ b/app/code/Magento/InstantPurchase/Model/InstantPurchaseInterface.php @@ -12,6 +12,7 @@ * Interface for detecting customer option to make instant purchase in a store. * * @api + * @since 100.2.0 */ interface InstantPurchaseInterface { @@ -21,6 +22,7 @@ interface InstantPurchaseInterface * @param Store $store * @param Customer $customer * @return InstantPurchaseOption + * @since 100.2.0 */ public function getOption( Store $store, diff --git a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php index 0748c5818c857..11ab119d6e5a5 100644 --- a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php +++ b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php @@ -16,6 +16,7 @@ * Option to make instant purchase. * * @api + * @since 100.2.0 */ class InstantPurchaseOption { @@ -82,6 +83,7 @@ public function __construct( * Checks if option available * * @return bool + * @since 100.2.0 */ public function isAvailable(): bool { @@ -98,6 +100,7 @@ public function isAvailable(): bool * * @return PaymentTokenInterface * @throws LocalizedException if payment token is not defined + * @since 100.2.0 */ public function getPaymentToken(): PaymentTokenInterface { @@ -114,6 +117,7 @@ public function getPaymentToken(): PaymentTokenInterface * * @return Address * @throws LocalizedException if shipping address is not defined + * @since 100.2.0 */ public function getShippingAddress(): Address { @@ -128,6 +132,7 @@ public function getShippingAddress(): Address * * @return Address * @throws LocalizedException if billing address is not defined + * @since 100.2.0 */ public function getBillingAddress(): Address { @@ -142,6 +147,7 @@ public function getBillingAddress(): Address * * @return ShippingMethodInterface * @throws LocalizedException if shipping method is not defined + * @since 100.2.0 */ public function getShippingMethod(): ShippingMethodInterface { diff --git a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionFactory.php b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionFactory.php index bd5811578ab1c..6e9490c9edb69 100644 --- a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionFactory.php +++ b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionFactory.php @@ -14,6 +14,7 @@ * Create instances of instant purchase option. * * @api + * @since 100.2.0 */ class InstantPurchaseOptionFactory { @@ -39,6 +40,7 @@ public function __construct(ObjectManagerInterface $objectManager) * @param Address|null $billingAddress * @param ShippingMethodInterface|null $shippingMethod * @return InstantPurchaseOption + * @since 100.2.0 */ public function create( PaymentTokenInterface $paymentToken = null, @@ -58,6 +60,7 @@ public function create( * Creates new empty instance (no option available). * * @return InstantPurchaseOption + * @since 100.2.0 */ public function createDisabledOption(): InstantPurchaseOption { diff --git a/app/code/Magento/InstantPurchase/Model/PaymentMethodChoose/PaymentTokenChooserInterface.php b/app/code/Magento/InstantPurchase/Model/PaymentMethodChoose/PaymentTokenChooserInterface.php index 2a4f1adeb4155..b96173081164c 100644 --- a/app/code/Magento/InstantPurchase/Model/PaymentMethodChoose/PaymentTokenChooserInterface.php +++ b/app/code/Magento/InstantPurchase/Model/PaymentMethodChoose/PaymentTokenChooserInterface.php @@ -13,6 +13,7 @@ * Interface to choose one of the stored payment methods for a customer if available. * * @api + * @since 100.2.0 */ interface PaymentTokenChooserInterface { @@ -20,6 +21,7 @@ interface PaymentTokenChooserInterface * @param Store $store * @param Customer $customer * @return PaymentTokenInterface|null + * @since 100.2.0 */ public function choose(Store $store, Customer $customer); } diff --git a/app/code/Magento/InstantPurchase/Model/PlaceOrder.php b/app/code/Magento/InstantPurchase/Model/PlaceOrder.php index 2ad56a8859cf3..bd30d19d6456b 100644 --- a/app/code/Magento/InstantPurchase/Model/PlaceOrder.php +++ b/app/code/Magento/InstantPurchase/Model/PlaceOrder.php @@ -21,6 +21,7 @@ * Place an order using instant purchase option. * * @api + * @since 100.2.0 */ class PlaceOrder { @@ -90,6 +91,7 @@ public function __construct( * @return int order identifier * @throws LocalizedException if order can not be placed. * @throws Throwable if unpredictable error occurred. + * @since 100.2.0 */ public function placeOrder( Store $store, diff --git a/app/code/Magento/InstantPurchase/Model/QuoteManagement/PaymentConfiguration.php b/app/code/Magento/InstantPurchase/Model/QuoteManagement/PaymentConfiguration.php index 9c8c44231e843..d1dfb11851500 100644 --- a/app/code/Magento/InstantPurchase/Model/QuoteManagement/PaymentConfiguration.php +++ b/app/code/Magento/InstantPurchase/Model/QuoteManagement/PaymentConfiguration.php @@ -15,6 +15,7 @@ * Configure payment method for quote. * * @api May be used for pluginization. + * @since 100.2.0 */ class PaymentConfiguration { @@ -42,6 +43,7 @@ public function __construct( * @param PaymentTokenInterface $paymentToken * @return Quote * @throws LocalizedException if payment method can not be configured for a quote. + * @since 100.2.0 */ public function configurePayment( Quote $quote, diff --git a/app/code/Magento/InstantPurchase/Model/QuoteManagement/Purchase.php b/app/code/Magento/InstantPurchase/Model/QuoteManagement/Purchase.php index b8220b4ca87eb..d05a4f2a1621a 100644 --- a/app/code/Magento/InstantPurchase/Model/QuoteManagement/Purchase.php +++ b/app/code/Magento/InstantPurchase/Model/QuoteManagement/Purchase.php @@ -14,6 +14,7 @@ * Purchase products from quote. * * @api May be used for pluginization. + * @since 100.2.0 */ class Purchase { @@ -46,6 +47,7 @@ public function __construct( * @param Quote $quote * @return int Order id * @throws LocalizedException if order can not be placed for a quote. + * @since 100.2.0 */ public function purchase(Quote $quote): int { diff --git a/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteCreation.php b/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteCreation.php index 993a64a3f0d7d..4db9c86f7184e 100644 --- a/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteCreation.php +++ b/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteCreation.php @@ -16,6 +16,7 @@ * Create Quote for instance purchase. * * @api May be used for pluginization. + * @since 100.2.0 */ class QuoteCreation { @@ -43,6 +44,7 @@ public function __construct( * @param Address $billingAddress * @return Quote * @throws LocalizedException if quote can not be created. + * @since 100.2.0 */ public function createQuote( Store $store, diff --git a/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteFilling.php b/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteFilling.php index 4afa964050fcd..727917f9e3406 100644 --- a/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteFilling.php +++ b/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteFilling.php @@ -14,6 +14,7 @@ * Fill quote with products for instant purchase. * * @api May be used for pluginization. + * @since 100.2.0 */ class QuoteFilling { @@ -25,6 +26,7 @@ class QuoteFilling * @param array $productRequest * @return Quote * @throws LocalizedException if product can not be added to quote. + * @since 100.2.0 */ public function fillQuote( Quote $quote, diff --git a/app/code/Magento/InstantPurchase/Model/QuoteManagement/ShippingConfiguration.php b/app/code/Magento/InstantPurchase/Model/QuoteManagement/ShippingConfiguration.php index 796fb924b31c9..772d6c6a033fa 100644 --- a/app/code/Magento/InstantPurchase/Model/QuoteManagement/ShippingConfiguration.php +++ b/app/code/Magento/InstantPurchase/Model/QuoteManagement/ShippingConfiguration.php @@ -16,6 +16,7 @@ * Configure shipping method for instant purchase * * @api May be used for pluginization. + * @since 100.2.0 */ class ShippingConfiguration { @@ -41,6 +42,7 @@ public function __construct( * @param ShippingMethodInterface $shippingMethod * @return Quote * @throws LocalizedException if shipping can not be configured for a quote. + * @since 100.2.0 */ public function configureShippingMethod( Quote $quote, diff --git a/app/code/Magento/InstantPurchase/Model/ShippingAddressChoose/ShippingAddressChooserInterface.php b/app/code/Magento/InstantPurchase/Model/ShippingAddressChoose/ShippingAddressChooserInterface.php index a65c8036a1e82..06f4358ecfa92 100644 --- a/app/code/Magento/InstantPurchase/Model/ShippingAddressChoose/ShippingAddressChooserInterface.php +++ b/app/code/Magento/InstantPurchase/Model/ShippingAddressChoose/ShippingAddressChooserInterface.php @@ -12,12 +12,14 @@ * Interface to choose shipping address for a customer if available. * * @api + * @since 100.2.0 */ interface ShippingAddressChooserInterface { /** * @param Customer $customer * @return Address|null + * @since 100.2.0 */ public function choose(Customer $customer); } diff --git a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserInterface.php b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserInterface.php index 3339ca34ee29b..0476f2c690d4d 100644 --- a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserInterface.php +++ b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserInterface.php @@ -11,6 +11,7 @@ * Provides mechanism to defer shipping method choose to the moment when quote is defined. * * @api + * @since 100.2.0 */ interface DeferredShippingMethodChooserInterface { @@ -24,6 +25,7 @@ interface DeferredShippingMethodChooserInterface * * @param Address $address * @return string|null Quote shipping method code if available + * @since 100.2.0 */ public function choose(Address $address); } diff --git a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php index ca0e9351967ad..6e2e8e562167c 100644 --- a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php +++ b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php @@ -10,6 +10,7 @@ * Use deferred shipping method code as a key for a deferred chooser. * * @api + * @since 100.2.0 */ class DeferredShippingMethodChooserPool { @@ -34,6 +35,7 @@ public function __construct(array $choosers) /** * @param string $type * @return DeferredShippingMethodChooserInterface + * @since 100.2.0 */ public function get($type) : DeferredShippingMethodChooserInterface { diff --git a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingMethodChooserInterface.php b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingMethodChooserInterface.php index c227ad793f255..dd3f9d0a6cd52 100644 --- a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingMethodChooserInterface.php +++ b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingMethodChooserInterface.php @@ -20,12 +20,14 @@ * DeferredShippingMethodChooserPool. * * @api + * @since 100.2.0 */ interface ShippingMethodChooserInterface { /** * @param Address $address * @return ShippingMethodInterface|null + * @since 100.2.0 */ public function choose(Address $address); } diff --git a/app/code/Magento/InstantPurchase/Model/Ui/CustomerAddressesFormatter.php b/app/code/Magento/InstantPurchase/Model/Ui/CustomerAddressesFormatter.php index 31c98143385a6..3b886fb03929e 100644 --- a/app/code/Magento/InstantPurchase/Model/Ui/CustomerAddressesFormatter.php +++ b/app/code/Magento/InstantPurchase/Model/Ui/CustomerAddressesFormatter.php @@ -11,6 +11,7 @@ * Address string presentation. * * @api May be used for pluginization. + * @since 100.2.0 */ class CustomerAddressesFormatter { @@ -19,6 +20,7 @@ class CustomerAddressesFormatter * * @param Address $address * @return string + * @since 100.2.0 */ public function format(Address $address): string { diff --git a/app/code/Magento/InstantPurchase/Model/Ui/PaymentTokenFormatter.php b/app/code/Magento/InstantPurchase/Model/Ui/PaymentTokenFormatter.php index 4c2a78e3a6024..16992ff6bf3f1 100644 --- a/app/code/Magento/InstantPurchase/Model/Ui/PaymentTokenFormatter.php +++ b/app/code/Magento/InstantPurchase/Model/Ui/PaymentTokenFormatter.php @@ -12,6 +12,7 @@ * Payment token string presentation. * * @api May be used for pluginization. + * @since 100.2.0 */ class PaymentTokenFormatter { @@ -34,6 +35,7 @@ public function __construct(IntegrationsManager $integrationsManager) * * @param PaymentTokenInterface $paymentToken * @return string + * @since 100.2.0 */ public function format(PaymentTokenInterface $paymentToken): string { diff --git a/app/code/Magento/InstantPurchase/Model/Ui/ShippingMethodFormatter.php b/app/code/Magento/InstantPurchase/Model/Ui/ShippingMethodFormatter.php index ec56b3e3dae02..eaefbe4c5f104 100644 --- a/app/code/Magento/InstantPurchase/Model/Ui/ShippingMethodFormatter.php +++ b/app/code/Magento/InstantPurchase/Model/Ui/ShippingMethodFormatter.php @@ -11,12 +11,14 @@ * Ship[ping method string presentation. * * @api May be used for pluginization. + * @since 100.2.0 */ class ShippingMethodFormatter { /** * @param ShippingMethodInterface $shippingMethod * @return string + * @since 100.2.0 */ public function format(ShippingMethodInterface $shippingMethod) : string { diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/AvailabilityCheckerInterface.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/AvailabilityCheckerInterface.php index 825c6830b3aa6..7fe336b7a6a66 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/AvailabilityCheckerInterface.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/AvailabilityCheckerInterface.php @@ -12,6 +12,7 @@ * instant_purchase/available configuration option in vault payment config. * * @api + * @since 100.2.0 */ interface AvailabilityCheckerInterface { @@ -19,6 +20,7 @@ interface AvailabilityCheckerInterface * Checks if payment method may be used for instant purchase. * * @return bool + * @since 100.2.0 */ public function isAvailable(): bool; } diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentAdditionalInformationProviderInterface.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentAdditionalInformationProviderInterface.php index 6f1e291a9a83b..9119b54329486 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentAdditionalInformationProviderInterface.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentAdditionalInformationProviderInterface.php @@ -14,6 +14,7 @@ * instant_purchase/additionalInformation configuration option in vault payment config. * * @api + * @since 100.2.0 */ interface PaymentAdditionalInformationProviderInterface { @@ -22,6 +23,7 @@ interface PaymentAdditionalInformationProviderInterface * * @param PaymentTokenInterface $paymentToken * @return array + * @since 100.2.0 */ public function getAdditionalInformation(PaymentTokenInterface $paymentToken): array; } diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentTokenFormatterInterface.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentTokenFormatterInterface.php index 1ee583c45cf40..c6d31a86b622d 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentTokenFormatterInterface.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentTokenFormatterInterface.php @@ -15,6 +15,7 @@ * instant_purchase/tokenFormat configuration option in vault payment config. * * @api + * @since 100.2.0 */ interface PaymentTokenFormatterInterface { @@ -23,6 +24,7 @@ interface PaymentTokenFormatterInterface * * @param PaymentTokenInterface $paymentToken * @return string + * @since 100.2.0 */ public function formatPaymentToken(PaymentTokenInterface $paymentToken): string; } diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAdditionalInformationProvider.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAdditionalInformationProvider.php index 56079823e9e91..0e788079794d6 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAdditionalInformationProvider.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAdditionalInformationProvider.php @@ -11,6 +11,7 @@ * Payment additional information provider that returns predefined value. * * @api + * @since 100.2.0 */ class StaticAdditionalInformationProvider implements PaymentAdditionalInformationProviderInterface { @@ -30,6 +31,7 @@ public function __construct(array $value = []) /** * @inheritdoc + * @since 100.2.0 */ public function getAdditionalInformation(PaymentTokenInterface $paymentToken): array { diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAvailabilityChecker.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAvailabilityChecker.php index 02e24b78f741c..78f421db6aaa8 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAvailabilityChecker.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAvailabilityChecker.php @@ -9,6 +9,7 @@ * Availability checker with predefined result. * * @api + * @since 100.2.0 */ class StaticAvailabilityChecker implements AvailabilityCheckerInterface { @@ -28,6 +29,7 @@ public function __construct(bool $value = true) /** * @inheritdoc + * @since 100.2.0 */ public function isAvailable(): bool { diff --git a/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button.php b/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button.php index 1345a04b357ab..b34858d098494 100644 --- a/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button.php +++ b/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button.php @@ -6,8 +6,12 @@ namespace Magento\Integration\Block\Adminhtml\Widget\Grid\Column\Renderer; use Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use Magento\Integration\Model\Integration; +use Magento\Backend\Block\Context; /** * Render HTML <button> tag. @@ -16,19 +20,75 @@ class Button extends AbstractRenderer { /** - * {@inheritdoc} + * @var SecureHtmlRenderer */ - public function render(\Magento\Framework\DataObject $row) + private $secureRenderer; + + /** + * @var Random + */ + private $random; + + /** + * Button constructor. + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random + */ + public function __construct( + Context $context, + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { + parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + } + + /** + * @inheritDoc + */ + public function render(DataObject $row) { - /** @var array $attributes */ - $attributes = $this->_prepareAttributes($row); - return sprintf('<button %s>%s</button>', $this->_getAttributesStr($attributes), $this->_getValue($row)); + $attributes = $this->extractAttributes($row); + $attributes['button-renderer-hook-id'] = 'hook' .$this->random->getRandomString(10); + + return sprintf('<button %s>%s</button>', $this->renderAttributes($attributes), $this->_getValue($row)) + .$this->renderSpecialAttributes($attributes); + } + + /** + * Extract attributes to render. + * + * @param DataObject $row + * @return string[] + */ + private function extractAttributes(DataObject $row): array + { + $attributes = []; + foreach ($this->_getValidAttributes() as $attributeName) { + $methodName = sprintf('_get%sAttribute', ucfirst($attributeName)); + $rowMethodName = sprintf('get%s', ucfirst($attributeName)); + $attributeValue = method_exists( + $this, + $methodName + ) ? $this->{$methodName}( + $row + ) : $this->getColumn()->{$rowMethodName}(); + if ($attributeValue) { + $attributes[$attributeName] = $attributeValue; + } + } + + return $attributes; } /** * Determine whether current integration came from config file * - * @param \Magento\Framework\DataObject $row + * @param DataObject $row * @return bool */ protected function _isConfigBasedIntegration(DataObject $row) @@ -43,7 +103,7 @@ protected function _isConfigBasedIntegration(DataObject $row) /** * Whether current item is disabled. * - * @param \Magento\Framework\DataObject $row + * @param DataObject $row * @return bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -53,7 +113,9 @@ protected function _isDisabled(DataObject $row) } /** - * @param \Magento\Framework\DataObject $row + * Retrieve "disabled" attribute value for the row. + * + * @param DataObject $row * @return string */ protected function _getDisabledAttribute(DataObject $row) @@ -68,33 +130,47 @@ protected function _getDisabledAttribute(DataObject $row) * - Then it tries to get it from the button's column layout description. * If received attribute value is empty - attribute is not added to final HTML. * - * @param \Magento\Framework\DataObject $row + * @param DataObject $row * @return array */ protected function _prepareAttributes(DataObject $row) { - $attributes = []; - foreach ($this->_getValidAttributes() as $attributeName) { - $methodName = sprintf('_get%sAttribute', ucfirst($attributeName)); - $rowMethodName = sprintf('get%s', ucfirst($attributeName)); - $attributeValue = method_exists( - $this, - $methodName - ) ? $this->{$methodName}( - $row - ) : $this->getColumn()->{$rowMethodName}(); - - if ($attributeValue) { - $attributes[] = sprintf( - '%s="%s"', - $attributeName, - $this->escapeHtmlAttr($attributeValue, false) - ); + $attributes = $this->extractAttributes($row); + foreach ($attributes as $attributeName => $attributeValue) { + if ($attributeName === 'style' || mb_strpos($attributeName, 'on') === 0) { + //Will render event handlers and style as separate tags + continue; } + $attributes[] = sprintf( + '%s="%s"', + $attributeName, + $this->escapeHtmlAttr($attributeValue, false) + ); } + return $attributes; } + /** + * Render HTML attributes. + * + * @param array $attributes + * @return string + */ + private function renderAttributes(array $attributes): string + { + $html = ''; + foreach ($attributes as $attributeName => $attributeValue) { + if ($attributeName === 'style' || mb_strpos($attributeName, 'on') === 0) { + //Will render event handlers and style as separate tags + continue; + } + $html .= ($html ? ' ' : '') ."{$attributeName}=\"{$this->escapeHtmlAttr($attributeValue)}\""; + } + + return $html; + } + /** * Get list of available HTML attributes for this element. * @@ -140,4 +216,36 @@ protected function _getAttributesStr($attributes) { return join(' ', $attributes); } + + /** + * Render special attributes as separate tags. + * + * @param string[] $attributes + * @return string + */ + private function renderSpecialAttributes(array $attributes): string + { + if (!$hookId = $attributes['button-renderer-hook-id']) { + return ''; + } + + $html = ''; + if (!empty($attributes['style'])) { + $html .= $this->secureRenderer->renderStyleAsTag( + $attributes['style'], + "[button-renderer-hook-id='$hookId']" + ); + } + foreach ($this->_getValidAttributes() as $attr) { + if (!empty($attributes[$attr]) && mb_strpos($attr, 'on') === 0) { + $html .= $this->secureRenderer->renderEventListenerAsTag( + $attr, + $attributes[$attr], + "*[button-renderer-hook-id='$hookId']" + ); + } + } + + return $html; + } } 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 8bcbb45653494..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->_getSession()->setIntegrationData($integrationData); + $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/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertAdminIntegrationNameInFormActionGroup.xml similarity index 61% rename from app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml rename to app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertAdminIntegrationNameInFormActionGroup.xml index 0f87ee90b7ce0..70903d524a4c1 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertAdminIntegrationNameInFormActionGroup.xml @@ -7,10 +7,10 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminOpenCmsBlockActionGroup"> + <actionGroup name="AssertAdminIntegrationNameInFormActionGroup"> <arguments> - <argument name="block_id" type="string"/> + <argument name="name" type="string"/> </arguments> - <amOnPage url="{{AdminEditBlockPage.url(block_id)}}" stepKey="openEditCmsBlock"/> + <seeInField userInput="{{name}}" selector="{{AdminNewIntegrationSection.name}}" stepKey="checkEnteredValueIsPreserved"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Integration/Test/Mftf/Page/AdminConfigServicesOauthPage.xml b/app/code/Magento/Integration/Test/Mftf/Page/AdminConfigServicesOauthPage.xml new file mode 100644 index 0000000000000..85f20c3617e1d --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/Page/AdminConfigServicesOauthPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminConfigServicesOauthPage" url="admin/system_config/edit/section/oauth/" area="admin" module="Magento_Integration"> + <section name="AdminConfigAccessTokenExpirationSection"/> + </page> +</pages> diff --git a/app/code/Magento/Integration/Test/Mftf/Section/AdminConfigAccessTokenExpirationSection.xml b/app/code/Magento/Integration/Test/Mftf/Section/AdminConfigAccessTokenExpirationSection.xml new file mode 100644 index 0000000000000..0f18c1e75979e --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/Section/AdminConfigAccessTokenExpirationSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminConfigAccessTokenExpirationSection"> + <element name="tabAccessTokenLifetime" type="select" selector="#oauth_access_token_lifetime-head"/> + <element name="CheckIfTabExpand" type="button" selector="#oauth_access_token_lifetime-head:not(.open)"/> + <element name="valueForTokenLifetime" type="input" selector="#oauth_access_token_lifetime_customer"/> + <element name="systemValueForTokenLifetime" type="checkbox" selector="#oauth_access_token_lifetime_customer_inherit"/> + <element name="valueForTokenLifetimeAdmin" type="input" selector="#oauth_access_token_lifetime_admin"/> + <element name="systemValueForTokenLifetimeAdmin" type="checkbox" selector="#oauth_access_token_lifetime_admin_inherit"/> + </section> +</sections> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminConfigSaveEmptySettingsTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminConfigSaveEmptySettingsTest.xml new file mode 100644 index 0000000000000..89a0fb4c1f026 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminConfigSaveEmptySettingsTest.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="AdminConfigSaveEmptySettingsTest"> + <annotations> + <features value="Configuration"/> + <stories value="Save settings 'Access Token Expiration'."/> + <title value="Save settings 'Access Token Expiration' with empty values."/> + <description value="Save settings 'Customer Token Lifetime' and 'Admin Token Lifetime' with empty values without validations."/> + <severity value="AVERAGE"/> + <testCaseId value="MC-37382"/> + <group value="configuration"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <amOnPage url="{{AdminConfigServicesOauthPage.url}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{AdminConfigAccessTokenExpirationSection.tabAccessTokenLifetime}}" dependentSelector="{{AdminConfigAccessTokenExpirationSection.CheckIfTabExpand}}" visible="true" stepKey="expandTab"/> + <waitForAjaxLoad stepKey="waitForAjax"/> + <uncheckOption selector="{{AdminConfigAccessTokenExpirationSection.systemValueForTokenLifetime}}" stepKey="uncheckUseSystemValue"/> + <fillField selector="{{AdminConfigAccessTokenExpirationSection.valueForTokenLifetime}}" userInput="" stepKey="valueForTokenLifetime"/> + <uncheckOption selector="{{AdminConfigAccessTokenExpirationSection.systemValueForTokenLifetimeAdmin}}" stepKey="uncheckUseSystemValueAdmin"/> + <fillField selector="{{AdminConfigAccessTokenExpirationSection.valueForTokenLifetimeAdmin}}" userInput="" stepKey="valueForTokenLifetimeAdmin"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + </test> +</tests> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml index 7bc1c9b5a274f..92133d617f626 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml @@ -54,5 +54,8 @@ <argument name="message" value="The integration with name "{{defaultIntegrationData.name}}" exists."/> <argument value="error" name="messageType"/> </actionGroup> + <actionGroup ref="AssertAdminIntegrationNameInFormActionGroup" stepKey="checkEnteredValueIsPreserved"> + <argument name="name" value="{{defaultIntegrationData.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Integration/Test/Unit/Block/Adminhtml/Widget/Grid/Column/Renderer/ButtonTest.php b/app/code/Magento/Integration/Test/Unit/Block/Adminhtml/Widget/Grid/Column/Renderer/ButtonTest.php index ce8a785b902d7..3813682eed004 100644 --- a/app/code/Magento/Integration/Test/Unit/Block/Adminhtml/Widget/Grid/Column/Renderer/ButtonTest.php +++ b/app/code/Magento/Integration/Test/Unit/Block/Adminhtml/Widget/Grid/Column/Renderer/ButtonTest.php @@ -15,6 +15,8 @@ use Magento\Integration\Block\Adminhtml\Widget\Grid\Column\Renderer\Button; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class ButtonTest extends TestCase { @@ -42,13 +44,37 @@ protected function setUp(): void { $this->escaperMock = $this->createMock(Escaper::class); $this->escaperMock->expects($this->any())->method('escapeHtml')->willReturnArgument(0); + $this->escaperMock->expects($this->any())->method('escapeHtmlAttr')->willReturnArgument(0); $this->contextMock = $this->createPartialMock(Context::class, ['getEscaper']); $this->contextMock->expects($this->any())->method('getEscaper')->willReturn($this->escaperMock); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('random'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); $this->objectManagerHelper = new ObjectManager($this); $this->buttonRenderer = $this->objectManagerHelper->getObject( Button::class, - ['context' => $this->contextMock] + ['context' => $this->contextMock, 'random' => $randomMock, 'secureRenderer' => $secureRendererMock] ); } @@ -57,10 +83,9 @@ protected function setUp(): void */ public function testRender() { - $expectedResult = '<button id="1" type="bigButton">my button</button>'; $column = $this->getMockBuilder(Column::class) ->disableOriginalConstructor() - ->setMethods(['getType', 'getId', 'getIndex']) + ->setMethods(['getType', 'getId', 'getIndex', 'getStyle', 'getOnclick']) ->getMock(); $column->expects($this->any()) ->method('getType') @@ -68,15 +93,25 @@ public function testRender() $column->expects($this->any()) ->method('getId') ->willReturn('1'); - $this->escaperMock->expects($this->at(0))->method('escapeHtmlAttr')->willReturn('1'); - $this->escaperMock->expects($this->at(1))->method('escapeHtmlAttr')->willReturn('bigButton'); $column->expects($this->any()) ->method('getIndex') ->willReturn('name'); + $column->expects($this->any()) + ->method('getStyle') + ->willReturn('display: block;'); + $column->expects($this->any()) + ->method('getOnclick') + ->willReturn('alert(1);'); $this->buttonRenderer->setColumn($column); $object = new DataObject(['name' => 'my button']); $actualResult = $this->buttonRenderer->render($object); - $this->assertEquals($expectedResult, $actualResult); + $this->assertEquals( + '<button id="1" type="bigButton" button-renderer-hook-id="hookrandom">my button</button>' + .'<style>[button-renderer-hook-id=\'hookrandom\'] { display: block; }</style>' + .'<script>document.querySelector(\'*[button-renderer-hook-id=\'hookrandom\']\').onclick = ' + .'function () { alert(1); };</script>', + $actualResult + ); } } 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/Integration/composer.json b/app/code/Magento/Integration/composer.json index 0b9752c743213..c85e84284b43f 100644 --- a/app/code/Magento/Integration/composer.json +++ b/app/code/Magento/Integration/composer.json @@ -12,7 +12,8 @@ "magento/module-customer": "*", "magento/module-security": "*", "magento/module-store": "*", - "magento/module-user": "*" + "magento/module-user": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Integration/etc/adminhtml/system.xml b/app/code/Magento/Integration/etc/adminhtml/system.xml index 6ef569a1d8a2f..3d465a9642805 100644 --- a/app/code/Magento/Integration/etc/adminhtml/system.xml +++ b/app/code/Magento/Integration/etc/adminhtml/system.xml @@ -16,12 +16,12 @@ <field id="customer" translate="label comment" type="text" sortOrder="30" showInDefault="1" canRestore="1"> <label>Customer Token Lifetime (hours)</label> <comment>We will disable this feature if the value is empty.</comment> - <validate>required-entry validate-zero-or-greater validate-number</validate> + <validate>validate-zero-or-greater validate-number</validate> </field> <field id="admin" translate="label comment" type="text" sortOrder="60" showInDefault="1" canRestore="1"> <label>Admin Token Lifetime (hours)</label> <comment>We will disable this feature if the value is empty.</comment> - <validate>required-entry validate-zero-or-greater validate-number</validate> + <validate>validate-zero-or-greater validate-number</validate> </field> </group> <group id="cleanup" translate="label" type="text" sortOrder="300" showInDefault="1"> diff --git a/app/code/Magento/Integration/view/adminhtml/templates/integration/activate/permissions/tab/webapi.phtml b/app/code/Magento/Integration/view/adminhtml/templates/integration/activate/permissions/tab/webapi.phtml index 1730509a65910..6dd7d1b4a2421 100644 --- a/app/code/Magento/Integration/view/adminhtml/templates/integration/activate/permissions/tab/webapi.phtml +++ b/app/code/Magento/Integration/view/adminhtml/templates/integration/activate/permissions/tab/webapi.phtml @@ -7,11 +7,13 @@ * * @var \Magento\Integration\Block\Adminhtml\Integration\Activate\Permissions\Tab\Webapi $block */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <fieldset class="admin__fieldset form-inline entry-edit"> - <?php if ($block->isTreeEmpty()) : ?> + <?php if ($block->isTreeEmpty()): ?> <p class="empty"><?= $block->escapeHtml(__('No permissions requested')) ?></p> - <?php else : ?> + <?php else: ?> <div class="field" data-role="tree-resources-container"> <div class="control"> <div id="resource-tree" class="tree x-tree" data-role="resource-tree"></div> @@ -19,8 +21,11 @@ </div> <?php endif ?> </fieldset> -<?php if (!$block->isTreeEmpty()) : ?> - <script> +<?php +if (!$block->isTreeEmpty()): + $treeJson = /* @noEscape */ $block->getResourcesTreeJson(); + $selectedJson = /* @noEscape */ $block->getSelectedResourcesJson(); + $scriptString = <<<script require(["jquery", "Magento_User/js/roles-tree"], function($){ $.widget('mage.rolesTree', $.mage.rolesTree, { _checkNode: function(event) {}, @@ -32,9 +37,11 @@ }); $('[data-role="resource-tree"]').rolesTree({ - 'treeInitData': <?= /* @noEscape */ $block->getResourcesTreeJson() ?>, - 'treeInitSelectedData': <?= /* @noEscape */ $block->getSelectedResourcesJson() ?> + 'treeInitData': {$treeJson}, + 'treeInitSelectedData': {$selectedJson} }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif ?> diff --git a/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml b/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml index ef0a667d2de47..b56ad208071d8 100644 --- a/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml +++ b/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml @@ -7,8 +7,12 @@ * * @var \Magento\Backend\Block\Template $block */ + +/** @var \Magento\Backend\Block\Template $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require([ "jquery", 'Magento_Ui/js/modal/confirm', @@ -18,34 +22,34 @@ ], function ($, Confirm) { window.integration = new Integration( - '<?= $block->escapeUrl( + '{$block->escapeJs( $block->getUrl( '*/*/permissionsDialog', ['id' => ':id', 'reauthorize' => ':isReauthorize', '_escape_params' => false] ) - ) ?>', - '<?= $block->escapeUrl( + )}', + '{$block->escapeJs( $block->getUrl( '*/*/tokensDialog', ['id' => ':id', 'reauthorize' => ':isReauthorize', '_escape_params' => false] ) - ) ?>', - '<?= $block->escapeUrl( + )}', + '{$block->escapeJs( $block->getUrl( '*/*/tokensExchange', ['id' => ':id', 'reauthorize' => ':isReauthorize', '_escape_params' => false] ) - ) ?>', - '<?= $block->escapeUrl( + )}', + '{$block->escapeJs( $block->getUrl( '*/*' ) - ) ?>', - '<?= $block->escapeUrl( + )}', + '{$block->escapeJs( $block->getUrl( '*/*/loginSuccessCallback' ) - ) ?>' + )}' ); /** @@ -55,8 +59,9 @@ $('div#integrationGrid').on('click', 'button#delete', function (e) { new Confirm({ - title: '<?= $block->escapeHtml(__('Are you sure?')) ?>', - content: "<?= $block->escapeHtml(__("Are you sure you want to delete this integration? You can't undo this action.")) ?>", + title: '{$block->escapeJs(__('Are you sure?'))}', + content: "{$block->escapeJs(__("Are you sure you want to delete this integration? " . + "You can't undo this action."))}", actions: { confirm: function () { $.mage.dataPost().postData({action: $(e.target).data('url'), data: {}}); @@ -67,6 +72,9 @@ }); }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> -<div id="integration-popup-container" style="display: none;"></div> +<div id="integration-popup-container"></div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", '#integration-popup-container') ?> diff --git a/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml b/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml index 1737f66ce4a1b..25caf5060cb5f 100644 --- a/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml +++ b/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Integration\Block\Adminhtml\Integration\Edit\Tab\Webapi */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= $block->getChildHtml() ?> @@ -18,8 +19,7 @@ <label class="label" for="all_resources"><span><?= $block->escapeHtml(__('Resource Access')) ?></span></label> <div class="control"> - <select id="all_resources" name="all_resources" - onchange="jQuery('[data-role=tree-resources-container]').toggle()" class="select"> + <select id="all_resources" name="all_resources" class="select"> <option value="0" <?= ($block->isEverythingAllowed() ? '' : 'selected="selected"') ?>> <?= $block->escapeHtml(__('Custom')) ?> </option> @@ -27,11 +27,16 @@ <?= $block->escapeHtml(__('All')) ?> </option> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + "jQuery('[data-role=tree-resources-container]').toggle()", + 'select#all_resources' + ) ?> </div> </div> <div class="field - <?php if ($block->isEverythingAllowed()) :?> + <?php if ($block->isEverythingAllowed()):?> no-display <?php endif ?>" data-role="tree-resources-container"> diff --git a/app/code/Magento/LayeredNavigation/Block/Navigation.php b/app/code/Magento/LayeredNavigation/Block/Navigation.php index e394fe7f6cf5b..85d3dd2a2a01d 100644 --- a/app/code/Magento/LayeredNavigation/Block/Navigation.php +++ b/app/code/Magento/LayeredNavigation/Block/Navigation.php @@ -76,6 +76,7 @@ protected function _prepareLayout() /** * @inheritdoc + * @since 100.3.4 */ protected function _beforeToHtml() { diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..85eb8830dffca --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DisplayProductCountDefaultValue"> + <data key="path">catalog/layered_navigation/display_product_count</data> + <data key="value">1</data> + </entity> + <entity name="PriceNavigationStepCalculationDefaultValue"> + <data key="path">catalog/layered_navigation/price_range_calculation</data> + <data key="value">auto</data> + </entity> +</entities> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml index cdd692763d399..49f2294b978ee 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml @@ -50,8 +50,12 @@ </actionGroup> <selectOption selector="{{AdminProductFormSection.customSelectField($$attribute.attribute[attribute_code]$$)}}" userInput="option1" stepKey="selectAttribute"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> - <magentoCLI command="indexer:reindex" stepKey="reindexAll"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Check storefront mobile view for shop by button is functioning as expected --> <comment userInput="Check storefront mobile view for shop by button is functioning as expected" stepKey="commentCheckShopByButton" /> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest.xml new file mode 100644 index 0000000000000..cb7e683605a68 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest.xml @@ -0,0 +1,397 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest"> + <annotations> + <features value="LayeredNavigation"/> + <stories value="Product attributes in Layered Navigation"/> + <title value="Limitation of displayed attribute options number in layered navigation with ElasticSearch"/> + <description value="All attribute options are shown in Layered navigation"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-28963"/> + <group value="layeredNavigation"/> + <group value="catalog"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + + <before> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <magentoCLI command="config:set {{DisplayProductCountDefaultValue.path}} {{DisplayProductCountDefaultValue.value}}" stepKey="enableDisplayProductCount"/> + <magentoCLI command="config:set {{PriceNavigationStepCalculationDefaultValue.path}} {{PriceNavigationStepCalculationDefaultValue.value}}" stepKey="setPriceNavigationStepCalculationDefaultValue"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Create an attribute --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <!--Create 15 attribute options--> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption4"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption5"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption6"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption7"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption8"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption9"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption10"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption11"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption12"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption13"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption14"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption15"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!--Get Created options data--> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="4" stepKey="getConfigAttributeOption4"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="5" stepKey="getConfigAttributeOption5"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="6" stepKey="getConfigAttributeOption6"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="7" stepKey="getConfigAttributeOption7"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="8" stepKey="getConfigAttributeOption8"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="9" stepKey="getConfigAttributeOption9"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="10" stepKey="getConfigAttributeOption10"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="11" stepKey="getConfigAttributeOption11"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="12" stepKey="getConfigAttributeOption12"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="13" stepKey="getConfigAttributeOption13"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="14" stepKey="getConfigAttributeOption14"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="15" stepKey="getConfigAttributeOption15"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Add attribute to attribute set--> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create simple products and set them created attribute value --> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct4"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption4"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct5"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption5"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct6"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption6"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct7"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption7"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct8"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption8"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct9"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption9"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct10"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption10"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct11"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption11"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct12"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption12"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct13"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption13"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct14"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption14"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct15"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption15"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create the configurable product --> + <createData entity="ConfigurableProduct15Options" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + <requiredEntity createDataKey="getConfigAttributeOption4"/> + <requiredEntity createDataKey="getConfigAttributeOption5"/> + <requiredEntity createDataKey="getConfigAttributeOption6"/> + <requiredEntity createDataKey="getConfigAttributeOption7"/> + <requiredEntity createDataKey="getConfigAttributeOption8"/> + <requiredEntity createDataKey="getConfigAttributeOption9"/> + <requiredEntity createDataKey="getConfigAttributeOption10"/> + <requiredEntity createDataKey="getConfigAttributeOption11"/> + <requiredEntity createDataKey="getConfigAttributeOption12"/> + <requiredEntity createDataKey="getConfigAttributeOption13"/> + <requiredEntity createDataKey="getConfigAttributeOption14"/> + <requiredEntity createDataKey="getConfigAttributeOption15"/> + </createData> + + <!-- Add simple products to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild4"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct4"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild5"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct5"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild6"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct6"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild7"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct7"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild8"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct8"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild9"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct9"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild10"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct10"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild11"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct11"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild12"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct12"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild13"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct13"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild14"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct14"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild15"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct15"/> + </createData> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigChildProduct4" stepKey="deleteConfigChildProduct4"/> + <deleteData createDataKey="createConfigChildProduct5" stepKey="deleteConfigChildProduct5"/> + <deleteData createDataKey="createConfigChildProduct6" stepKey="deleteConfigChildProduct6"/> + <deleteData createDataKey="createConfigChildProduct7" stepKey="deleteConfigChildProduct7"/> + <deleteData createDataKey="createConfigChildProduct8" stepKey="deleteConfigChildProduct8"/> + <deleteData createDataKey="createConfigChildProduct9" stepKey="deleteConfigChildProduct9"/> + <deleteData createDataKey="createConfigChildProduct10" stepKey="deleteConfigChildProduct10"/> + <deleteData createDataKey="createConfigChildProduct11" stepKey="deleteConfigChildProduct11"/> + <deleteData createDataKey="createConfigChildProduct12" stepKey="deleteConfigChildProduct12"/> + <deleteData createDataKey="createConfigChildProduct13" stepKey="deleteConfigChildProduct13"/> + <deleteData createDataKey="createConfigChildProduct14" stepKey="deleteConfigChildProduct14"/> + <deleteData createDataKey="createConfigChildProduct15" stepKey="deleteConfigChildProduct15"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategory"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + <!--Check filtration options for created attribute. All attribute options should be displayed --> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption1PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption1.label$"/> + <argument name="attributeOptionPosition" value="1"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption2PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption2.label$"/> + <argument name="attributeOptionPosition" value="2"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption3PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption3.label$"/> + <argument name="attributeOptionPosition" value="3"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption4PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption4.label$"/> + <argument name="attributeOptionPosition" value="4"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption5PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption5.label$"/> + <argument name="attributeOptionPosition" value="5"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption6PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption6.label$"/> + <argument name="attributeOptionPosition" value="6"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption7PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption7.label$"/> + <argument name="attributeOptionPosition" value="7"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption8PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption8.label$"/> + <argument name="attributeOptionPosition" value="8"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption9PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption9.label$"/> + <argument name="attributeOptionPosition" value="9"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption10PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption10.label$"/> + <argument name="attributeOptionPosition" value="10"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption11PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption11.label$"/> + <argument name="attributeOptionPosition" value="11"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption12PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption12.label$"/> + <argument name="attributeOptionPosition" value="12"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption13PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption13.label$"/> + <argument name="attributeOptionPosition" value="13"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption14PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption14.label$"/> + <argument name="attributeOptionPosition" value="14"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption15PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption15.label$"/> + <argument name="attributeOptionPosition" value="15"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest.xml new file mode 100644 index 0000000000000..be56caa6d3246 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest.xml @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest"> + <annotations> + <features value="LayeredNavigation"/> + <stories value="Product attributes in Layered Navigation"/> + <title value="Create and add new dropdown product attribute to existing set, assign it to existing product with that set and see it on layered navigation"/> + <description value="Verify that new dropdown attribute in existing attribute set shows on layered navigation on storefront without reindex"/> + <severity value="MAJOR"/> + <testCaseId value="MC-35954"/> + <group value="layeredNavigation"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + + <before> + <!--Create category, attribute set with multiselect product attribute with two options--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <createData entity="multipleSelectProductAttribute" stepKey="createMultiselectAttribute"/> + <createData entity="ProductAttributeOption10" stepKey="firstMultiselectOption"> + <requiredEntity createDataKey="createMultiselectAttribute"/> + </createData> + <createData entity="ProductAttributeOption11" stepKey="secondMultiselectOption"> + <requiredEntity createDataKey="createMultiselectAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstMultiselectOption"> + <requiredEntity createDataKey="createMultiselectAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getSecondMultiselectOption"> + <requiredEntity createDataKey="createMultiselectAttribute"/> + </getData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$createAttributeSet.attribute_set_id$/" stepKey="onAttributeSetEdit"/> + <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignMultiselectAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$createMultiselectAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> + <!--Create configurable product with created attribute set and multiselect attribute--> + <createData entity="SimpleOne" storeCode="all" stepKey="createFirstSimpleProduct"> + <field key="attribute_set_id">$createAttributeSet.attribute_set_id$</field> + <requiredEntity createDataKey="createMultiselectAttribute"/> + <requiredEntity createDataKey="getFirstMultiselectOption"/> + </createData> + <createData entity="SimpleOne" storeCode="all" stepKey="createSecondSimpleProduct"> + <field key="attribute_set_id">$createAttributeSet.attribute_set_id$</field> + <requiredEntity createDataKey="createMultiselectAttribute"/> + <requiredEntity createDataKey="getSecondMultiselectOption"/> + </createData> + <createData entity="BaseConfigurableProduct" stepKey="createConfigurableProduct"> + <field key="attribute_set_id">$createAttributeSet.attribute_set_id$</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ConfigurableProductOneOption" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigurableProduct"/> + <requiredEntity createDataKey="createMultiselectAttribute"/> + <requiredEntity createDataKey="getFirstMultiselectOption"/> + </createData> + <createData entity="ConfigurableProductOneOption" stepKey="createConfigProductOption2"> + <requiredEntity createDataKey="createConfigurableProduct"/> + <requiredEntity createDataKey="createMultiselectAttribute"/> + <requiredEntity createDataKey="getSecondMultiselectOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild"> + <requiredEntity createDataKey="createConfigurableProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigurableProduct"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </createData> + <!--Create new dropdown attribute with two options and set Use in layered navigation "Filterable (no results)"--> + <createData entity="dropdownProductAttribute" stepKey="createDropdownAttribute"/> + <createData entity="ProductAttributeOption10" stepKey="firstDropdownOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + <createData entity="ProductAttributeOption11" stepKey="secondDropdownOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstDropdownOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getSecondDropdownOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </getData> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="goToDropdownAttributePage"> + <argument name="productAttributeCode" value="$createDropdownAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup" stepKey="setDropdownUseInLayeredNavigationNoResults"> + <argument name="useInLayeredNavigationValue" value="Filterable (no results)"/> + </actionGroup> + <actionGroup ref="AdminProductAttributeSaveActionGroup" stepKey="saveDropdownAttribute"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </before> + + <after> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createConfigurableProduct" stepKey="deleteConfigurableProduct"/> + <deleteData createDataKey="createMultiselectAttribute" stepKey="deleteMultiselectAttribute"/> + <deleteData createDataKey="createDropdownAttribute" stepKey="deleteDropdownAttribute"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Set attribute option Use in layered navigation to "Filterable(no results)"--> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="goToMultiselectAttributePage"> + <argument name="productAttributeCode" value="$createMultiselectAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup" stepKey="setMultiselectUseInLayeredNavigationNoResults"> + <argument name="useInLayeredNavigationValue" value="Filterable (no results)"/> + </actionGroup> + <actionGroup ref="AdminProductAttributeSaveActionGroup" stepKey="saveMultiselectAttribute"/> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionsTitle($createMultiselectAttribute.default_frontend_label$)}}" stepKey="waitForMultiselectAttributeVisible"/> + <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle($createMultiselectAttribute.default_frontend_label$)}}" dependentSelector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" visible="false" stepKey="clickToExpandAttribute"/> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" stepKey="waitForMultiselectAttributeOptionsVisible"/> + <seeElement selector="{{StorefrontCategorySidebarSection.enabledFilterOptionItemByLabel($getFirstMultiselectOption.label$)}}" stepKey="assertMultiselectAttributeFirstOptionInLayeredNavigation"/> + <seeElement selector="{{StorefrontCategorySidebarSection.enabledFilterOptionItemByLabel($getSecondMultiselectOption.label$)}}" stepKey="assertMultiselectAttributeSecondOptionInLayeredNavigation"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$createAttributeSet.attribute_set_id$/" stepKey="onAttributeSetEditPage"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageLoad"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignDropdownAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$createDropdownAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSetWithDropdownAttribute"/> + <!--Assign dropdown attribute to child product of configurable--> + <amOnPage url="{{AdminProductEditPage.url($createFirstSimpleProduct.id$)}}" stepKey="visitAdminEditProductPage"/> + <waitForElementVisible selector="{{AdminProductFormSection.customSelectField('$createDropdownAttribute.attribute_code$')}}" stepKey="waitForDropdownAttributeSelectVisible"/> + <selectOption selector="{{AdminProductFormSection.customSelectField('$createDropdownAttribute.attribute_code$')}}" userInput="$getFirstDropdownOption.label$" stepKey="selectValueOfNewAttribute"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> + <!--Assert that dropdown attribute is present on layered navigation with both options--> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoadWithDropdownAttribute"/> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionsTitle($createDropdownAttribute.default_frontend_label$)}}" stepKey="waitForDropdownAttributeVisible"/> + <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle($createDropdownAttribute.default_frontend_label$)}}" dependentSelector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" visible="false" stepKey="clickToExpandDropdownAttribute"/> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" stepKey="waitForDropdownAttributeOptionsVisible"/> + <seeElement selector="{{StorefrontCategorySidebarSection.enabledFilterOptionItemByLabel($getFirstDropdownOption.label$)}}" stepKey="assertDropdownAttributeFirstOptionInLayeredNavigation"/> + <seeElement selector="{{StorefrontCategorySidebarSection.disabledFilterOptionItemByLabel($getSecondDropdownOption.label$)}}" stepKey="assertDropdownAttributeSecondOptionInLayeredNavigation"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php b/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php index a728f4c3a4393..808b01bac58aa 100644 --- a/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php +++ b/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php @@ -8,9 +8,11 @@ namespace Magento\LoginAsCustomer\Model; use Magento\Customer\Model\Session; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface; use Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface; +use Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerAdminIdInterface; /** * @inheritdoc @@ -29,16 +31,25 @@ class AuthenticateCustomerBySecret implements AuthenticateCustomerBySecretInterf */ private $customerSession; + /** + * @var SetLoggedAsCustomerAdminIdInterface + */ + private $setLoggedAsCustomerAdminId; + /** * @param GetAuthenticationDataBySecretInterface $getAuthenticationDataBySecret * @param Session $customerSession + * @param SetLoggedAsCustomerAdminIdInterface $setLoggedAsCustomerAdminId */ public function __construct( GetAuthenticationDataBySecretInterface $getAuthenticationDataBySecret, - Session $customerSession + Session $customerSession, + ?SetLoggedAsCustomerAdminIdInterface $setLoggedAsCustomerAdminId = null ) { $this->getAuthenticationDataBySecret = $getAuthenticationDataBySecret; $this->customerSession = $customerSession; + $this->setLoggedAsCustomerAdminId = $setLoggedAsCustomerAdminId + ?? ObjectManager::getInstance()->get(SetLoggedAsCustomerAdminIdInterface::class); } /** @@ -58,6 +69,6 @@ public function execute(string $secret): void } $this->customerSession->regenerateId(); - $this->customerSession->setLoggedAsCustomerAdmindId($authenticationData->getAdminId()); + $this->setLoggedAsCustomerAdminId->execute($authenticationData->getAdminId()); } } diff --git a/app/code/Magento/LoginAsCustomer/Model/Config.php b/app/code/Magento/LoginAsCustomer/Model/Config.php index 2cfafa6ac09a3..bec9527c65f95 100644 --- a/app/code/Magento/LoginAsCustomer/Model/Config.php +++ b/app/code/Magento/LoginAsCustomer/Model/Config.php @@ -10,16 +10,9 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\LoginAsCustomerApi\Api\ConfigInterface; -/** - * @inheritdoc - */ class Config implements ConfigInterface { - /** - * Extension config path - */ - private const XML_PATH_ENABLED - = 'login_as_customer/general/enabled'; + private const XML_PATH_ENABLED = 'login_as_customer/general/enabled'; private const XML_PATH_STORE_VIEW_MANUAL_CHOICE_ENABLED = 'login_as_customer/general/store_view_manual_choice_enabled'; private const XML_PATH_AUTHENTICATION_EXPIRATION_TIME @@ -33,9 +26,8 @@ class Config implements ConfigInterface /** * @param ScopeConfigInterface $scopeConfig */ - public function __construct( - ScopeConfigInterface $scopeConfig - ) { + public function __construct(ScopeConfigInterface $scopeConfig) + { $this->scopeConfig = $scopeConfig; } @@ -44,7 +36,7 @@ public function __construct( */ public function isEnabled(): bool { - return (bool)$this->scopeConfig->getValue(self::XML_PATH_ENABLED); + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED); } /** @@ -52,7 +44,7 @@ public function isEnabled(): bool */ public function isStoreManualChoiceEnabled(): bool { - return (bool)$this->scopeConfig->getValue(self::XML_PATH_STORE_VIEW_MANUAL_CHOICE_ENABLED); + return $this->scopeConfig->isSetFlag(self::XML_PATH_STORE_VIEW_MANUAL_CHOICE_ENABLED); } /** diff --git a/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerAdminId.php b/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerAdminId.php new file mode 100644 index 0000000000000..17af8a3b5c11f --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerAdminId.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\Customer\Model\Session; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; + +/** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class GetLoggedAsCustomerAdminId implements GetLoggedAsCustomerAdminIdInterface +{ + /** + * @var Session + */ + private $session; + + /** + * @param Session $session + */ + public function __construct(Session $session) + { + $this->session = $session; + } + + /** + * @inheritdoc + */ + public function execute(): int + { + return (int)$this->session->getLoggedAsCustomerAdmindId(); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerCustomerId.php b/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerCustomerId.php new file mode 100644 index 0000000000000..9783b04a5a03f --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerCustomerId.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\Backend\Model\Auth\Session; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerCustomerIdInterface; + +/** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class GetLoggedAsCustomerCustomerId implements GetLoggedAsCustomerCustomerIdInterface +{ + /** + * @var Session + */ + private $session; + + /** + * @param Session $session + */ + public function __construct(Session $session) + { + $this->session = $session; + } + + /** + * @inheritdoc + */ + public function execute(): int + { + return (int)$this->session->getLoggedAsCustomerCustomerId(); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerResult.php b/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerResult.php new file mode 100644 index 0000000000000..0d2af8669777c --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerResult.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface; + +/** + * @inheritdoc + */ +class IsLoginAsCustomerEnabledForCustomerResult implements IsLoginAsCustomerEnabledForCustomerResultInterface +{ + /** + * @var string[] + */ + private $messages; + + /** + * @param array $messages + */ + public function __construct(array $messages = []) + { + $this->messages = $messages; + } + + /** + * @inheritdoc + */ + public function isEnabled(): bool + { + return empty($this->messages); + } + + /** + * @inheritdoc + */ + public function getMessages(): array + { + return $this->messages; + } + + /** + * @inheritdoc + */ + public function setMessages(array $messages): void + { + $this->messages = $messages; + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/Resolver/IsLoginAsCustomerEnabledResolver.php b/app/code/Magento/LoginAsCustomer/Model/Resolver/IsLoginAsCustomerEnabledResolver.php new file mode 100644 index 0000000000000..89cb960e78bb8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/Resolver/IsLoginAsCustomerEnabledResolver.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model\Resolver; + +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface; + +/** + * @inheritdoc + */ +class IsLoginAsCustomerEnabledResolver implements IsLoginAsCustomerEnabledForCustomerInterface +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory + */ + private $resultFactory; + + /** + * @param ConfigInterface $config + * @param IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory + */ + public function __construct( + ConfigInterface $config, + IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory + ) { + $this->config = $config; + $this->resultFactory = $resultFactory; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): IsLoginAsCustomerEnabledForCustomerResultInterface + { + $messages = []; + if (!$this->config->isEnabled()) { + $messages[] = __('Login as Customer is disabled.'); + } + + return $this->resultFactory->create(['messages' => $messages]); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerAdminId.php b/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerAdminId.php new file mode 100644 index 0000000000000..aa16dbcd4f808 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerAdminId.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\Customer\Model\Session; +use Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerAdminIdInterface; + +/** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class SetLoggedAsCustomerAdminId implements SetLoggedAsCustomerAdminIdInterface +{ + /** + * @var Session + */ + private $session; + + /** + * @param Session $session + */ + public function __construct(Session $session) + { + $this->session = $session; + } + + /** + * @inheritdoc + */ + public function execute(int $adminId): void + { + $this->session->setLoggedAsCustomerAdmindId($adminId); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerCustomerId.php b/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerCustomerId.php new file mode 100644 index 0000000000000..95e159bdeded3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerCustomerId.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\Backend\Model\Auth\Session; +use Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerCustomerIdInterface; + +/** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class SetLoggedAsCustomerCustomerId implements SetLoggedAsCustomerCustomerIdInterface +{ + /** + * @var Session + */ + private $session; + + /** + * @param Session $session + */ + public function __construct(Session $session) + { + $this->session = $session; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): void + { + $this->session->setLoggedAsCustomerCustomerId($customerId); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Plugin/AdminLogoutPlugin.php b/app/code/Magento/LoginAsCustomer/Plugin/AdminLogoutPlugin.php index 3b8d26129a91e..9b8567663578d 100644 --- a/app/code/Magento/LoginAsCustomer/Plugin/AdminLogoutPlugin.php +++ b/app/code/Magento/LoginAsCustomer/Plugin/AdminLogoutPlugin.php @@ -10,9 +10,12 @@ use Magento\Backend\Model\Auth; use Magento\LoginAsCustomerApi\Api\ConfigInterface; use Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataForUserInterface; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerCustomerIdInterface; /** * Delete all Login as Customer sessions for logging out admin. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class AdminLogoutPlugin { @@ -26,16 +29,24 @@ class AdminLogoutPlugin */ private $deleteAuthenticationDataForUser; + /** + * @var GetLoggedAsCustomerCustomerIdInterface + */ + private $getLoggedAsCustomerCustomerId; + /** * @param ConfigInterface $config * @param DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser + * @param GetLoggedAsCustomerCustomerIdInterface $getLoggedAsCustomerCustomerId */ public function __construct( ConfigInterface $config, - DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser + DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser, + GetLoggedAsCustomerCustomerIdInterface $getLoggedAsCustomerCustomerId ) { $this->config = $config; $this->deleteAuthenticationDataForUser = $deleteAuthenticationDataForUser; + $this->getLoggedAsCustomerCustomerId = $getLoggedAsCustomerCustomerId; } /** @@ -45,8 +56,10 @@ public function __construct( */ public function beforeLogout(Auth $subject): void { - if ($this->config->isEnabled()) { - $userId = (int)$subject->getUser()->getId(); + $user = $subject->getUser(); + $isLoggedAsCustomer = (bool)$this->getLoggedAsCustomerCustomerId->execute(); + if ($this->config->isEnabled() && $user && $isLoggedAsCustomer) { + $userId = (int)$user->getId(); $this->deleteAuthenticationDataForUser->execute($userId); } } diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertContainsMessageOrderCreatedByAdminActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertContainsMessageOrderCreatedByAdminActionGroup.xml new file mode 100644 index 0000000000000..bcf6fc96aa131 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertContainsMessageOrderCreatedByAdminActionGroup.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="AdminAssertContainsMessageOrderCreatedByAdminActionGroup"> + <annotations> + <description>Assert Admin Order page contains message about Order created by a Store Administrator. + </description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + <argument name="adminUserFullName" type="string"/> + </arguments> + + <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="gotoOrderPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="Order Placed by {{adminUserFullName}} using Login as Customer" + stepKey="seeMessageOrderCreatedByAdmin"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotAvailableDirectlyActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotAvailableDirectlyActionGroup.xml new file mode 100644 index 0000000000000..7e032b168f062 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotAvailableDirectlyActionGroup.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="AdminAssertLoginAsCustomerConfigNotAvailableDirectlyActionGroup"> + <annotations> + <description>Verify Login as Customer config section is not available by direct url.</description> + </annotations> + + <amOnPage url="{{AdminLoginAsCustomerConfigPage.url}}" stepKey="navigateToLoginAsCustomerConfigSection"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeInCurrentUrl url="admin/system_config/index" stepKey="seeRedirectToConfigIndexPage"/> + <dontSee userInput="Login as Customer" stepKey="dontSeeLoginAsCustomer"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotVisibleActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotVisibleActionGroup.xml new file mode 100644 index 0000000000000..875869d9928a4 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotVisibleActionGroup.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="AdminAssertLoginAsCustomerConfigNotVisibleActionGroup"> + <annotations> + <description>Verify no Login as Customer config section available.</description> + </annotations> + + <!-- TODO: update --> + <dontSee userInput="Login as Customer" stepKey="dontSeeLoginAsCustomerItem"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigVisibleActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigVisibleActionGroup.xml new file mode 100644 index 0000000000000..cdc513651ad54 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigVisibleActionGroup.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="AdminAssertLoginAsCustomerConfigVisibleActionGroup"> + <annotations> + <description>Verify Login as Customer config section available.</description> + </annotations> + + <seeElement selector="{{AdminCustomerConfigSection.loginAsCustomerSettingsHead}}" stepKey="seeLoginAsCustomerSettingsHead"/> + <seeElement selector="{{AdminCustomerConfigSection.enableExtensionLabel}}" stepKey="seeEnableExtensionLabel"/> + <seeElement selector="{{AdminCustomerConfigSection.disablePageCacheLabel}}" stepKey="seeDisablePageCacheLabel"/> + <seeElement selector="{{AdminCustomerConfigSection.storeViewToLoginToLabel}}" stepKey="seeStoreViewToLoginToLabel"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerLogRecordActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerLogRecordActionGroup.xml new file mode 100644 index 0000000000000..da47864e28eac --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerLogRecordActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertLoginAsCustomerLogRecordActionGroup"> + <annotations> + <description>Assert Login as Customer Log record is correct.</description> + </annotations> + <arguments> + <argument name="rowNumber" type="string"/> + <argument name="adminId" type="string"/> + <argument name="customerId" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{AdminLoginAsCustomerLogPage.url}}" stepKey="checkUrl"/> + <see selector="{{AdminLoginAsCustomerLogGridSection.adminIdInRow(rowNumber)}}" userInput="{{adminId}}" + stepKey="seeCorrectAdminId"/> + <see selector="{{AdminLoginAsCustomerLogGridSection.customerIdInRow(rowNumber)}}" userInput="{{customerId}}" + stepKey="seeCorrectCustomerId"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerSectionLinkNotAvailableActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerSectionLinkNotAvailableActionGroup.xml new file mode 100644 index 0000000000000..779cb1e5c8899 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerSectionLinkNotAvailableActionGroup.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="AdminAssertLoginAsCustomerSectionLinkNotAvailableActionGroup"> + <annotations> + <description>Verify Login as Customer config section isn't available.</description> + </annotations> + + <conditionalClick selector="{{CaptchaFormsDisplayingSection.customer}}" dependentSelector="{{AdminCustomerConfigSection.loginAsCustomerSettingsSectionLink}}" visible="false" stepKey="expandCustomerGroup"/> + <dontSeeElement selector="{{AdminCustomerConfigSection.loginAsCustomerSettingsSectionLink}}" stepKey="dontSeeLoginAsCustomerSettingsLink"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebSiteAndGroupActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebSiteAndGroupActionGroup.xml new file mode 100644 index 0000000000000..e7f55e69b1cda --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebSiteAndGroupActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateCustomerWithWebSiteAndGroupActionGroup"> + <conditionalClick selector="{{AdminCustomerAccountInformationSection.assistanceAllowed}}" dependentSelector="{{AdminCustomerAccountInformationSection.assistanceAllowed}}" visible="true" stepKey="clickAllowAssistance" after="FillEmail"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml new file mode 100644 index 0000000000000..52f5b190c3cb8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.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="AdminEditUserRoleActionGroup"> + <annotations> + <description>Open User Role resources for edit.</description> + </annotations> + <arguments> + <argument name="roleName" type="string"/> + </arguments> + + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRolesGrid"/> + <fillField selector="{{AdminRoleGridSection.roleNameFilterTextField}}" userInput="{{roleName}}" + stepKey="enterRoleName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearch"/> + <see selector="{{AdminDataGridTableSection.row('1')}}" userInput="{{roleName}}" stepKey="seeUserRole"/> + <click selector="{{AdminDataGridTableSection.row('1')}}" stepKey="openRoleEditPage"/> + <waitForPageLoad stepKey="waitForRoleEditPageLoad"/> + <fillField selector="{{AdminEditRoleInfoSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterThePassword" /> + <click selector="{{AdminCreateRoleSection.roleResources}}" stepKey="clickToOpenRoleResources"/> + <waitForPageLoad stepKey="waitForRoleResourceTab"/> + <selectOption userInput="Custom" selector="{{AdminCreateRoleSection.resourceAccess}}" + stepKey="selectResourceAccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminFilterLoginAsCustomerLogActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminFilterLoginAsCustomerLogActionGroup.xml new file mode 100644 index 0000000000000..17eb351bf8f1b --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminFilterLoginAsCustomerLogActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFilterLoginAsCustomerLogActionGroup"> + <annotations> + <description>Filter Login as Customer Log records.</description> + </annotations> + <arguments> + <argument name="adminId" type="string"/> + <argument name="customerId" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{AdminLoginAsCustomerLogPage.url}}" stepKey="checkUrl"/> + <click selector="{{AdminLoginAsCustomerLogToolbarSection.resetFilter}}" stepKey="resetFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForResetFilter"/> + <fillField selector="{{AdminLoginAsCustomerLogFiltersSection.adminId}}" userInput="{{adminId}}" + stepKey="fillAdminId"/> + <fillField selector="{{AdminLoginAsCustomerLogFiltersSection.customerId}}" userInput="{{customerId}}" + stepKey="fillCustomerId"/> + <click selector="{{AdminLoginAsCustomerLogToolbarSection.search}}" stepKey="applyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForApplyFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnCustomerPageActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnCustomerPageActionGroup.xml new file mode 100644 index 0000000000000..e56d898fa9197 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnCustomerPageActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginAsCustomerAbsentOnCustomerPageActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is absent on Customer page.</description> + </annotations> + <arguments> + <argument name="customerId" type="string"/> + </arguments> + + <amOnPage url="{{AdminEditCustomerPage.url(customerId)}}" stepKey="gotoCustomerPage"/> + <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <dontSee userInput="Login as Customer" stepKey="dontSeeLoginAsCustomer"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnOrderPageActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnOrderPageActionGroup.xml new file mode 100644 index 0000000000000..1119f6b05fac3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnOrderPageActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginAsCustomerAbsentOnOrderPageActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is absent on Order page.</description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + + <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="gotoOrderPage"/> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <dontSee userInput="Login as Customer" stepKey="dontSeeLoginAsCustomer"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogAbsentInMenuActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogAbsentInMenuActionGroup.xml new file mode 100644 index 0000000000000..beb0f4cba973c --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogAbsentInMenuActionGroup.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="AdminLoginAsCustomerLogAbsentInMenuActionGroup"> + <annotations> + <description>Verify Login as Customer is absent in admin menu.</description> + </annotations> + + <click selector="{{AdminMenuSection.menuItem(AdminMenuCustomers.dataUiId)}}" + stepKey="clickOnCustomersMenuItem"/> + <dontSeeElement selector="{{AdminMenuSection.menuItem(AdminMenuLoginAsCustomer.dataUiId)}}" + stepKey="dontSeeLoginAsCustomerLog"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogPageNotAvailableActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogPageNotAvailableActionGroup.xml new file mode 100644 index 0000000000000..939ff73199a63 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogPageNotAvailableActionGroup.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="AdminLoginAsCustomerLogPageNotAvailableActionGroup"> + <annotations> + <description>Verify Login as Customer is not available by direct url.</description> + </annotations> + + <amOnPage url="{{AdminLoginAsCustomerLogPage.url}}" stepKey="openAdminLoginAsCustomerLogPage"/> + <waitForPageLoad stepKey="waitForLoginAsCustomerLogPageLoad"/> + <see userInput="404 Error" selector="{{AdminHeaderSection.pageHeading}}" stepKey="see404PageHeading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageActionGroup.xml new file mode 100644 index 0000000000000..599a6f8f9e270 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageActionGroup.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="AdminLoginAsCustomerLoginFromCustomerPageActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is works properly from Customer page.</description> + </annotations> + <arguments> + <argument name="customerId" type="string"/> + </arguments> + + <amOnPage url="{{AdminEditCustomerPage.url(customerId)}}" stepKey="gotoCustomerPage"/> + <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <click selector="{{AdminCustomerMainActionsSection.loginAsCustomer}}" stepKey="clickLoginAsCustomerLink"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="You are about to Login as Customer" + stepKey="seeModal"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="Actions taken while in "Login as Customer" will affect actual customer data." stepKey="seeModalMessage"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickLogin"/> + <switchToNextTab stepKey="switchToNewTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml new file mode 100644 index 0000000000000..8db34a05252ee --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is works properly from Customer page with manual Store View choose.</description> + </annotations> + <arguments> + <argument name="customerId" type="string"/> + <argument name="storeViewName" type="string" defaultValue="default"/> + </arguments> + + <amOnPage url="{{AdminEditCustomerPage.url(customerId)}}" stepKey="gotoCustomerPage"/> + <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <click selector="{{AdminCustomerMainActionsSection.loginAsCustomer}}" stepKey="clickLoginAsCustomerLink"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Login as Customer: Select Store View" stepKey="seeModal"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="Actions taken while in "Login as Customer" will affect actual customer data." stepKey="seeModalMessage"/> + <selectOption selector="{{AdminLoginAsCustomerConfirmationModalSection.storeView}}" userInput="{{storeViewName}}" stepKey="selectStoreView"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickLogin"/> + <switchToNextTab stepKey="switchToNewTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml new file mode 100644 index 0000000000000..a478f8e9d18cd --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.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="AdminLoginAsCustomerLoginFromOrderPageActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is works properly from Order grid page.</description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + + <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="gotoOrderPage"/> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <click selector="{{AdminOrderDetailsMainActionsSection.loginAsCustomer}}" stepKey="clickLoginAsCustomerLink"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="You are about to Login as Customer" + stepKey="seeModal"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="Actions taken while in "Login as Customer" will affect actual customer data." stepKey="seeModalMessage"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickLogin"/> + <switchToNextTab stepKey="switchToNewTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogActionGroup.xml new file mode 100644 index 0000000000000..9130ba5b05c51 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogActionGroup.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="AdminOpenLoginAsCustomerLogActionGroup"> + <annotations> + <description>Navigate to Login as Customer Log page.</description> + </annotations> + + <amOnPage url="{{AdminLoginAsCustomerLogPage.url}}" stepKey="gotoLoginAsCustomerLogPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="Login as Customer Log" stepKey="titleIsVisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogFromMenuActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogFromMenuActionGroup.xml new file mode 100644 index 0000000000000..b1a99d67afe4f --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogFromMenuActionGroup.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="AdminOpenLoginAsCustomerLogFromMenuActionGroup"> + <annotations> + <description>Navigate to Login as Customer Log from Menu.</description> + </annotations> + + <click selector="{{AdminMenuSection.menuItem(AdminMenuCustomers.dataUiId)}}" + stepKey="clickOnCustomersMenuItem"/> + <click selector="{{AdminMenuSection.menuItem(AdminMenuLoginAsCustomer.dataUiId)}}" + stepKey="openLoginAsCustomerLog"/> + <waitForPageLoad stepKey="waitForLoginAsCustomerLog"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Login as Customer Log" + stepKey="seeForLoginAsCustomerLog"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminRevokeRoleResourceActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminRevokeRoleResourceActionGroup.xml new file mode 100644 index 0000000000000..030b53408951e --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminRevokeRoleResourceActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminRevokeRoleResourceActionGroup"> + <annotations> + <description>Revoke access to resource from edit role page.</description> + </annotations> + <arguments> + <argument name="resourceName" type="string"/> + </arguments> + + <selectOption selector="{{AdminEditRoleResourcesSection.resourceAccess}}" userInput="0" + stepKey="selectResourceAccessCustom"/> + <waitForElementVisible selector="{{AdminEditRoleInfoSection.blockName(resourceName)}}" + stepKey="waitForElementVisible"/> + <scrollTo selector="{{AdminEditRoleInfoSection.blockName(resourceName)}}" x="0" y="-80" + stepKey="scrollToContentBlock"/> + <click selector="{{AdminEditRoleInfoSection.blockName(resourceName)}}" stepKey="clickContentBlockCheckbox"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AssertStorefrontStickyLoginAsCustomerNotificationBannerActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AssertStorefrontStickyLoginAsCustomerNotificationBannerActionGroup.xml new file mode 100644 index 0000000000000..b1b3ccd05ddfc --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AssertStorefrontStickyLoginAsCustomerNotificationBannerActionGroup.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontStickyLoginAsCustomerNotificationBannerActionGroup"> + <annotations> + <description>Verify Sticky Login as Customer notification banner present on page.</description> + </annotations> + <arguments> + <argument name="customerFullName" type="string"/> + <argument name="websiteName" type="string" defaultValue="Main Website"/> + </arguments> + + <waitForElementVisible selector="{{StorefrontLoginAsCustomerNotificationSection.notificationText}}" stepKey="waitForNotificationBanner"/> + <see selector="{{StorefrontLoginAsCustomerNotificationSection.notificationText}}" + userInput="You are connected as {{customerFullName}} on {{websiteName}}" + stepKey="assertCorrectNotificationBannerMessage"/> + <seeElement selector="{{StorefrontLoginAsCustomerNotificationSection.closeLink}}" + stepKey="assertCloseNotificationBannerPresent"/> + <executeJS function="window.scrollTo(0,document.body.scrollHeight);" stepKey="scrollToBottomOfPage"/> + <see selector="{{StorefrontLoginAsCustomerNotificationSection.notificationText}}" + userInput="You are connected as {{customerFullName}} on {{websiteName}}" + stepKey="assertCorrectNotificationBannerMessageAfterScroll"/> + <seeElement selector="{{StorefrontLoginAsCustomerNotificationSection.closeLink}}" + stepKey="assertCloseNotificationBannerPresentAfterScroll"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup.xml new file mode 100644 index 0000000000000..f40ea7f93c7a1 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup.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="StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup"> + <annotations> + <description>Verify Storefront Order page contains message about Order created by a Store Administrator. + </description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + + <amOnPage url="{{StorefrontCustomerOrderViewPage.url(orderId)}}" stepKey="gotoOrderPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="Order Placed by Store Administrator" stepKey="seeMessageOrderCreatedByAdmin"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertCustomerOnStoreViewActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertCustomerOnStoreViewActionGroup.xml new file mode 100644 index 0000000000000..f63cda2303526 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertCustomerOnStoreViewActionGroup.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="StorefrontAssertCustomerOnStoreViewActionGroup"> + <annotations> + <description>Assert Customer is on the provided Store View.</description> + </annotations> + <arguments> + <argument name="storeViewName" type="string" defaultValue="Default Store View"/> + </arguments> + + <see selector="{{StorefrontHeaderSection.storeViewSwitcher}}" userInput="{{storeViewName}}" stepKey="clickStoreViewSwitcher"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerLoggedInActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerLoggedInActionGroup.xml new file mode 100644 index 0000000000000..bb7e938bdfb59 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerLoggedInActionGroup.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="StorefrontAssertLoginAsCustomerLoggedInActionGroup"> + <annotations> + <description>Verify Admin successfully logged in as Customer.</description> + </annotations> + <arguments> + <argument name="customerFullName" type="string"/> + <argument name="customerEmail" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{StorefrontCustomerDashboardPage.url}}" stepKey="assertOnCustomerAccountPage"/> + <see selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" userInput="Welcome, {{customerFullName}}!" stepKey="assertCorrectWelcomeMessage"/> + <see selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" + userInput="{{customerEmail}}" stepKey="assertCustomerEmailInContactInformation"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerNotificationBannerActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerNotificationBannerActionGroup.xml new file mode 100644 index 0000000000000..ce2e261f10040 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerNotificationBannerActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup"> + <annotations> + <description>Verify Login as Customer notification banner present on page.</description> + </annotations> + <arguments> + <argument name="customerFullName" type="string"/> + <argument name="websiteName" type="string" defaultValue="Main Website"/> + </arguments> + + <waitForElementVisible selector="{{StorefrontLoginAsCustomerNotificationSection.notificationText}}" stepKey="waitForNotificationBanner"/> + <see selector="{{StorefrontLoginAsCustomerNotificationSection.notificationText}}" + userInput="You are connected as {{customerFullName}} on {{websiteName}}" + stepKey="assertCorrectNotificationBannerMessage"/> + <seeElement selector="{{StorefrontLoginAsCustomerNotificationSection.closeLink}}" + stepKey="assertCloseNotificationBannerPresent"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutAndCloseTabActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutAndCloseTabActionGroup.xml new file mode 100644 index 0000000000000..87e5b264a6ed6 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutAndCloseTabActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSignOutAndCloseTabActionGroup"> + <annotations> + <description>Customer sign out and close tab.</description> + </annotations> + + <click selector="{{StoreFrontSignOutSection.customerAccount}}" stepKey="clickCustomerButton"/> + <waitForElementVisible selector="{{StoreFrontSignOutSection.signOut}}" stepKey="waitForSignOut"/> + <click selector="{{StoreFrontSignOutSection.signOut}}" stepKey="clickToSignOut"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You are signed out" stepKey="signOut"/> + <closeTab stepKey="closeTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutNotificationBannerAndCloseTabActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutNotificationBannerAndCloseTabActionGroup.xml new file mode 100644 index 0000000000000..e0e6973509eb3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutNotificationBannerAndCloseTabActionGroup.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="StorefrontSignOutNotificationBannerAndCloseTabActionGroup"> + <annotations> + <description>Customer sign out by Notification Banner and close tab.</description> + </annotations> + + <waitForElementVisible selector="{{StorefrontLoginAsCustomerNotificationSection.notificationText}}" stepKey="waitForNotificationBanner"/> + <click selector="{{StorefrontLoginAsCustomerNotificationSection.closeLink}}" stepKey="clickToSignOut"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You are signed out" stepKey="signOut"/> + <closeTab stepKey="closeTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..38779dd987c65 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuLoginAsCustomer"> + <data key="pageTitle">Login as Customer Log</data> + <data key="title">Login as Customer Log</data> + <data key="dataUiId">magento-loginascustomer-login-log</data> + </entity> +</entities> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/CustomerData.xml new file mode 100644 index 0000000000000..10cdc87be6430 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/CustomerData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="Simple_US_Customer_Assistance_Allowed" type="customer" extends="Simple_US_Customer"> + <requiredEntity type="customer_extension_attribute">AssistanceAllowed</requiredEntity> + </entity> + <entity name="Simple_US_CA_Customer_Assistance_Allowed" type="customer" extends="Simple_US_CA_Customer"> + <requiredEntity type="customer_extension_attribute">AssistanceAllowed</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/ExtensionAttributeAssistanceData.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/ExtensionAttributeAssistanceData.xml new file mode 100644 index 0000000000000..44582cfae5c36 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/ExtensionAttributeAssistanceData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AssistanceDisallowed" type="customer_extension_attribute"> + <data key="assistance_allowed">1</data> + </entity> + <entity name="AssistanceAllowed" type="customer_extension_attribute"> + <data key="assistance_allowed">2</data> + </entity> +</entities> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/LoginAsCustomerConfigData.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/LoginAsCustomerConfigData.xml new file mode 100644 index 0000000000000..316a810ce13e4 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/LoginAsCustomerConfigData.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details.`` + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="LoginAsCustomerConfigDataEnabled"> + <data key="path">login_as_customer/general/enabled</data> + </entity> + <entity name="LoginAsCustomerDisablePageCache"> + <data key="path">login_as_customer/general/disable_page_cache</data> + </entity> + <entity name="LoginAsCustomerStoreViewLogin"> + <data key="path">login_as_customer/general/store_view_manual_choice_enabled</data> + </entity> +</entities> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/UserRoleData.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/UserRoleData.xml new file mode 100644 index 0000000000000..720ae7eb2147e --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/UserRoleData.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <!--This Role has access for all resources individually --> + <entity name="CustomRoleAllResources" type="user_role"> + <data key="name" unique="suffix">allAccessRole</data> + <data key="rolename">allAccessRole</data> + <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + <data key="resourceAccess">Custom</data> + <data key="resource"> + [ + 'Magento_Backend::dashboard', + 'Magento_Analytics::analytics', + 'Magento_Sales::sales', + 'Magento_Catalog::catalog', + 'Magento_Customer::customer', + 'Magento_Cart::cart', + 'Magento_Backend::myaccount', + 'Magento_Backend::marketing', + 'Magento_Backend::content', + 'Magento_Reports::report', + 'Magento_Backend::stores', + 'Magento_Backend::system', + 'Magento_Backend::global_search', + ] + </data> + </entity> +</entities> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE.txt b/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminCustomerConfigPage.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminCustomerConfigPage.xml new file mode 100644 index 0000000000000..b48e20237cee8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminCustomerConfigPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminLoginAsCustomerConfigPage" url="admin/system_config/edit/section/login_as_customer" area="admin" module="Magento_LoginAsCustomer"> + <section name="AdminCustomerConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLogPage.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLogPage.xml new file mode 100644 index 0000000000000..a917ab6acb182 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLogPage.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:/Page/etc/PageObject.xsd"> + <page name="AdminLoginAsCustomerLogPage" url="loginascustomer_log/log/index/" area="admin" module="Magento_LoginAsCustomer"> + <section name="AdminLoginAsCustomerLogToolbarSection"/> + <section name="AdminLoginAsCustomerLogFiltersSection"/> + <section name="AdminLoginAsCustomerLogGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLoginManualPage.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLoginManualPage.xml new file mode 100644 index 0000000000000..ddb87ba83bc3a --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLoginManualPage.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:/Page/etc/PageObject.xsd"> + <page name="AdminLoginAsCustomerLoginManualPage" url="loginascustomer/login/manual/entity_id/{{id}}/" + area="storefront" module="Magento_LoginAsCustomer" parameterized="true"> + <section name="AdminLoginAsCustomerLoginManualActionsSection"/> + <section name="AdminLoginAsCustomerLoginManualContentSection"/> + </page> +</pages> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminRoleEditPage.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminRoleEditPage.xml new file mode 100644 index 0000000000000..12eba90aea723 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminRoleEditPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminRoleEditPage" url="admin/user_role/editrole/rid/{{roleId}}/" module="Magento_User" area="admin" parameterized="true"> + <section name="AdminRoleGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/StorefrontLoginAsCustomerLoginProceedPage.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/StorefrontLoginAsCustomerLoginProceedPage.xml new file mode 100644 index 0000000000000..05af5f506112e --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/StorefrontLoginAsCustomerLoginProceedPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:/Page/etc/PageObject.xsd"> + <page name="StorefrontLoginAsCustomerLoginProceedPage" url="loginascustomer/login/proceed" area="storefront" module="Magento_LoginAsCustomer"/> +</pages> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/README.md b/app/code/Magento/LoginAsCustomer/Test/Mftf/README.md new file mode 100644 index 0000000000000..1d574fc35cdab --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Functional Tests + +The Functional Tests for **Magento Login as Customer** module. diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml new file mode 100644 index 0000000000000..1a31afad3fbf0 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerAccountInformationSection.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="AdminCustomerAccountInformationSection"> + <element name="assistanceAllowed" type="button" selector="//input[@name='customer[extension_attributes][assistance_allowed]']/../label"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerConfigSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerConfigSection.xml new file mode 100644 index 0000000000000..14b3336ea5484 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerConfigSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerConfigSection"> + <element name="loginAsCustomerSettingsSectionLink" type="text" selector="//a/span[contains(text(),'Login as Customer')]" timeout="30"/> + <element name="loginAsCustomerSettingsHead" type="text" selector="#login_as_customer_general-head" timeout="30"/> + <element name="enableExtensionLabel" type="text" selector="//span[contains(text(),'Enable Extension') and contains(@data-config-scope,'[GLOBAL]')]" timeout="30"/> + <element name="disablePageCacheLabel" type="text" selector="//span[contains(text(),'Disable Page Cache For Admin User') and contains(@data-config-scope,'[GLOBAL]')]" timeout="30"/> + <element name="storeViewToLoginToLabel" type="text" selector="//span[contains(text(),'Store View To Login To') and contains(@data-config-scope,'[GLOBAL]')]" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerGridSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerGridSection.xml new file mode 100644 index 0000000000000..4d7c49644957d --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerGridSection.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="AdminCustomerGridSection"> + <element name="customerLoginAsCustomerLinkByEmail" type="text" selector="//tr[contains(@class, 'data-row') and td/div[text()='{{customerEmail}}']]//a[@class='action-menu-item'][text() = 'Login as Customer']" parameterized="true" timeout="30"/> + </section> +</sections> + diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml new file mode 100644 index 0000000000000..b7e35ba113795 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerMainActionsSection.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="AdminCustomerMainActionsSection"> + <element name="loginAsCustomer" type="button" selector="#login_as_customer" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerConfirmationModalSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerConfirmationModalSection.xml new file mode 100644 index 0000000000000..f400ba02a5392 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerConfirmationModalSection.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="AdminLoginAsCustomerConfirmationModalSection"> + <element name="storeView" type="select" selector="//select[@id='lac-confirmation-popup-store-id']"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogFiltersSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogFiltersSection.xml new file mode 100644 index 0000000000000..dce2204f86f82 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogFiltersSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminLoginAsCustomerLogFiltersSection"> + <element name="loginId" type="input" selector="//input[@id='subscriberGrid_filter_login_id']"/> + <element name="customerId" type="input" selector="//input[@id='subscriberGrid_filter_customer_id']"/> + <element name="customerEmail" type="input" selector="//input[@id='subscriberGrid_filter_email']"/> + <element name="adminId" type="input" selector="//input[@id='subscriberGrid_filter_admin_id']"/> + <element name="adminName" type="input" selector="//input[@id='subscriberGrid_filter_username']"/> + <element name="from" type="input" selector="//input[@name='created_at[from]']"/> + <element name="to" type="input" selector="//input[@name='created_at[to]']"/> + </section> +</sections> + diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogGridSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogGridSection.xml new file mode 100644 index 0000000000000..032281a2277f4 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogGridSection.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="AdminLoginAsCustomerLogGridSection"> + <element name="loginIdInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[1]" parameterized="true"/> + <element name="customerIdInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[2]" parameterized="true"/> + <element name="customerEmailInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[3]" parameterized="true"/> + <element name="adminIdInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[4]" parameterized="true"/> + <element name="adminNameInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[5]" parameterized="true"/> + <element name="createdAtInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[6]" parameterized="true"/> + </section> +</sections> + diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogToolbarSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogToolbarSection.xml new file mode 100644 index 0000000000000..a403367ee0d02 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogToolbarSection.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="AdminLoginAsCustomerLogToolbarSection"> + <element name="search" type="button" selector="button[data-action='grid-filter-apply']"/> + <element name="resetFilter" type="button" selector="button[data-action='grid-filter-reset']"/> + </section> +</sections> + diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualActionsSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualActionsSection.xml new file mode 100644 index 0000000000000..a2d373d4d7ab3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualActionsSection.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="AdminLoginAsCustomerLoginManualActionsSection"> + <element name="loginAsCustomer" type="button" selector="#save" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualContentSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualContentSection.xml new file mode 100644 index 0000000000000..944bd2679e703 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualContentSection.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="AdminLoginAsCustomerLoginManualContentSection"> + <element name="storeView" type="select" selector="//select[@name='store_id']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml new file mode 100644 index 0000000000000..629ec253d2778 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.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="AdminOrderDetailsMainActionsSection"> + <element name="loginAsCustomer" type="button" selector="#guest_to_customer" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrdersGridSection.xml new file mode 100644 index 0000000000000..34c4a14e81bb5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrdersGridSection.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="AdminOrdersGridSection"> + <element name="loginAsCustomerLink" type="text" selector="//td/div[contains(.,'{{orderId}}')]/../..//a[@class='action-menu-item'][text() = 'Login as Customer']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/StorefrontLoginAsCustomerNotificationSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/StorefrontLoginAsCustomerNotificationSection.xml new file mode 100644 index 0000000000000..cee7609632e87 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/StorefrontLoginAsCustomerNotificationSection.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="StorefrontLoginAsCustomerNotificationSection"> + <element name="notificationText" type="text" selector="//div[contains(@class, 'lac-notification')]//div[contains(@class, 'lac-notification-text')]/span" timeout="30"/> + <element name="closeLink" type="button" selector="//div[contains(@class, 'lac-notification')]//div[contains(@class, 'lac-notification-links')]/a[contains(@class, 'lac-notification-close-link')]" timeout="30"/> + </section> +</sections> + diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerButtonTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerButtonTest.xml new file mode 100644 index 0000000000000..8902877b38e54 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerButtonTest.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminChangUserAccessToLoginAsCustomerButtonTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Permissions and ACl"/> + <title value="Change admin user's access to 'Login as Customer Button'"/> + <description + value="Verify admin user's access to 'Login as Customer Button' can be changed"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="MQE-1964"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserBefore"/> + + <!--Create New Role--> + <actionGroup ref="AdminOpenCreateRolePageActionGroup" stepKey="goToNewRolePage"/> + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillNewRoleForm"> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveNewRole"/> + + <!--Create New User--> + <actionGroup ref="AdminCreateUserWithApiRoleActionGroup" stepKey="adminCreateUser"> + <argument name="user" value="NewAdminUser"/> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!--Delete new User--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutNewUserAfter"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserAfter"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteNewUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + + <!--Delete new Role--> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteCustomRoleAllResources"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOutFromAdminPanel"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Verify new User has access to 'Login as Customer Button' --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="verifyLoginAsCustomerWorksOnCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="customerSignOutAndCloseTab"/> + + <!-- Revoke 'Login as Customer Button' access for new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutNewUser"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUser"/> + + <actionGroup ref="AdminEditUserRoleActionGroup" stepKey="openEditUserRole"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminRevokeRoleResourceActionGroup" stepKey="revokeLoginAsCustomerAccess"> + <argument name="resourceName" value="Allow Login as Customer Button"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveEditedRole"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutDefaultAdminUserAfterRevoke"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUserAfterRevoke"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Verify new User no longer has access to 'Login as Customer Button' --> + <actionGroup ref="AdminLoginAsCustomerAbsentOnCustomerPageActionGroup" stepKey="verifyLoginAsCustomerAbsentOnCustomerPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerLogTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerLogTest.xml new file mode 100644 index 0000000000000..c0aa3740309a6 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerLogTest.xml @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminChangUserAccessToLoginAsCustomerLogTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Permissions and ACl"/> + <title value="Change admin user's access to 'Login as Customer Log'"/> + <description + value="Verify admin user's access to 'Login as Customer Log' can be changed"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="MQE-1964"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserBefore"/> + + <!--Create New Role--> + <actionGroup ref="AdminOpenCreateRolePageActionGroup" stepKey="goToNewRolePage"/> + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillNewRoleForm"> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveNewRole"/> + + <!--Create New User--> + <actionGroup ref="AdminCreateUserWithApiRoleActionGroup" stepKey="adminCreateUser"> + <argument name="user" value="NewAdminUser"/> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + </before> + <after> + <!--Delete new User--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutNewUserAfter"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserAfter"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteNewUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + + <!--Delete new Role--> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteCustomRoleAllResources"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOutFromAdminPanel"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Verify new User has access to 'Login as Customer Log' --> + <actionGroup ref="AdminOpenLoginAsCustomerLogFromMenuActionGroup" stepKey="openLoginAsCustomerLog"/> + + <!-- Revoke 'Login as Customer Log' access for new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutNewUser"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUser"/> + + <actionGroup ref="AdminEditUserRoleActionGroup" stepKey="openEditUserRole"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminRevokeRoleResourceActionGroup" stepKey="revokeLoginAsCustomerAccess"> + <argument name="resourceName" value="View Login as Customer Log"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveEditedRole"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutDefaultAdminUserAfterRevoke"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUserAfterRevoke"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Verify new User no longer has access to 'Login as Customer Log' menu item --> + <actionGroup ref="AdminLoginAsCustomerLogAbsentInMenuActionGroup" stepKey="verifyLoginAsCustomerLogAbsentInMenu"/> + + <!-- Verify new User no longer has access to 'Login as Customer Log' --> + <actionGroup ref="AdminLoginAsCustomerLogPageNotAvailableActionGroup" stepKey="verifyLoginAsCustomerLogPageNotAvailable"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml new file mode 100644 index 0000000000000..c083383dd8861 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.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="AdminLoginAsCustomerAddProductToWishlistTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Added product to wish-list"/> + <title value="Admin user login as customer and add products to customer's wish-list"/> + <description + value="Verify that Admin can add products to customer's wish-list using Login as Customer functionality"/> + <severity value="AVERAGE"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Admin Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Navigate to Product page and add it to Wishlist --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createSimpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addProductToWishlist"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Open Customer Wishlist and verify Product present there --> + <actionGroup ref="AssertProductIsPresentInWishListActionGroup" stepKey="assertProductInWishlist"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productPrice" value="$$createSimpleProduct.price$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml new file mode 100644 index 0000000000000..de9790894015c --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerAutoDetectionTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Select Store View based on 'Store View To Login In' setting"/> + <title + value="Admin user directly login into customer account with store View To Login In = Auto detection"/> + <description + value="Verify admin user can directly login into customer account to Default store view when Store View To Login In = Auto detection"/> + <severity value="BLOCKER"/> + <group value="login_as_customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI + command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" + stepKey="enableAddStoreCodeToUrls"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCustomStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI + command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" + stepKey="disableAddStoreCodeToUrls"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Assert Customer logged on on default store view --> + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromCustomerGird"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontStoreCodeInUrlActionGroup" stepKey="seeDefaultStoreCodeInUrl"/> + + <!-- Log out Customer and close tab --> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAndCloseTab"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml new file mode 100644 index 0000000000000..f9418a9cf1e1b --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.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="AdminLoginAsCustomerDirectlyToCustomWebsiteTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Login as Customer into additional website"/> + <title value="Admin user directly login into customer account on custom website"/> + <description + value="Verify admin user can directly login into customer account on custom website using 'Login as customer' functionality"/> + <severity value="BLOCKER"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI + command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" + stepKey="enableAddStoreCodeToUrls"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createCustomWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminCreateCustomerWithWebSiteAndGroupActionGroup" stepKey="createCustomer"> + <argument name="customerData" value="Simple_US_Customer_Assistance_Allowed"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeView" value="{{customStoreEN.name}}"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="Simple_US_Customer_Assistance_Allowed.email"/> + </actionGroup> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI + command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" + stepKey="disableAddStoreCodeToUrls"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="OpenEditCustomerFrom"> + <argument name="customer" value="Simple_US_Customer_Assistance_Allowed"/> + </actionGroup> + <grabFromCurrentUrl regex="~id/(\d+)/~" stepKey="customerId" /> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="${customerId}"/> + </actionGroup> + + <!-- Assert Customer logged on Custom Website --> + <actionGroup ref="AssertStorefrontStoreCodeInUrlActionGroup" stepKey="seeStoreCodeInUrl"> + <argument name="storeCode" value="{{customStoreEN.code}}"/> + </actionGroup> + + <!-- Log out Customer and close tab --> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAndCloseTab"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml new file mode 100644 index 0000000000000..cf90f0b6a8511 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerEditCustomersAddressTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Edit Customer addresses"/> + <title value="Admin user login as customer and edit customer's address"/> + <description + value="Verify Admin can access customer's personal cabinet and change his default shipping and billing addresses using Login as Customer functionality"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAdmin"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer Login from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Add new default address --> + <actionGroup ref="StorefrontAddCustomerDefaultAddressActionGroup" stepKey="addNewDefaultAddress"> + <argument name="Address" value="US_Address_CA"/> + </actionGroup> + + <!-- Open Customer edit page --> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAfterLoggedInAsCustomer"/> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + + <!-- Assert Customer Default Billing Address --> + <actionGroup stepKey="checkDefaultBilling" ref="AdminAssertCustomerDefaultBillingAddress"> + <argument name="firstName" value="$$createCustomer.firstname$$"/> + <argument name="lastName" value="$$createCustomer.lastname$$"/> + <argument name="street1" value="{{US_Address_CA.street[0]}}"/> + <argument name="state" value="{{US_Address_CA.state}}"/> + <argument name="postcode" value="{{US_Address_CA.postcode}}"/> + <argument name="country" value="{{US_Address_CA.country}}"/> + <argument name="telephone" value="{{US_Address_CA.telephone}}"/> + </actionGroup> + + <!-- Assert Customer Default Shipping Address --> + <actionGroup stepKey="checkDefaultShipping" ref="AdminAssertCustomerDefaultShippingAddress"> + <argument name="firstName" value="$$createCustomer.firstname$$"/> + <argument name="lastName" value="$$createCustomer.lastname$$"/> + <argument name="street1" value="{{US_Address_CA.street[0]}}"/> + <argument name="state" value="{{US_Address_CA.state}}"/> + <argument name="postcode" value="{{US_Address_CA.postcode}}"/> + <argument name="country" value="{{US_Address_CA.country}}"/> + <argument name="telephone" value="{{US_Address_CA.telephone}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml new file mode 100644 index 0000000000000..4ef72d949065d --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Availability of UI elements if module enable/disable"/> + <title value="'Login as Customer Log' not shown if 'Login as customer' functionality is disabled"/> + <description value="Verify that 'Login as Customer Log' not shown if 'Login as customer' functionality is disabled"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Verify Login as Customer Log is absent in admin menu --> + <actionGroup ref="AdminLoginAsCustomerLogAbsentInMenuActionGroup" stepKey="verifyLoginAsCustomerLogAbsentInMenu"/> + + <!-- Verify Login as Customer Log is not available by direct url --> + <actionGroup ref="AdminLoginAsCustomerLogPageNotAvailableActionGroup" stepKey="verifyLoginAsCustomerLogNotAvailable"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml new file mode 100644 index 0000000000000..5b5e9e21113c8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerLoggingTest"> + <annotations> + <features value="Login as Customer"/> + <!-- TODO: change "stories" value --> + <stories value="Place order and reorder"/> + <title value="Using 'Login as Customer' is logged properly"/> + <description + value="Verify that 'Login as customer Log' record information about using 'Login as Customer' functionality properly"/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="NewAdminUser" stepKey="createNewAdmin"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createFirstCustomer"/> + <createData entity="Simple_US_CA_Customer_Assistance_Allowed" stepKey="createSecondCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> + </before> + <after> + <deleteData createDataKey="createFirstCustomer" stepKey="deleteFirstCustomer"/> + <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToDeleteNewAdmin"/> + <actionGroup ref="AdminDeleteUserViaCurlActionGroup" stepKey="deleteNewAdmin"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login into First Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsFirstCustomerByDefaultAdmin"> + <argument name="customerId" value="$$createFirstCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutFirstCustomerDefaultAdmin"/> + + <!-- Login into Second Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsSecondCustomerByDefaultAdmin"> + <argument name="customerId" value="$$createSecondCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutSecondCustomerDefaultAdmin"/> + + <!-- Log out as Default Admin User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsDefaultAdmin"/> + + <!-- Login as New Admin User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="$$createNewAdmin.username$$"/> + <argument name="password" value="$$createNewAdmin.password$$"/> + </actionGroup> + + <!-- Login into First Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsFirstCustomerByNewAdmin"> + <argument name="customerId" value="$$createFirstCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutFirstCustomerNewAdmin"/> + + <!-- Login into Second Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsSecondCustomerByNewAdmin"> + <argument name="customerId" value="$$createSecondCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutSecondCustomerNewAdmin"/> + + <!-- Navigate to Login as Customer Log page --> + <actionGroup ref="AdminOpenLoginAsCustomerLogActionGroup" stepKey="gotoLoginAsCustomerLog"/> + + <!-- Perform assertions --> + <actionGroup ref="AdminAssertLoginAsCustomerLogRecordActionGroup" stepKey="verifyDefaultAdminFirstCustomerLogRecord"> + <argument name="rowNumber" value="4"/> + <argument name="adminId" value="1"/> + <argument name="customerId" value="$$createFirstCustomer.id$$"/> + </actionGroup> + <actionGroup ref="AdminAssertLoginAsCustomerLogRecordActionGroup" stepKey="verifyDefaultAdminSecondCustomerLogRecord"> + <argument name="rowNumber" value="3"/> + <argument name="adminId" value="1"/> + <argument name="customerId" value="$$createSecondCustomer.id$$"/> + </actionGroup> + <actionGroup ref="AdminAssertLoginAsCustomerLogRecordActionGroup" stepKey="verifyNewAdminFirstCustomerLogRecord"> + <argument name="rowNumber" value="2"/> + <argument name="adminId" value="$$createNewAdmin.id$$"/> + <argument name="customerId" value="$$createFirstCustomer.id$$"/> + </actionGroup> + <actionGroup ref="AdminAssertLoginAsCustomerLogRecordActionGroup" stepKey="verifyNewAdminSecondCustomerLogRecord"> + <argument name="rowNumber" value="1"/> + <argument name="adminId" value="$$createNewAdmin.id$$"/> + <argument name="customerId" value="$$createSecondCustomer.id$$"/> + </actionGroup> + + <!-- Log out as New Admin User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsNewAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml new file mode 100644 index 0000000000000..27aee2061f204 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.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="AdminLoginAsCustomerManualChooseStoreCodeInUrlTest" extends="AdminLoginAsCustomerManualChooseTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Select Store View based on 'Store View To Login In' setting"/> + <title + value="Admin user directly login into customer account with store View To Login In = Manual Choose when store code is added to url"/> + <description + value="Verify admin user can directly login into customer account to Custom store view when Store View To Login In = Manual Choose when store code is added to url"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI + command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" + stepKey="enableAddStoreCodeToUrls" after="enableLoginAsCustomerManualChoose"/> + </before> + <after> + <magentoCLI + command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" + stepKey="disableAddStoreCodeToUrls" after="enableLoginAsCustomerAutoDetection"/> + </after> + <actionGroup ref="AssertStorefrontStoreCodeInUrlActionGroup" stepKey="seeCustomStoreCodeInUrl" after="assertCustomStoreView"> + <argument name="storeCode" value="{{customStore.code}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml new file mode 100644 index 0000000000000..da966fdcc1291 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerManualChooseTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Select Store View based on 'Store View To Login In' setting"/> + <title + value="Admin user directly login into customer account with store View To Login In = Manual Choose"/> + <description + value="Verify admin user can directly login into customer account to Custom store view when Store View To Login In = Manual Choose"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="https://github.com/magento/magento2-login-as-customer/issues/58"/> + </skip> + </annotations> + <before> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 1" + stepKey="enableLoginAsCustomerManualChoose"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> + </before> + <after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCustomStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + <argument name="storeViewName" value="{{customStore.name}}"/> + </actionGroup> + + <!-- Assert Customer logged on on custom store view --> + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromCustomerGird"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCustomerOnStoreViewActionGroup" stepKey="assertCustomStoreView"> + <argument name="storeViewName" value="{{customStore.name}}"/> + </actionGroup> + + <!-- Log out Customer and close tab --> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAndCloseTab"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml new file mode 100644 index 0000000000000..79c7571a08cfb --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerMultishippingLoggingTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Place order and reorder"/> + <title value="Admin User login as Customer and place Order with Multiple Addresses"/> + <description value="Verify that Admin user can place Order with Multiple Addresses using 'Login as customer' functionality "/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + <group value="multishipping"/> + <skip> + <issueId value="https://github.com/magento/magento2-login-as-customer/pull/192"/> + </skip> + </annotations> + + <before> + <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShipping"/> + <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShipping"/> + <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="SimpleProduct2" stepKey="createProduct1"/> + <createData entity="SimpleProduct2" stepKey="createProduct2"/> + <createData entity="Simple_US_Customer_Assistance_Allowed_Two_Addresses" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllOrdersGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <!-- Add Products to Cart --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProduct1ToCart"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProduct2ToCart"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + + <!-- Place Order --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <actionGroup ref="CheckingWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <waitForPageLoad stepKey="waitForShippingInfoPageLoad"/> + <actionGroup ref="SelectMultiShippingInfoActionGroup" stepKey="checkoutWithMultipleShipping"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="checkoutWithPaymentMethod"/> + <waitForPageLoad stepKey="waitForReviewOrderPageLoad"/> + <actionGroup ref="ReviewOrderForMultiShipmentActionGroup" stepKey="reviewOrderForMultiShipment"> + <argument name="totalNameForFirstOrder" value="Shipping & Handling"/> + <argument name="totalPositionForFirstOrder" value="1"/> + <argument name="totalNameForSecondOrder" value="Shipping & Handling"/> + <argument name="totalPositionForSecondOrder" value="2"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPlaceOrderPageLoad"/> + <actionGroup ref="StorefrontPlaceOrderForMultipleAddressesActionGroup" stepKey="placeOrder"> + <argument name="firstOrderPosition" value="1"/> + <argument name="secondOrderPosition" value="2"/> + </actionGroup> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + + <!-- Assert Storefront Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyStorefrontMessageFirstOrder"> + <argument name="orderId" value="{$getFirstOrderIdPlaceOrder}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyStorefrontMessageSecondOrder"> + <argument name="orderId" value="{$getSecondOrderIdPlaceOrder}"/> + </actionGroup> + + <!-- Assert Admin Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="AdminAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyAdminMessageFirstOrder"> + <argument name="orderId" value="{$getFirstOrderIdPlaceOrder}"/> + <argument name="adminUserFullName" value="Magento User"/> + </actionGroup> + <actionGroup ref="AdminAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyAdminMessageSecondOrder"> + <argument name="orderId" value="{$getSecondOrderIdPlaceOrder}"/> + <argument name="adminUserFullName" value="Magento User"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml new file mode 100644 index 0000000000000..8169b9df4c43d --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.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="AdminLoginAsCustomerPlaceOrderTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Place order and reorder"/> + <title value="Admin user login as customer and place order"/> + <description + value="Verify that admin user can place order using 'Login as customer' functionality"/> + <severity value="BLOCKER"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + + <!-- Create new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminCreateUserWithRoleActionGroup" stepKey="createAdminUser"> + <argument name="user" value="activeAdmin"/> + <argument name="role" value="roleDefaultAdministrator"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutMasterAdmin"/> + + <!-- Login as new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToNewAdmin"> + <argument name="username" value="{{activeAdmin.username}}"/> + <argument name="password" value="{{activeAdmin.password}}"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Delete new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminDeleteUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="activeAdmin"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Place Order as Customer --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$$createProduct.sku$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openCart"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderId"/> + + <!-- Assert Storefront Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyStorefrontMessageOrderCreatedByAdmin"> + <argument name="orderId" value="{$grabOrderId}"/> + </actionGroup> + + <!-- Assert Admin Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="AdminAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyAdminMessageOrderCreatedByAdmin"> + <argument name="orderId" value="{$grabOrderId}"/> + <argument name="adminUserFullName" value="{{activeAdmin.firstname}} {{activeAdmin.lastname}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml new file mode 100644 index 0000000000000..11d622319af33 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerReorderTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Place order and reorder"/> + <title value="Admin user login as customer and reorder existing order"/> + <description + value="Verify that admin user can reorder using 'Login as customer' functionality"/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + + <!-- Create new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminCreateUserWithRoleActionGroup" stepKey="createAdminUser"> + <argument name="user" value="activeAdmin"/> + <argument name="role" value="roleDefaultAdministrator"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutMasterAdmin"/> + + <!-- Login as new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToNewAdmin"> + <argument name="username" value="{{activeAdmin.username}}"/> + <argument name="password" value="{{activeAdmin.password}}"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Delete new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminDeleteUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="activeAdmin"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login to storefront as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Place Order as Customer --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$$createProduct.sku$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openCart"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderId"/> + + <!-- Log out from storefront as Customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogOut"/> + + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Make reorder --> + <actionGroup ref="StorefrontCustomerReorderActionGroup" stepKey="makeReorder"> + <argument name="orderNumber" value="{$grabOrderId}"/> + </actionGroup> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeReorder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabReorderId"/> + + <!-- Assert Storefront Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyStorefrontMessageOrderCreatedByAdmin"> + <argument name="orderId" value="${grabReorderId}"/> + </actionGroup> + + <!-- Assert Admin Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="AdminAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyAdminMessageOrderCreatedByAdmin"> + <argument name="orderId" value="${grabReorderId}"/> + <argument name="adminUserFullName" value="{{activeAdmin.firstname}} {{activeAdmin.lastname}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml new file mode 100644 index 0000000000000..bc4c4adc3ac5a --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerSubscribeToNewsletterTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Subscribe to newsletter"/> + <title value="Admin user login as customer and make subscription to newsletter"/> + <description + value="Verify that Admin can subscribe to newsletter using Login as Customer functionality"/> + <severity value="AVERAGE"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Admin Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="lLoginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Subscribe for newsletter --> + <actionGroup ref="StorefrontCustomerNavigateToNewsletterPageActionGroup" stepKey="navigateToNewsletterPage"/> + <actionGroup ref="StorefrontCustomerUpdateGeneralSubscriptionActionGroup" stepKey="subscribeToNewsletter"/> + <actionGroup ref="AssertStorefrontCustomerMessagesActionGroup" stepKey="assertMessage"> + <argument name="message" value="We have saved your subscription."/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAfterLoggedInAsCustomer"/> + + <!-- Verify subscription successful --> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <actionGroup ref="AdminAssertCustomerIsSubscribedToNewsletters" stepKey="assertSubscribedToNewsletter"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml new file mode 100644 index 0000000000000..e7b5de55a56cb --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerUserLogoutTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Destroy impersonated customer sessions on admin logout"/> + <title + value="Login as Customer sessions are ended/invalidated when the related admin session is logged out."/> + <description + value="Verify Login as Customer session is ended/invalidated when the related admin session is logged out."/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login into Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomer"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Assert correctly logged in as Customer --> + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromCustomerPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + + <!-- End Admin session --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + + <!-- Assert Customer session invalidated --> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="openCustomerAccountPage"/> + <actionGroup ref="StorefrontAssertOnCustomerLoginPageActionGroup" stepKey="AssertOnCustomerLoginPage"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml new file mode 100644 index 0000000000000..5bbc218e0a948 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerUserSingleSessionTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Destroy impersonated customer sessions on admin logout"/> + <title value="Admin users can have only one 'Login as Customer' session at a time"/> + <description + value="Verify Admin users can have only one 'Login as Customer' session at a time"/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createFirstCustomer"/> + <createData entity="Simple_US_CA_Customer_Assistance_Allowed" stepKey="createSecondCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> + </before> + <after> + <deleteData createDataKey="createFirstCustomer" stepKey="deleteFirstCustomer"/> + <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login into First Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsFirstCustomer"> + <argument name="customerId" value="$$createFirstCustomer.id$$"/> + </actionGroup> + + <!-- Assert correctly logged in as First Customer --> + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromFirstCustomerPage"> + <argument name="customerFullName" value="$$createFirstCustomer.firstname$$ $$createFirstCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createFirstCustomer.email$$"/> + </actionGroup> + + <!-- Login into Second Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsSecondCustomer"> + <argument name="customerId" value="$$createSecondCustomer.id$$"/> + </actionGroup> + + <!-- Assert correctly logged in as Second Customer --> + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromSecondCustomerPage"> + <argument name="customerFullName" value="$$createSecondCustomer.firstname$$ $$createSecondCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createSecondCustomer.email$$"/> + </actionGroup> + + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutSecondCustomer"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml new file mode 100644 index 0000000000000..50513797d06e9 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminNoAccessToLoginAsCustomerButtonTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Permissions and ACl"/> + <title value="User does not have access to 'Login as customer' button"/> + <description value="Login into Magento Admin panel as user that does not have access to 'Login as customer' button"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="MQE-1964"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserBefore"/> + + <!--Create New Role--> + <actionGroup ref="AdminOpenCreateRolePageActionGroup" stepKey="goToNewRolePage"/> + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillNewRoleForm"> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + <actionGroup ref="AdminRevokeRoleResourceActionGroup" stepKey="revokeLoginAsCustomerAccess"> + <argument name="resourceName" value="Allow Login as Customer Button"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveNewRole"/> + + <!--Create New User--> + <actionGroup ref="AdminCreateUserWithApiRoleActionGroup" stepKey="adminCreateUser"> + <argument name="user" value="NewAdminUser"/> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + + <!--Delete new User--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsSaleRoleUser"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserAfter"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteNewUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + + <!--Delete new Role--> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteCustomRoleAllResources"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOutFromAdminPanel"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Verify Login as Customer Login action is absent on Customer page --> + <actionGroup ref="AdminLoginAsCustomerAbsentOnCustomerPageActionGroup" stepKey="verifyLoginAsCustomerAbsentOnCustomerPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + + <!-- Create order --> + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderId"/> + + <!-- Verify Login as Customer Login action is absent on Order page --> + <actionGroup ref="AdminLoginAsCustomerAbsentOnOrderPageActionGroup" stepKey="verifyLoginAsCustomerAbsentOnOrderPage"> + <argument name="orderId" value="{$grabOrderId}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml new file mode 100644 index 0000000000000..d48f167656301 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminNoAccessToLoginAsCustomerConfigurationTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Permissions and ACl"/> + <title value="User does not have access to 'Login as customer' section in System Configuration"/> + <description + value="Login into Magento Admin panel as user that does not have access to 'Login as customer' section in System Configuration"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="MQE-1964"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserBefore"/> + + <!--Create New Role--> + <actionGroup ref="AdminOpenCreateRolePageActionGroup" stepKey="goToNewRolePage"/> + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillNewRoleForm"> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + <actionGroup ref="AdminRevokeRoleResourceActionGroup" stepKey="revokeLoginAsCustomerAccess"> + <argument name="resourceName" value="Login as Customer Section"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveNewRole"/> + + <!--Create New User--> + <actionGroup ref="AdminCreateUserWithApiRoleActionGroup" stepKey="adminCreateUser"> + <argument name="user" value="NewAdminUser"/> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + + <!--Delete new User--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsSaleRoleUser"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserAfter"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteNewUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + + <!--Delete new Role--> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteCustomRoleAllResources"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOutFromAdminPanel"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Navigate to Configuration page and open Customers tab --> + <actionGroup ref="AdminOpenStoreConfigPageActionGroup" stepKey="openStoreConfig"/> + <actionGroup ref="AdminExpandConfigTabActionGroup" stepKey="expandCustomersTab"> + <argument name="tabName" value="Customers"/> + </actionGroup> + + <!-- Assert no Login as Customer config section visible --> + <actionGroup ref="AdminAssertLoginAsCustomerConfigNotVisibleActionGroup" stepKey="assertConfigNotVisible"/> + + <!-- Assert Login as Customer config section is not available by direct url --> + <actionGroup ref="AdminAssertLoginAsCustomerConfigNotAvailableDirectlyActionGroup" + stepKey="assertConfigNotAvailableDirectly"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml new file mode 100644 index 0000000000000..e1ea363bdf6bc --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUINotShownIfLoginAsCustomerDisabledTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Availability of UI elements if module enable/disable"/> + <title value="UI elements are not shown if 'Login as customer' functionality is disabled"/> + <description value="Verify that UI elements are not shown if 'Login as customer' functionality is disabled"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Verify Login as Customer Login action is absent on Customer page --> + <actionGroup ref="AdminLoginAsCustomerAbsentOnCustomerPageActionGroup" stepKey="verifyLoginAsCustomerAbsentOnCustomerPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + + <!-- Create order --> + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderId"/> + + <!-- Verify Login as Customer Login action is absent on Order page --> + <actionGroup ref="AdminLoginAsCustomerAbsentOnOrderPageActionGroup" stepKey="verifyLoginAsCustomerAbsentOnOrderPage"> + <argument name="orderId" value="{$grabOrderId}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml new file mode 100644 index 0000000000000..ea06263901b9e --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUIShownIfLoginAsCustomerEnabledTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Availability of UI elements if module enable/disable"/> + <title value="UI elements are shown if 'Login as customer' functionality is enabled"/> + <description + value="Verify that UI elements are present and links are working if 'Login as customer' functionality enabled"/> + <severity value="BLOCKER"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Verify Login as Customer Login action works correctly from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="verifyLoginAsCustomerWorksOnCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromCustomerPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAfterLoggedInFromCustomerPage"/> + + <!-- Create order --> + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderId"/> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderId"/> + </actionGroup> + + <!-- Verify Login as Customer Login action works correctly from Order page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromOrderPageActionGroup" + stepKey="verifyLoginAsCustomerWorksOnOrderPage"> + <argument name="orderId" value="$grabOrderId"/> + </actionGroup> + + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromOrderPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAfterLoggedInFromOrderPage"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml new file mode 100644 index 0000000000000..4f85b9167fa54 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Notification banner appears on all pages in session"/> + <title value="Verify banner is persistent and appears during all page views in session"/> + <description value="Banner is persistent and appears on all pages in session"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <closeTab stepKey="closeLoginAsCustomerTab"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + </after> + + <!-- Admin Login as Customer from Customer page and assert notification banner --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBanner"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Go to Wishlist and assert notification banner --> + <amOnPage url="{{StorefrontCustomerWishlistPage.url}}" stepKey="amOnWishListPage"/> + <waitForPageLoad stepKey="waitForWishlistPageLoad"/> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerOnWishList"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Go to category page and assert notification banner --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerOnCategoryPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Go to product page and assert notification banner --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createSimpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerOnProductPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Add product to cart and assert notification banner --> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCartPage"/> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerOnCartPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Proceed to checkout and assert notification banner --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <waitForPageLoad stepKey="waitForProceedToCheckout"/> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerOnCheckoutPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Assert notification banner before place order --> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerBeforePlaceOrder"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Assert notification banner after place order --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerAfterPlaceOrder"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml new file mode 100644 index 0000000000000..351a3c569ce24 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.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="StorefrontLoginAsCustomerNotificationBannerTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Availability of UI elements if module enable/disable"/> + <title value="Notification Banner is present on Storefront page"/> + <description + value="Verify that Notification Banner is present on page if 'Login as customer' functionality used"/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Assert Notification Banner is present on page --> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBanner"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Log out Customer by Notification Banner and close tab --> + <actionGroup ref="StorefrontSignOutNotificationBannerAndCloseTabActionGroup" stepKey="signOutAndCloseTab"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml new file mode 100644 index 0000000000000..3e70da8f8158d --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml @@ -0,0 +1,117 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="'Login as Customer' should see special prices on a category page"/> + <title value="Special prices shown on category when Admin user Login as customer account"/> + <description value="Login as customer sees special prices on category"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="https://github.com/magento/magento2-login-as-customer/pull/193"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">10</field> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"> + <field key="group_id">3</field> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> + <argument name="name" value="{{_defaultCatalogRule.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + </after> + + <!-- Creating a new catalog price rule with 50 percent discount for Retailer customer group --> + <actionGroup ref="NewCatalogPriceRuleByUIWithConditionIsCategoryActionGroup" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> + <argument name="categoryId" value="$createCategory.id$"/> + </actionGroup> + <actionGroup ref="SelectRetailerCustomerGroupActionGroup" stepKey="selectRetailerCustomerGroup"/> + <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForCatalogPriceRule"> + <argument name="apply" value="{{CatalogRuleToPercent.simple_action}}"/> + <argument name="discountAmount" value="50"/> + </actionGroup> + + <!-- Save and apply the new catalog price rule --> + <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + + <!-- Admin Login as Customer --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBanner"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Check simple product prices on store front category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Price"> + <argument name="productInfo" value="$5.00"/> + <argument name="productNumber" value="1"/> + </actionGroup> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1RegularPrice"> + <argument name="productInfo" value="$10.00"/> + <argument name="productNumber" value="1"/> + </actionGroup> + + <!-- Place order --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$$createProduct.sku$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openCart"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderId"/> + <closeTab stepKey="closeLoginAsCustomerTab"/> + + <!-- Open order in admin --> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="addFilterToGridAndOpenOrder"> + <argument name="orderId" value="{$grabOrderId}"/> + </actionGroup> + + <!-- Assert order subtotal --> + <scrollTo selector="{{AdminOrderTotalSection.subTotal}}" stepKey="scrollToOrderTotalSection"/> + <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="$5.00" stepKey="checkOrderTotalInBackend"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest.xml new file mode 100644 index 0000000000000..b2c7c6c35db18 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Customer shopping cart shouldn't merge with guest shopping cart"/> + <title value="Customer shopping cart is not merged with guest shopping cart"/> + <description value="Shopping cart customer is not merged with guest cart"/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <closeTab stepKey="closeLoginAsCustomerTab"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + </after> + + <!-- Add product to guest cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createSimpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Admin Login as Customer --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBanner"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Check the mini cart is empty --> + <actionGroup ref="AssertMiniCartEmptyActionGroup" stepKey="miniCartEmpty"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml new file mode 100644 index 0000000000000..611bc1044fd00 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.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="StorefrontStickyLoginAsCustomerNotificationBannerTest"> + <annotations> + <features value="Login as Customer"/> + <useCaseId value="https://github.com/magento/magento2/issues/29354"/> + <stories value="Availability of sticky UI elements if module enable/disable"/> + <title value="Sticky Notification Banner is present on Storefront page"/> + <description + value="Verify that Sticky Notification Banner is present on page if 'Login as customer' functionality used"/> + <testCaseId value=""/> + <group value="login_as_customer"/> + <severity value="CRITICAL"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + </after> + + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <actionGroup ref="AssertStorefrontStickyLoginAsCustomerNotificationBannerActionGroup" stepKey="assertStickyNotificationBanner"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <actionGroup ref="StorefrontSignOutNotificationBannerAndCloseTabActionGroup" stepKey="signOutAndCloseTab"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/composer.json b/app/code/Magento/LoginAsCustomer/composer.json index ec81374528e7b..e58ec90e8f8bb 100755 --- a/app/code/Magento/LoginAsCustomer/composer.json +++ b/app/code/Magento/LoginAsCustomer/composer.json @@ -4,6 +4,7 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", + "magento/module-backend": "*", "magento/module-customer": "*", "magento/module-login-as-customer-api": "*" }, diff --git a/app/code/Magento/LoginAsCustomer/etc/config.xml b/app/code/Magento/LoginAsCustomer/etc/config.xml index 936ae1ff2f05d..7e39cc39145eb 100644 --- a/app/code/Magento/LoginAsCustomer/etc/config.xml +++ b/app/code/Magento/LoginAsCustomer/etc/config.xml @@ -10,7 +10,7 @@ <default> <login_as_customer> <general> - <enabled>0</enabled> + <enabled>1</enabled> <store_view_manual_choice_enabled>0</store_view_manual_choice_enabled> <authentication_data_expiration_time>60</authentication_data_expiration_time> </general> diff --git a/app/code/Magento/LoginAsCustomer/etc/di.xml b/app/code/Magento/LoginAsCustomer/etc/di.xml index c0ba4901ba7b8..9927237c51db6 100755 --- a/app/code/Magento/LoginAsCustomer/etc/di.xml +++ b/app/code/Magento/LoginAsCustomer/etc/di.xml @@ -5,14 +5,35 @@ * 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\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface" type="Magento\LoginAsCustomer\Model\AuthenticationData"/> - <preference for="Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\SaveAuthenticationData"/> - <preference for="Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\GetAuthenticationDataBySecret"/> - <preference for="Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface" type="Magento\LoginAsCustomer\Model\AuthenticateCustomerBySecret"/> - <preference for="Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataForUserInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\DeleteAuthenticationDataForUser"/> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface" + type="Magento\LoginAsCustomer\Model\AuthenticationData"/> + <preference for="Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface" + type="Magento\LoginAsCustomer\Model\ResourceModel\SaveAuthenticationData"/> + <preference for="Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface" + type="Magento\LoginAsCustomer\Model\ResourceModel\GetAuthenticationDataBySecret"/> + <preference for="Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface" + type="Magento\LoginAsCustomer\Model\AuthenticateCustomerBySecret"/> + <preference for="Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataForUserInterface" + type="Magento\LoginAsCustomer\Model\ResourceModel\DeleteAuthenticationDataForUser"/> <preference for="Magento\LoginAsCustomerApi\Api\ConfigInterface" type="Magento\LoginAsCustomer\Model\Config"/> <preference for="Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerSessionActiveInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\IsLoginAsCustomerSessionActive"/> + <preference for="Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface" type="Magento\LoginAsCustomer\Model\GetLoggedAsCustomerAdminId"/> + <preference for="Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerCustomerIdInterface" type="Magento\LoginAsCustomer\Model\GetLoggedAsCustomerCustomerId"/> + <preference for="Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerAdminIdInterface" type="Magento\LoginAsCustomer\Model\SetLoggedAsCustomerAdminId"/> + <preference for="Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface" + type="Magento\LoginAsCustomer\Model\IsLoginAsCustomerEnabledForCustomerResult"/> + <preference for="Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerCustomerIdInterface" type="Magento\LoginAsCustomer\Model\SetLoggedAsCustomerCustomerId"/> + <type name="Magento\LoginAsCustomerApi\Model\IsLoginAsCustomerEnabledForCustomerChain"> + <arguments> + <argument name="resolvers" xsi:type="array"> + <item name="is_enabled" xsi:type="object"> + Magento\LoginAsCustomer\Model\Resolver\IsLoginAsCustomerEnabledResolver + </item> + </argument> + </arguments> + </type> <type name="Magento\Backend\Model\Auth"> <plugin name="login_as_customer_admin_logout" type="Magento\LoginAsCustomer\Plugin\AdminLogoutPlugin"/> </type> diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php index e2d11b2c8cb80..ce5a5501fbe55 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php @@ -16,6 +16,7 @@ * Login confirmation pop-up * * @api + * @since 100.4.0 */ class ConfirmationPopup extends Template { @@ -56,6 +57,7 @@ public function __construct( /** * @inheritdoc + * @since 100.4.0 */ public function getJsLayout() { @@ -78,6 +80,7 @@ public function getJsLayout() /** * @inheritdoc + * @since 100.4.0 */ public function toHtml() { diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php index 7ccdcfe45e482..39a7055ed65bb 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php @@ -12,6 +12,7 @@ use Magento\Backend\Model\Auth\Session; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Controller\ResultInterface; @@ -22,7 +23,9 @@ use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterfaceFactory; use Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataForUserInterface; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface; use Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface; +use Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerCustomerIdInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -80,6 +83,16 @@ class Login extends Action implements HttpGetActionInterface */ private $url; + /** + * @var SetLoggedAsCustomerCustomerIdInterface + */ + private $setLoggedAsCustomerCustomerId; + + /** + * @var IsLoginAsCustomerEnabledForCustomerInterface + */ + private $isLoginAsCustomerEnabled; + /** * @param Context $context * @param Session $authSession @@ -87,9 +100,13 @@ class Login extends Action implements HttpGetActionInterface * @param CustomerRepositoryInterface $customerRepository * @param ConfigInterface $config * @param AuthenticationDataInterfaceFactory $authenticationDataFactory - * @param SaveAuthenticationDataInterface $saveAuthenticationData , + * @param SaveAuthenticationDataInterface $saveAuthenticationData * @param DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser * @param Url $url + * @param SetLoggedAsCustomerCustomerIdInterface $setLoggedAsCustomerCustomerId + * @param IsLoginAsCustomerEnabledForCustomerInterface $isLoginAsCustomerEnabled + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Context $context, @@ -100,7 +117,9 @@ public function __construct( AuthenticationDataInterfaceFactory $authenticationDataFactory, SaveAuthenticationDataInterface $saveAuthenticationData, DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser, - Url $url + Url $url, + ?SetLoggedAsCustomerCustomerIdInterface $setLoggedAsCustomerCustomerId = null, + ?IsLoginAsCustomerEnabledForCustomerInterface $isLoginAsCustomerEnabled = null ) { parent::__construct($context); @@ -112,6 +131,10 @@ public function __construct( $this->saveAuthenticationData = $saveAuthenticationData; $this->deleteAuthenticationDataForUser = $deleteAuthenticationDataForUser; $this->url = $url; + $this->setLoggedAsCustomerCustomerId = $setLoggedAsCustomerCustomerId + ?? ObjectManager::getInstance()->get(SetLoggedAsCustomerCustomerIdInterface::class); + $this->isLoginAsCustomerEnabled = $isLoginAsCustomerEnabled + ?? ObjectManager::getInstance()->get(IsLoginAsCustomerEnabledForCustomerInterface::class); } /** @@ -126,20 +149,24 @@ public function execute(): ResultInterface /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - if (!$this->config->isEnabled()) { - $this->messageManager->addErrorMessage(__('Login as Customer is disabled.')); - return $resultRedirect->setPath('customer/index/index'); - } - $customerId = (int)$this->_request->getParam('customer_id'); if (!$customerId) { $customerId = (int)$this->_request->getParam('entity_id'); } + $isLoginAsCustomerEnabled = $this->isLoginAsCustomerEnabled->execute($customerId); + if (!$isLoginAsCustomerEnabled->isEnabled()) { + foreach ($isLoginAsCustomerEnabled->getMessages() as $message) { + $this->messageManager->addErrorMessage(__($message)); + } + + return $resultRedirect->setPath('customer/index/index'); + } + try { $customer = $this->customerRepository->getById($customerId); } catch (NoSuchEntityException $e) { - $this->messageManager->addErrorMessage(__('Customer with this ID are no longer exist.')); + $this->messageManager->addErrorMessage('Customer with this ID are no longer exist.'); return $resultRedirect->setPath('customer/index/index'); } @@ -167,6 +194,7 @@ public function execute(): ResultInterface $this->deleteAuthenticationDataForUser->execute($userId); $secret = $this->saveAuthenticationData->execute($authenticationData); + $this->setLoggedAsCustomerCustomerId->execute($customerId); $redirectUrl = $this->getLoginProceedRedirectUrl($secret, $storeId); $resultRedirect->setUrl($redirectUrl); diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Model/Config/Source/StoreViewLogin.php b/app/code/Magento/LoginAsCustomerAdminUi/Model/Config/Source/StoreViewLogin.php index 265c4fedb722d..9d14a4b44d10b 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Model/Config/Source/StoreViewLogin.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Model/Config/Source/StoreViewLogin.php @@ -12,14 +12,7 @@ */ class StoreViewLogin implements \Magento\Framework\Data\OptionSourceInterface { - /** - * @const int - */ private const AUTODETECT = 0; - - /** - * @const int - */ private const MANUAL = 1; /** diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php b/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php index 89ee2791e38af..2cdcd5723df4b 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php @@ -8,10 +8,11 @@ namespace Magento\LoginAsCustomerAdminUi\Plugin\Button; use Magento\Backend\Block\Widget\Button\ButtonList; -use Magento\Backend\Block\Widget\Button\Toolbar; -use Magento\Framework\View\Element\AbstractBlock; -use Magento\Framework\Escaper; +use Magento\Backend\Block\Widget\Button\ToolbarInterface; use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Escaper; +use Magento\Framework\View\Element\AbstractBlock; +use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider; use Magento\LoginAsCustomerApi\Api\ConfigInterface; /** @@ -34,69 +35,80 @@ class ToolbarPlugin */ private $config; + /** + * @var DataProvider + */ + private $dataProvider; + /** * ToolbarPlugin constructor. * @param AuthorizationInterface $authorization * @param ConfigInterface $config * @param Escaper $escaper + * @param DataProvider $dataProvider */ public function __construct( AuthorizationInterface $authorization, ConfigInterface $config, - Escaper $escaper + Escaper $escaper, + DataProvider $dataProvider ) { $this->authorization = $authorization; $this->config = $config; $this->escaper = $escaper; + $this->dataProvider = $dataProvider; } /** * Add Login as Customer button. * - * @param \Magento\Backend\Block\Widget\Button\Toolbar $subject - * @param \Magento\Framework\View\Element\AbstractBlock $context - * @param \Magento\Backend\Block\Widget\Button\ButtonList $buttonList + * @param ToolbarInterface $subject + * @param AbstractBlock $context + * @param ButtonList $buttonList * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforePushButtons( - Toolbar $subject, + ToolbarInterface $subject, AbstractBlock $context, ButtonList $buttonList - ):void { - $order = false; + ): void { $nameInLayout = $context->getNameInLayout(); - if ('sales_order_edit' == $nameInLayout) { - $order = $context->getOrder(); - } elseif ('sales_invoice_view' == $nameInLayout) { - $order = $context->getInvoice()->getOrder(); - } elseif ('sales_shipment_view' == $nameInLayout) { - $order = $context->getShipment()->getOrder(); - } elseif ('sales_creditmemo_view' == $nameInLayout) { - $order = $context->getCreditmemo()->getOrder(); + $order = $this->getOrder($nameInLayout, $context); + if ($order + && !empty($order['customer_id']) + && $this->config->isEnabled() + && $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button') + ) { + $customerId = (int)$order['customer_id']; + $buttonList->add( + 'guest_to_customer', + $this->dataProvider->getData($customerId), + -1 + ); } - if ($order) { + } - $isAllowed = $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button'); - $isEnabled = $this->config->isEnabled(); - if ($isAllowed && $isEnabled) { - if (!empty($order['customer_id'])) { - $buttonUrl = $context->getUrl('loginascustomer/login/login', [ - 'customer_id' => $order['customer_id'] - ]); - $buttonList->add( - 'guest_to_customer', - [ - 'label' => __('Login as Customer'), - 'onclick' => 'window.lacConfirmationPopup("' - . $this->escaper->escapeHtml($this->escaper->escapeJs($buttonUrl)) - . '")', - 'class' => 'reset' - ], - -1 - ); - } - } + /** + * Extract order data from context. + * + * @param string $nameInLayout + * @param AbstractBlock $context + * @return array|null + */ + private function getOrder(string $nameInLayout, AbstractBlock $context) + { + switch ($nameInLayout) { + case 'sales_order_edit': + return $context->getOrder(); + case 'sales_invoice_view': + return $context->getInvoice()->getOrder(); + case 'sales_shipment_view': + return $context->getShipment()->getOrder(); + case 'sales_creditmemo_view': + return $context->getCreditmemo()->getOrder(); } + + return null; } } diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php new file mode 100644 index 0000000000000..24a70fc429467 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button; + +use Magento\Framework\Escaper; +use Magento\Framework\UrlInterface; + +/** + * Get data for Login as Customer button. + * + * Use this class as a base for virtual types declaration. + */ +class DataProvider +{ + /** + * @var Escaper + */ + private $escaper; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @var array + */ + private $data; + + /** + * @param Escaper $escaper + * @param UrlInterface $urlBuilder + * @param array $data + */ + public function __construct( + Escaper $escaper, + UrlInterface $urlBuilder, + array $data = [] + ) { + $this->escaper = $escaper; + $this->urlBuilder = $urlBuilder; + $this->data = $data; + } + + /** + * Get data for Login as Customer button. + * + * @param int $customerId + * @return array + */ + public function getData(int $customerId): array + { + $buttonData = [ + 'on_click' => 'window.lacConfirmationPopup("' + . $this->escaper->escapeHtml($this->escaper->escapeJs($this->getLoginUrl($customerId))) + . '")', + ]; + + return array_merge_recursive($buttonData, $this->data); + } + + /** + * Get Login as Customer login url. + * + * @param int $customerId + * @return string + */ + private function getLoginUrl(int $customerId): string + { + return $this->urlBuilder->getUrl('loginascustomer/login/login', ['customer_id' => $customerId]); + } +} diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php index 0f8f7750262f2..ab43fca3d447e 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php @@ -7,12 +7,13 @@ namespace Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Control; -use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; -use Magento\Framework\AuthorizationInterface; -use Magento\Framework\Escaper; -use Magento\Framework\Registry; use Magento\Backend\Block\Widget\Context; use Magento\Customer\Block\Adminhtml\Edit\GenericButton; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider; use Magento\LoginAsCustomerApi\Api\ConfigInterface; /** @@ -31,26 +32,26 @@ class LoginAsCustomerButton extends GenericButton implements ButtonProviderInter private $config; /** - * Escaper - * - * @var Escaper + * @var DataProvider */ - private $escaper; + private $dataProvider; /** * @param Context $context * @param Registry $registry * @param ConfigInterface $config + * @param DataProvider $dataProvider */ public function __construct( Context $context, Registry $registry, - ConfigInterface $config + ConfigInterface $config, + ?DataProvider $dataProvider = null ) { parent::__construct($context, $registry); $this->authorization = $context->getAuthorization(); $this->config = $config; - $this->escaper = $context->getEscaper(); + $this->dataProvider = $dataProvider ?? ObjectManager::getInstance()->get(DataProvider::class); } /** @@ -58,31 +59,14 @@ public function __construct( */ public function getButtonData(): array { - $customerId = $this->getCustomerId(); + $customerId = (int)$this->getCustomerId(); $data = []; $isAllowed = $customerId && $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button'); $isEnabled = $this->config->isEnabled(); if ($isAllowed && $isEnabled) { - $data = [ - 'label' => __('Login as Customer'), - 'class' => 'login login-button', - 'on_click' => 'window.lacConfirmationPopup("' - . $this->escaper->escapeHtml($this->escaper->escapeJs($this->getLoginUrl())) - . '")', - 'sort_order' => 70, - ]; + $data = $this->dataProvider->getData($customerId); } return $data; } - - /** - * Get Login as Customer login url. - * - * @return string - */ - public function getLoginUrl(): string - { - return $this->getUrl('loginascustomer/login/login', ['customer_id' => $this->getCustomerId()]); - } } diff --git a/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml b/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml index dabab45205527..b73a1d856c888 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml +++ b/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml @@ -6,7 +6,17 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Backend\Block\Widget\Button\Toolbar"> + <type name="Magento\Backend\Block\Widget\Button\ToolbarInterface"> <plugin name="login_as_customer_button_toolbar" type="Magento\LoginAsCustomerAdminUi\Plugin\Button\ToolbarPlugin"/> </type> + <type name="Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Control\LoginAsCustomerButton"> + <arguments> + <argument name="dataProvider" xsi:type="object">Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Control\LoginAsCustomerButton\DataProvider</argument> + </arguments> + </type> + <type name="Magento\LoginAsCustomerAdminUi\Plugin\Button\ToolbarPlugin"> + <arguments> + <argument name="dataProvider" xsi:type="object">Magento\LoginAsCustomerAdminUi\Plugin\Button\ToolbarPlugin\DataProvider</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/LoginAsCustomerAdminUi/etc/csp_whitelist.xml b/app/code/Magento/LoginAsCustomerAdminUi/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..b023d2adf03fd --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/etc/csp_whitelist.xml @@ -0,0 +1,25 @@ +<?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="script-src"> + <values> + <value id="paypal_objects" type="host">www.paypalobjects.com</value> + <value id="braintree_js_gateway" type="host">js.braintreegateway.com</value> + <value id="paypal_tag_gateway" type="host">www.paypal.com</value> + </values> + </policy> + <policy id="img-src"> + <values> + <value id="paypal_objects" type="host">www.paypalobjects.com</value> + <value id="paypal_analytics" type="host">t.paypal.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/LoginAsCustomerAdminUi/etc/di.xml b/app/code/Magento/LoginAsCustomerAdminUi/etc/di.xml new file mode 100644 index 0000000000000..8ba8c5c6ead43 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/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"> + <!-- Move to adminhtml area after https://github.com/magento/magento2/issues/17825 is fixed. --> + <virtualType name="Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Control\LoginAsCustomerButton\DataProvider" + type="Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider"> + <arguments> + <argument name="data" xsi:type="array"> + <item name="label" xsi:type="string" translatable="true">Login as Customer</item> + <item name="class" xsi:type="string">login login-button</item> + <item name="sort_order" xsi:type="number">15</item> + </argument> + </arguments> + </virtualType> + <!-- Move to adminhtml area after https://github.com/magento/magento2/issues/17825 is fixed. --> + <virtualType name="Magento\LoginAsCustomerAdminUi\Plugin\Button\ToolbarPlugin\DataProvider" + type="Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider"> + <arguments> + <argument name="data" xsi:type="array"> + <item name="label" xsi:type="string" translatable="true">Login as Customer</item> + <item name="class" xsi:type="string">reset</item> + </argument> + </arguments> + </virtualType> +</config> diff --git a/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/css/source/_module.less index 2901f95f0e279..d702bc49f23ed 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/css/source/_module.less @@ -39,4 +39,14 @@ } } } + + .page-actions { + .page-actions-buttons { + .login-button { + -ms-flex-order: -1; + -webkit-order: -1; + order: -1; + } + } + } } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php index aadcc8e1b566b..7048fa5a9e418 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php @@ -11,6 +11,7 @@ * LoginAsCustomer config * * @api + * @since 100.4.0 */ interface ConfigInterface { @@ -18,6 +19,7 @@ interface ConfigInterface * Check if Login as Customer extension is enabled * * @return bool + * @since 100.4.0 */ public function isEnabled(): bool; @@ -25,6 +27,7 @@ public function isEnabled(): bool; * Check if store view manual choice is enabled * * @return bool + * @since 100.4.0 */ public function isStoreManualChoiceEnabled(): bool; @@ -32,6 +35,7 @@ public function isStoreManualChoiceEnabled(): bool; * Get authentication data expiration time (in seconds) * * @return int + * @since 100.4.0 */ public function getAuthenticationDataExpirationTime(): int; } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php index f74f63c39f7ba..1126de140192a 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php @@ -13,6 +13,7 @@ * Authentication data * * @api + * @since 100.4.0 */ interface AuthenticationDataInterface extends ExtensibleDataInterface { @@ -20,6 +21,7 @@ interface AuthenticationDataInterface extends ExtensibleDataInterface * Get Customer Id * * @return int + * @since 100.4.0 */ public function getCustomerId(): int; @@ -27,6 +29,7 @@ public function getCustomerId(): int; * Get Admin Id * * @return int + * @since 100.4.0 */ public function getAdminId(): int; @@ -36,6 +39,7 @@ public function getAdminId(): int; * Fully qualified namespaces is needed for proper work of ccode generation * * @return \Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataExtensionInterface|null + * @since 100.4.0 */ public function getExtensionAttributes(): ?AuthenticationDataExtensionInterface; } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php new file mode 100644 index 0000000000000..b7d3a616176ef --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api\Data; + +/** + * IsLoginAsCustomerEnabledForCustomerInterface results. + */ +interface IsLoginAsCustomerEnabledForCustomerResultInterface +{ + /** + * Check if no validation failures occurred. + * + * @return bool + */ + public function isEnabled(): bool; + + /** + * Get error messages as array in case of validation failure, else return empty array. + * + * @return string[] + */ + public function getMessages(): array; + + /** + * Set error messages as array in case of validation failure. + * + * @param string[] $messages + */ + public function setMessages(array $messages): void; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php index 47e8d72b3b47e..b5c7405d05cdd 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php @@ -14,6 +14,7 @@ * Get authentication data by secret * * @api + * @since 100.4.0 */ interface GetAuthenticationDataBySecretInterface { @@ -23,6 +24,7 @@ interface GetAuthenticationDataBySecretInterface * @param string $secret * @return AuthenticationDataInterface * @throws LocalizedException + * @since 100.4.0 */ public function execute(string $secret): AuthenticationDataInterface; } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerAdminIdInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerAdminIdInterface.php new file mode 100644 index 0000000000000..49c0f796be006 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerAdminIdInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Get id of Admin logged as Customer. + */ +interface GetLoggedAsCustomerAdminIdInterface +{ + /** + * Get id of Admin logged as Customer. + * + * @return int + */ + public function execute(): int; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerCustomerIdInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerCustomerIdInterface.php new file mode 100644 index 0000000000000..047061b3edd69 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerCustomerIdInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Get id of Customer Admin is logged as. + */ +interface GetLoggedAsCustomerCustomerIdInterface +{ + /** + * Get id of Customer Admin is logged as. + * + * @return int + */ + public function execute(): int; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php new file mode 100644 index 0000000000000..a5355fd4566d5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface; + +/** + * Check if Login as Customer functionality is enabled for Customer. + */ +interface IsLoginAsCustomerEnabledForCustomerInterface +{ + /** + * Check if Login as Customer functionality is enabled for Customer. + * + * @param int $customerId + * @return IsLoginAsCustomerEnabledForCustomerResultInterface + */ + public function execute(int $customerId): IsLoginAsCustomerEnabledForCustomerResultInterface; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php index 30674375ed021..6e3688e586c7c 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php @@ -11,6 +11,7 @@ * Check if Login as Customer session is still active. * * @api + * @since 100.4.0 */ interface IsLoginAsCustomerSessionActiveInterface { @@ -20,6 +21,7 @@ interface IsLoginAsCustomerSessionActiveInterface * @param int $customerId * @param int $userId * @return bool + * @since 100.4.0 */ public function execute(int $customerId, int $userId): bool; } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php index 88d4cb8056cf6..1d828d7f802c0 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php @@ -14,6 +14,7 @@ * Save authentication data. Return secret key * * @api + * @since 100.4.0 */ interface SaveAuthenticationDataInterface { @@ -23,6 +24,7 @@ interface SaveAuthenticationDataInterface * @param Data\AuthenticationDataInterface $authenticationData * @return string * @throws LocalizedException + * @since 100.4.0 */ public function execute(AuthenticationDataInterface $authenticationData): string; } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerAdminIdInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerAdminIdInterface.php new file mode 100644 index 0000000000000..b921c2fc6e8d3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerAdminIdInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Set id of Admin logged as Customer. + */ +interface SetLoggedAsCustomerAdminIdInterface +{ + /** + * Set id of Admin logged as Customer. + * + * @param int $adminId + * @return void + */ + public function execute(int $adminId): void; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerCustomerIdInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerCustomerIdInterface.php new file mode 100644 index 0000000000000..265ae1aa36c45 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerCustomerIdInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Set id of Customer Admin is logged as. + */ +interface SetLoggedAsCustomerCustomerIdInterface +{ + /** + * Set id of Customer Admin is logged as. + * + * @param int $customerId + * @return void + */ + public function execute(int $customerId): void; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Model/IsLoginAsCustomerEnabledForCustomerChain.php b/app/code/Magento/LoginAsCustomerApi/Model/IsLoginAsCustomerEnabledForCustomerChain.php new file mode 100644 index 0000000000000..c852327743760 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Model/IsLoginAsCustomerEnabledForCustomerChain.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Model; + +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface; +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface; + +/** + * @inheritdoc + */ +class IsLoginAsCustomerEnabledForCustomerChain implements IsLoginAsCustomerEnabledForCustomerInterface +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory + */ + private $resultFactory; + + /** + * @var IsLoginAsCustomerEnabledForCustomerResultInterface[] + */ + private $resolvers; + + /** + * @param ConfigInterface $config + * @param IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory + * @param array $resolvers + */ + public function __construct( + ConfigInterface $config, + IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory, + array $resolvers = [] + ) { + $this->config = $config; + $this->resultFactory = $resultFactory; + $this->resolvers = $resolvers; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): IsLoginAsCustomerEnabledForCustomerResultInterface + { + $messages = [[]]; + /** @var IsLoginAsCustomerEnabledForCustomerInterface $resolver */ + foreach ($this->resolvers as $resolver) { + $resolverResult = $resolver->execute($customerId); + if (!$resolverResult->isEnabled()) { + $messages[] = $resolverResult->getMessages(); + } + } + + return $this->resultFactory->create(['messages' => array_merge(...$messages)]); + } +} diff --git a/app/code/Magento/LoginAsCustomerApi/etc/di.xml b/app/code/Magento/LoginAsCustomerApi/etc/di.xml new file mode 100644 index 0000000000000..18915f8f16267 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/etc/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface" + type="Magento\LoginAsCustomerApi\Model\IsLoginAsCustomerEnabledForCustomerChain"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/Api/ConfigInterface.php b/app/code/Magento/LoginAsCustomerAssistance/Api/ConfigInterface.php new file mode 100644 index 0000000000000..7cd54567d26d5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Api/ConfigInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Api; + +/** + * LoginAsCustomerAssistance config. + */ +interface ConfigInterface +{ + /** + * Get title for shopping assistance checkbox. + * + * @return string + */ + public function getShoppingAssistanceCheckboxTitle(): string; + + /** + * Get tooltip for shopping assistance checkbox. + * + * @return string + */ + public function getShoppingAssistanceCheckboxTooltip(): string; +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Api/IsAssistanceEnabledInterface.php b/app/code/Magento/LoginAsCustomerAssistance/Api/IsAssistanceEnabledInterface.php new file mode 100644 index 0000000000000..916d03477a5d3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Api/IsAssistanceEnabledInterface.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Api; + +/** + * Get 'assistance_allowed' attribute from Customer. + */ +interface IsAssistanceEnabledInterface +{ + /** + * Merchant assistance denied by customer status code. + */ + public const DENIED = 1; + + /** + * Merchant assistance allowed by customer status code. + */ + public const ALLOWED = 2; + + /** + * Get 'assistance_allowed' attribute from Customer by id. + * + * @param int $customerId + * @return bool + */ + public function execute(int $customerId): bool; +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Api/SetAssistanceInterface.php b/app/code/Magento/LoginAsCustomerAssistance/Api/SetAssistanceInterface.php new file mode 100644 index 0000000000000..ce8d2020341be --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Api/SetAssistanceInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Api; + +/** + * Set 'assistance_allowed' attribute to Customer. + */ +interface SetAssistanceInterface +{ + /** + * Set 'assistance_allowed' attribute to Customer by id. + * + * @param int $customerId + * @param bool $isEnabled + */ + public function execute(int $customerId, bool $isEnabled): void; +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Block/Adminhtml/NotAllowedPopup.php b/app/code/Magento/LoginAsCustomerAssistance/Block/Adminhtml/NotAllowedPopup.php new file mode 100644 index 0000000000000..b98ea203057b1 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Block/Adminhtml/NotAllowedPopup.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Block\Adminhtml; + +use Magento\Backend\Block\Template; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; + +/** + * Pop-up for Login as Customer button then Login as Customer is not allowed. + * + * @api + */ +class NotAllowedPopup extends Template +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var Json + */ + private $json; + + /** + * @param Template\Context $context + * @param ConfigInterface $config + * @param Json $json + * @param array $data + */ + public function __construct( + Template\Context $context, + ConfigInterface $config, + Json $json, + array $data = [] + ) { + parent::__construct($context, $data); + $this->config = $config; + $this->json = $json; + } + + /** + * @inheritdoc + */ + public function getJsLayout() + { + $layout = $this->json->unserialize(parent::getJsLayout()); + + $layout['components']['lac-not-allowed-popup']['title'] = __('Login as Customer not enabled'); + $layout['components']['lac-not-allowed-popup']['content'] = __( + 'The user has not enabled the "Allow remote shopping assistance" functionality. ' + . 'Contact the customer to discuss this user configuration.' + ); + + return $this->json->serialize($layout); + } + + /** + * @inheritdoc + */ + public function toHtml() + { + if (!$this->config->isEnabled()) { + return ''; + } + return parent::toHtml(); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/LICENSE.txt b/app/code/Magento/LoginAsCustomerAssistance/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/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/LoginAsCustomerAssistance/LICENSE_AFL.txt b/app/code/Magento/LoginAsCustomerAssistance/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/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/LoginAsCustomerAssistance/Model/Config.php b/app/code/Magento/LoginAsCustomerAssistance/Model/Config.php new file mode 100644 index 0000000000000..c2244fa3a799c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/Config.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\LoginAsCustomerAssistance\Api\ConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * @inheritdoc + */ +class Config implements ConfigInterface +{ + private const XML_PATH_SHOPPING_ASSISTANCE_CHECKBOX_TITLE + = 'login_as_customer/general/shopping_assistance_checkbox_title'; + private const XML_PATH_SHOPPING_ASSISTANCE_CHECKBOX_TOOLTIP + = 'login_as_customer/general/shopping_assistance_checkbox_tooltip'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * @inheritdoc + */ + public function getShoppingAssistanceCheckboxTitle(): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_SHOPPING_ASSISTANCE_CHECKBOX_TITLE, + ScopeInterface::SCOPE_WEBSITE + ); + } + + /** + * @inheritdoc + */ + public function getShoppingAssistanceCheckboxTooltip(): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_SHOPPING_ASSISTANCE_CHECKBOX_TOOLTIP, + ScopeInterface::SCOPE_WEBSITE + ); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/IsAssistanceEnabled.php b/app/code/Magento/LoginAsCustomerAssistance/Model/IsAssistanceEnabled.php new file mode 100644 index 0000000000000..da77c96164228 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/IsAssistanceEnabled.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerExtensionFactory; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface; +use Magento\LoginAsCustomerAssistance\Model\ResourceModel\GetLoginAsCustomerAssistanceAllowed; + +/** + * Check if customer allows Login as Customer assistance. + */ +class IsAssistanceEnabled implements IsAssistanceEnabledInterface +{ + /** + * @var array + */ + private $registry = []; + + /** + * @var GetLoginAsCustomerAssistanceAllowed + */ + private $getLoginAsCustomerAssistanceAllowed; + + /** + * @param GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + */ + public function __construct( + GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + ) { + $this->getLoginAsCustomerAssistanceAllowed = $getLoginAsCustomerAssistanceAllowed; + } + + /** + * Check if customer allows Login as Customer assistance by Customer id. + * + * @param int $customerId + * @return bool + */ + public function execute(int $customerId): bool + { + if (!isset($this->registry[$customerId])) { + $this->registry[$customerId] = $this->getLoginAsCustomerAssistanceAllowed->execute($customerId); + } + + return $this->registry[$customerId]; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/Processor/IsLoginAsCustomerAllowedResolver.php b/app/code/Magento/LoginAsCustomerAssistance/Model/Processor/IsLoginAsCustomerAllowedResolver.php new file mode 100644 index 0000000000000..966ea477b2394 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/Processor/IsLoginAsCustomerAllowedResolver.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model\Processor; + +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory; +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface; +use Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface; + +/** + * @inheritdoc + */ +class IsLoginAsCustomerAllowedResolver implements IsLoginAsCustomerEnabledForCustomerInterface +{ + /** + * @var IsAssistanceEnabledInterface + */ + private $isAssistanceEnabled; + + /** + * @var IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory + */ + private $resultFactory; + + /** + * @param IsAssistanceEnabledInterface $isAssistanceEnabled + * @param IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory + */ + public function __construct( + IsAssistanceEnabledInterface $isAssistanceEnabled, + IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory + ) { + $this->isAssistanceEnabled = $isAssistanceEnabled; + $this->resultFactory = $resultFactory; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): IsLoginAsCustomerEnabledForCustomerResultInterface + { + $messages = []; + if (!$this->isAssistanceEnabled->execute($customerId)) { + $messages[] = __('Login as Customer assistance is disabled for this Customer.'); + } + + return $this->resultFactory->create(['messages' => $messages]); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/DeleteLoginAsCustomerAssistanceAllowed.php b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/DeleteLoginAsCustomerAssistanceAllowed.php new file mode 100644 index 0000000000000..360f2e2799282 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/DeleteLoginAsCustomerAssistanceAllowed.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; + +/** + * Delete Login as Customer assistance allowed record. + */ +class DeleteLoginAsCustomerAssistanceAllowed +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Delete Login as Customer assistance allowed record by Customer id. + * + * @param int $customerId + * @return void + */ + public function execute(int $customerId): void + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('login_as_customer_assistance_allowed'); + + $connection->delete( + $tableName, + [ + 'customer_id = ?' => $customerId + ] + ); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/GetLoginAsCustomerAssistanceAllowed.php b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/GetLoginAsCustomerAssistanceAllowed.php new file mode 100644 index 0000000000000..412fd86351988 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/GetLoginAsCustomerAssistanceAllowed.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; + +/** + * Get Login as Customer assistance allowed record. + */ +class GetLoginAsCustomerAssistanceAllowed +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Get Login as Customer assistance allowed record by Customer id. + * + * @param int $customerId + * @return bool + */ + public function execute(int $customerId): bool + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('login_as_customer_assistance_allowed'); + + $select = $connection->select() + ->from( + $tableName + ) + ->where('customer_id = ?', $customerId); + + return !!$connection->fetchOne($select); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/SaveLoginAsCustomerAssistanceAllowed.php b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/SaveLoginAsCustomerAssistanceAllowed.php new file mode 100644 index 0000000000000..c3b396bbe332d --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/SaveLoginAsCustomerAssistanceAllowed.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; + +/** + * Save Login as Customer assistance allowed record. + */ +class SaveLoginAsCustomerAssistanceAllowed +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Save Login as Customer assistance allowed record by Customer id. + * + * @param int $customerId + * @return void + */ + public function execute(int $customerId): void + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('login_as_customer_assistance_allowed'); + + $connection->insertOnDuplicate( + $tableName, + [ + 'customer_id' => $customerId + ] + ); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/SetAssistance.php b/app/code/Magento/LoginAsCustomerAssistance/Model/SetAssistance.php new file mode 100644 index 0000000000000..9131599d9cba0 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/SetAssistance.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerExtensionFactory; +use Magento\LoginAsCustomerAssistance\Api\SetAssistanceInterface; +use Magento\LoginAsCustomerAssistance\Model\ResourceModel\DeleteLoginAsCustomerAssistanceAllowed; +use Magento\LoginAsCustomerAssistance\Model\ResourceModel\SaveLoginAsCustomerAssistanceAllowed; + +/** + * @inheritdoc + */ +class SetAssistance implements SetAssistanceInterface +{ + /** + * @var array + */ + private $registry = []; + + /** + * @var CustomerExtensionFactory + */ + private $customerExtensionFactory; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var DeleteLoginAsCustomerAssistanceAllowed + */ + private $deleteLoginAsCustomerAssistanceAllowed; + + /** + * @var SaveLoginAsCustomerAssistanceAllowed + */ + private $saveLoginAsCustomerAssistanceAllowed; + + /** + * @param CustomerExtensionFactory $customerExtensionFactory + * @param CustomerRepositoryInterface $customerRepository + * @param DeleteLoginAsCustomerAssistanceAllowed $deleteLoginAsCustomerAssistanceAllowed + * @param SaveLoginAsCustomerAssistanceAllowed $saveLoginAsCustomerAssistanceAllowed + */ + public function __construct( + CustomerExtensionFactory $customerExtensionFactory, + CustomerRepositoryInterface $customerRepository, + DeleteLoginAsCustomerAssistanceAllowed $deleteLoginAsCustomerAssistanceAllowed, + SaveLoginAsCustomerAssistanceAllowed $saveLoginAsCustomerAssistanceAllowed + ) { + $this->customerExtensionFactory = $customerExtensionFactory; + $this->customerRepository = $customerRepository; + $this->deleteLoginAsCustomerAssistanceAllowed = $deleteLoginAsCustomerAssistanceAllowed; + $this->saveLoginAsCustomerAssistanceAllowed = $saveLoginAsCustomerAssistanceAllowed; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId, bool $isEnabled): void + { + if ($this->isUpdateRequired($customerId, $isEnabled)) { + if ($isEnabled) { + $this->saveLoginAsCustomerAssistanceAllowed->execute($customerId); + } else { + $this->deleteLoginAsCustomerAssistanceAllowed->execute($customerId); + } + $this->updateRegistry($customerId, $isEnabled); + } + } + + /** + * Check if 'assistance_allowed' cached value differs from actual. + * + * @param int $customerId + * @param bool $isEnabled + * @return bool + */ + private function isUpdateRequired(int $customerId, bool $isEnabled): bool + { + return !isset($this->registry[$customerId]) || $this->registry[$customerId] !== $isEnabled; + } + + /** + * Update 'assistance_allowed' cached value. + * + * @param int $customerId + * @param bool $isEnabled + */ + private function updateRegistry(int $customerId, bool $isEnabled): void + { + $this->registry[$customerId] = $isEnabled; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerDataValidatePlugin.php b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerDataValidatePlugin.php new file mode 100644 index 0000000000000..4bbf691e2b58e --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerDataValidatePlugin.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Plugin; + +use Magento\Customer\Model\Metadata\Form; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Validator\Exception as ValidatorException; +use Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface; +use Magento\LoginAsCustomerAssistance\Model\ResourceModel\GetLoginAsCustomerAssistanceAllowed; + +/** + * Check if User have permission to change Customers Opt-In preference. + */ +class CustomerDataValidatePlugin +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var GetLoginAsCustomerAssistanceAllowed + */ + private $getLoginAsCustomerAssistanceAllowed; + + /** + * @param AuthorizationInterface $authorization + * @param GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + */ + public function __construct( + AuthorizationInterface $authorization, + GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + ) { + $this->authorization = $authorization; + $this->getLoginAsCustomerAssistanceAllowed = $getLoginAsCustomerAssistanceAllowed; + } + + /** + * Check if User have permission to change Customers Opt-In preference. + * + * @param Form $subject + * @param RequestInterface $request + * @param null|string $scope + * @param bool $scopeOnly + * @throws ValidatorException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExtractData( + Form $subject, + RequestInterface $request, + $scope = null, + $scopeOnly = true + ): void { + if ($this->isSetAssistanceAllowedParam($request) + && !$this->authorization->isAllowed('Magento_LoginAsCustomer::allow_shopping_assistance') + ) { + $customerId = $request->getParam('customer_id'); + $assistanceAllowedParam = + (int)$request->getParam('customer')['extension_attributes']['assistance_allowed']; + $assistanceAllowed = $this->getLoginAsCustomerAssistanceAllowed->execute((int)$customerId); + $assistanceAllowedStatus = $this->resolveStatus($assistanceAllowed); + if ($this->isAssistanceAllowedChangeImportant($assistanceAllowedStatus, $assistanceAllowedParam)) { + $errorMessages = [ + MessageInterface::TYPE_ERROR => [ + __( + 'You have no permission to change Opt-In preference.' + ), + ], + ]; + + throw new ValidatorException(null, null, $errorMessages); + } + } + } + + /** + * Check if assistance_allowed param is set. + * + * @param RequestInterface $request + * @return bool + */ + private function isSetAssistanceAllowedParam(RequestInterface $request): bool + { + return is_array($request->getParam('customer')) + && isset($request->getParam('customer')['extension_attributes']) + && isset($request->getParam('customer')['extension_attributes']['assistance_allowed']); + } + + /** + * Check if change of assistance_allowed attribute is important. + * + * E. g. if assistance_allowed is going to be disabled while now it's enabled + * or if it's going to be enabled while now it is disabled or not set at all. + * + * @param int $assistanceAllowed + * @param int $assistanceAllowedParam + * @return bool + */ + private function isAssistanceAllowedChangeImportant(int $assistanceAllowed, int $assistanceAllowedParam): bool + { + $result = false; + if (($assistanceAllowedParam === IsAssistanceEnabledInterface::DENIED + && $assistanceAllowed === IsAssistanceEnabledInterface::ALLOWED) + || + ($assistanceAllowedParam === IsAssistanceEnabledInterface::ALLOWED + && $assistanceAllowed !== IsAssistanceEnabledInterface::ALLOWED)) { + $result = true; + } + + return $result; + } + + /** + * Get integer status value from boolean. + * + * @param bool $assistanceAllowed + * @return int + */ + private function resolveStatus(bool $assistanceAllowed): int + { + return $assistanceAllowed ? IsAssistanceEnabledInterface::ALLOWED : IsAssistanceEnabledInterface::DENIED; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerExtractorPlugin.php b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerExtractorPlugin.php new file mode 100644 index 0000000000000..156d84bcae9bd --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerExtractorPlugin.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Plugin; + +use Magento\Customer\Api\Data\CustomerExtensionFactory; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\CustomerExtractor; +use Magento\Framework\App\RequestInterface; + +/** + * Plugin for Magento\Customer\Model\CustomerExtractor. + */ +class CustomerExtractorPlugin +{ + /** + * @var CustomerExtensionFactory + */ + private $customerExtensionFactory; + + /** + * @param CustomerExtensionFactory $customerExtensionFactory + */ + public function __construct( + CustomerExtensionFactory $customerExtensionFactory + ) { + $this->customerExtensionFactory = $customerExtensionFactory; + } + + /** + * Add assistance_allowed extension attribute value to Customer instance. + * + * @param CustomerExtractor $subject + * @param CustomerInterface $customer + * @param string $formCode + * @param RequestInterface $request + * @param array $attributeValues + * @return CustomerInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExtract( + CustomerExtractor $subject, + CustomerInterface $customer, + string $formCode, + RequestInterface $request, + array $attributeValues = [] + ) { + $assistanceAllowedStatus = $request->getParam('assistance_allowed'); + if (!empty($assistanceAllowedStatus)) { + $extensionAttributes = $customer->getExtensionAttributes(); + if (null === $extensionAttributes) { + $extensionAttributes = $this->customerExtensionFactory->create(); + } + $extensionAttributes->setAssistanceAllowed((int)$assistanceAllowedStatus); + $customer->setExtensionAttributes($extensionAttributes); + } + + return $customer; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerPlugin.php b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerPlugin.php new file mode 100644 index 0000000000000..0bc22bf5d8869 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerPlugin.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Plugin; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\LoginAsCustomerAssistance\Api\SetAssistanceInterface; +use Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled; + +/** + * Plugin for Customer assistance_allowed extension attribute. + */ +class CustomerPlugin +{ + /** + * @var SetAssistanceInterface + */ + private $setAssistance; + + /** + * @param SetAssistanceInterface $setAssistance + */ + public function __construct( + SetAssistanceInterface $setAssistance + ) { + $this->setAssistance = $setAssistance; + } + + /** + * Save assistance_allowed extension attribute for Customer instance. + * + * @param CustomerRepositoryInterface $subject + * @param CustomerInterface $result + * @param CustomerInterface $customer + * @return CustomerInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + CustomerRepositoryInterface $subject, + CustomerInterface $result, + CustomerInterface $customer + ): CustomerInterface { + $customerId = (int)$result->getId(); + $customerExtensionAttributes = $customer->getExtensionAttributes(); + if ($customerExtensionAttributes && $customerExtensionAttributes->getAssistanceAllowed()) { + $isEnabled = (int)$customerExtensionAttributes->getAssistanceAllowed() === IsAssistanceEnabled::ALLOWED; + $this->setAssistance->execute($customerId, $isEnabled); + } + + return $result; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Plugin/DataProviderWithDefaultAddressesPlugin.php b/app/code/Magento/LoginAsCustomerAssistance/Plugin/DataProviderWithDefaultAddressesPlugin.php new file mode 100644 index 0000000000000..6653340285d32 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Plugin/DataProviderWithDefaultAddressesPlugin.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Plugin; + +use Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses; +use Magento\Framework\AuthorizationInterface; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface; +use Magento\LoginAsCustomerAssistance\Model\ResourceModel\GetLoginAsCustomerAssistanceAllowed; + +/** + * Plugin for managing assistance_allowed extension attribute in Customer form Data Provider. + */ +class DataProviderWithDefaultAddressesPlugin +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var GetLoginAsCustomerAssistanceAllowed + */ + private $getLoginAsCustomerAssistanceAllowed; + + /** + * @param AuthorizationInterface $authorization + * @param ConfigInterface $config + * @param GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + */ + public function __construct( + AuthorizationInterface $authorization, + ConfigInterface $config, + GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + ) { + $this->authorization = $authorization; + $this->config = $config; + $this->getLoginAsCustomerAssistanceAllowed = $getLoginAsCustomerAssistanceAllowed; + } + + /** + * Add assistance_allowed extension attribute data to Customer form Data Provider. + * + * @param DataProviderWithDefaultAddresses $subject + * @param array $result + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetData( + DataProviderWithDefaultAddresses $subject, + array $result + ): array { + $isAssistanceAllowed = []; + + foreach ($result as $id => $entityData) { + if ($id) { + $assistanceAllowedStatus = $this->resolveStatus( + $this->getLoginAsCustomerAssistanceAllowed->execute((int)$entityData['customer_id']) + ); + $isAssistanceAllowed[$id]['customer']['extension_attributes']['assistance_allowed'] = + (string)$assistanceAllowedStatus; + } + } + + return array_replace_recursive($result, $isAssistanceAllowed); + } + + /** + * Modify assistance_allowed extension attribute metadata for Customer form Data Provider. + * + * @param DataProviderWithDefaultAddresses $subject + * @param array $result + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetMeta( + DataProviderWithDefaultAddresses $subject, + array $result + ): array { + if (!$this->config->isEnabled()) { + $assistanceAllowedConfig = ['visible' => false]; + } elseif (!$this->authorization->isAllowed('Magento_LoginAsCustomer::allow_shopping_assistance')) { + $assistanceAllowedConfig = [ + 'disabled' => true, + 'notice' => __('You have no permission to change Opt-In preference.'), + ]; + } else { + $assistanceAllowedConfig = []; + } + + $config = [ + 'customer' => [ + 'children' => [ + 'extension_attributes.assistance_allowed' => [ + 'arguments' => [ + 'data' => [ + 'config' => $assistanceAllowedConfig, + ], + ], + ], + ], + ], + ]; + + return array_replace_recursive($result, $config); + } + + /** + * Get integer status value from boolean. + * + * @param bool $assistanceAllowed + * @return int + */ + private function resolveStatus(bool $assistanceAllowed): int + { + return $assistanceAllowed ? IsAssistanceEnabledInterface::ALLOWED : IsAssistanceEnabledInterface::DENIED; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Plugin/LoginAsCustomerButtonDataProviderPlugin.php b/app/code/Magento/LoginAsCustomerAssistance/Plugin/LoginAsCustomerButtonDataProviderPlugin.php new file mode 100644 index 0000000000000..45a3eb512e7f8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Plugin/LoginAsCustomerButtonDataProviderPlugin.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Plugin; + +use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider; +use Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled; + +/** + * Change Login as Customer button behavior if Customer has not granted permission. + */ +class LoginAsCustomerButtonDataProviderPlugin +{ + /** + * @var IsAssistanceEnabled + */ + private $isAssistanceEnabled; + + /** + * @param IsAssistanceEnabled $isAssistanceEnabled + */ + public function __construct( + IsAssistanceEnabled $isAssistanceEnabled + ) { + $this->isAssistanceEnabled = $isAssistanceEnabled; + } + + /** + * Change Login as Customer button behavior if Customer has not granted permission. + * + * @param DataProvider $subject + * @param array $result + * @param int $customerId + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetData(DataProvider $subject, array $result, int $customerId): array + { + if (isset($result['on_click']) && !$this->isAssistanceEnabled->execute($customerId)) { + $result['on_click'] = 'window.lacNotAllowedPopup()'; + } + + return $result; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/README.md b/app/code/Magento/LoginAsCustomerAssistance/README.md new file mode 100644 index 0000000000000..b43dd6c8db43a --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/README.md @@ -0,0 +1,3 @@ +# Magento_LoginAsCustomerAssistance module + +The Magento_LoginAsCustomerAssistance module provides possibility to enable/disable LoginAsCustomer functionality per Customer. diff --git a/app/code/Magento/LoginAsCustomerAssistance/ViewModel/ShoppingAssistanceViewModel.php b/app/code/Magento/LoginAsCustomerAssistance/ViewModel/ShoppingAssistanceViewModel.php new file mode 100644 index 0000000000000..f7e224efaa19a --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/ViewModel/ShoppingAssistanceViewModel.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\ViewModel; + +use Magento\Customer\Model\Session; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerAssistance\Api\ConfigInterface as AssistanceConfigInterface; +use Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled; + +/** + * View model for Login as Customer Shopping Assistance block. + */ +class ShoppingAssistanceViewModel implements ArgumentInterface +{ + /** + * @var AssistanceConfigInterface + */ + private $assistanceConfig; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var IsAssistanceEnabled + */ + private $isAssistanceEnabled; + + /** + * @var Session + */ + private $session; + + /** + * @param AssistanceConfigInterface $assistanceConfig + * @param ConfigInterface $config + * @param IsAssistanceEnabled $isAssistanceEnabled + * @param Session $session + */ + public function __construct( + AssistanceConfigInterface $assistanceConfig, + ConfigInterface $config, + IsAssistanceEnabled $isAssistanceEnabled, + Session $session + ) { + $this->assistanceConfig = $assistanceConfig; + $this->config = $config; + $this->isAssistanceEnabled = $isAssistanceEnabled; + $this->session = $session; + } + + /** + * Is Login as Customer functionality enabled. + * + * @return bool + */ + public function isLoginAsCustomerEnabled(): bool + { + return $this->config->isEnabled(); + } + + /** + * Is merchant assistance allowed by Customer. + * + * @return bool + */ + public function isAssistanceAllowed(): bool + { + $customerId = $this->session->getId(); + + return $customerId && $this->isAssistanceEnabled->execute((int)$customerId); + } + + /** + * Get shopping assistance checkbox title from config. + * + * @return string + */ + public function getAssistanceCheckboxTitle() + { + return $this->assistanceConfig->getShoppingAssistanceCheckboxTitle(); + } + + /** + * Get shopping assistance checkbox tooltip text from config. + * + * @return string + */ + public function getAssistanceCheckboxTooltip() + { + return $this->assistanceConfig->getShoppingAssistanceCheckboxTooltip(); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/composer.json b/app/code/Magento/LoginAsCustomerAssistance/composer.json new file mode 100644 index 0000000000000..a02852533b950 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-login-as-customer-assistance", + "description": "", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-customer": "*", + "magento/module-store": "*", + "magento/module-login-as-customer": "*", + "magento/module-login-as-customer-api": "*" + }, + "suggest": { + "magento/module-login-as-customer-admin-ui": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ "registration.php" ], + "psr-4": { + "Magento\\LoginAsCustomerAssistance\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/acl.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/acl.xml new file mode 100644 index 0000000000000..2c16b5a9125df --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/acl.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Customer::customer"> + <resource id="Magento_LoginAsCustomer::login"> + <resource id="Magento_LoginAsCustomer::allow_shopping_assistance" title="Allow remote shopping assistance" sortOrder="20" /> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/di.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..3071e3038ffcc --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/di.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses"> + <plugin name="login_as_customer_customer_data_provider_plugin" + type="Magento\LoginAsCustomerAssistance\Plugin\DataProviderWithDefaultAddressesPlugin"/> + </type> + <type name="Magento\Customer\Model\Metadata\Form"> + <plugin name="login_as_customer_customer_data_validate_plugin" + type="Magento\LoginAsCustomerAssistance\Plugin\CustomerDataValidatePlugin"/> + </type> + <type name="Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider"> + <plugin name="login_as_customer_button_data_provider_plugin" + type="Magento\LoginAsCustomerAssistance\Plugin\LoginAsCustomerButtonDataProviderPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/system.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..bfdc5519937da --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/system.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="login_as_customer" showInWebsite="1"> + <group id="general" showInWebsite="1"> + <field id="shopping_assistance_checkbox_title" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>Title for Login as Customer opt-in checkbox</label> + </field> + <field id="shopping_assistance_checkbox_tooltip" translate="label" type="textarea" sortOrder="60" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>Login as Customer checkbox tooltip</label> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/config.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/config.xml new file mode 100644 index 0000000000000..9b74e4734f00e --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/config.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_Store:etc/config.xsd"> + <default> + <login_as_customer> + <general> + <shopping_assistance_checkbox_title>Allow remote shopping assistance</shopping_assistance_checkbox_title> + <shopping_assistance_checkbox_tooltip>This allows merchants to "see what you see" and take actions on your behalf in order to provide better assistance.</shopping_assistance_checkbox_tooltip> + </general> + </login_as_customer> + </default> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema.xml new file mode 100644 index 0000000000000..deaecc2bfb777 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="login_as_customer_assistance_allowed" resource="default" engine="innodb" comment="Magento Login as Customer Assistance Allowed Table"> + <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" comment="Customer ID"/> + <constraint xsi:type="foreign" referenceId="LOGIN_AS_CUSTOMER_ASSISTANCE_ALLOWED_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + table="login_as_customer_assistance_allowed" column="customer_id" referenceTable="customer_entity" + referenceColumn="entity_id" onDelete="CASCADE"/> + <constraint xsi:type="unique" referenceId="LOGIN_AS_CUSTOMER_ASSISTANCE_ALLOWED_CUSTOMER_ID"> + <column name="customer_id"/> + </constraint> + </table> +</schema> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema_whitelist.json b/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..2c8aa79f3c7b1 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema_whitelist.json @@ -0,0 +1,11 @@ +{ + "login_as_customer_assistance_allowed": { + "column": { + "customer_id": true + }, + "constraint": { + "LOGIN_AS_CSTR_ASSISTANCE_ALLOWED_CSTR_ID_CSTR_ENTT_ENTT_ID": true, + "LOGIN_AS_CUSTOMER_ASSISTANCE_ALLOWED_CUSTOMER_ID": true + } + } +} \ No newline at end of file diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/di.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/di.xml new file mode 100755 index 0000000000000..0cbf3b4d10da6 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/di.xml @@ -0,0 +1,29 @@ +<?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\LoginAsCustomerAssistance\Api\ConfigInterface" + type="Magento\LoginAsCustomerAssistance\Model\Config"/> + <preference for="Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface" + type="Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled"/> + <preference for="Magento\LoginAsCustomerAssistance\Api\SetAssistanceInterface" + type="Magento\LoginAsCustomerAssistance\Model\SetAssistance"/> + <type name="Magento\LoginAsCustomerApi\Model\IsLoginAsCustomerEnabledForCustomerChain"> + <arguments> + <argument name="resolvers" xsi:type="array"> + <item name="is_allowed" xsi:type="object"> + Magento\LoginAsCustomerAssistance\Model\Processor\IsLoginAsCustomerAllowedResolver + </item> + </argument> + </arguments> + </type> + <type name="Magento\Customer\Api\CustomerRepositoryInterface"> + <plugin name="login_as_customer_customer_repository_plugin" + type="Magento\LoginAsCustomerAssistance\Plugin\CustomerPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/extension_attributes.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/extension_attributes.xml new file mode 100644 index 0000000000000..ff47820faadaa --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/extension_attributes.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:Api/etc/extension_attributes.xsd"> + <extension_attributes for="Magento\Customer\Api\Data\CustomerInterface"> + <attribute code="assistance_allowed" type="integer"/> + </extension_attributes> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/frontend/di.xml new file mode 100644 index 0000000000000..bdb2f82eddd60 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/frontend/di.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Customer\Model\CustomerExtractor"> + <plugin name="add_assistance_allowed_to_customer_data" + type="Magento\LoginAsCustomerAssistance\Plugin\CustomerExtractorPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/module.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/module.xml new file mode 100644 index 0000000000000..f443691bcf126 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_LoginAsCustomerAssistance"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/registration.php b/app/code/Magento/LoginAsCustomerAssistance/registration.php new file mode 100644 index 0000000000000..c2be7af4bd396 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Magento_LoginAsCustomerAssistance', + __DIR__ +); diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/layout/loginascustomer_confirmation_popup.xml b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/layout/loginascustomer_confirmation_popup.xml new file mode 100644 index 0000000000000..ef2e81cd37804 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/layout/loginascustomer_confirmation_popup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <referenceContainer name="content"> + <block class="Magento\LoginAsCustomerAssistance\Block\Adminhtml\NotAllowedPopup" name="lac.not.allowed.popup" template="Magento_LoginAsCustomerAssistance::not-allowed-popup.phtml"> + <arguments> + <argument name="jsLayout" xsi:type="array"> + <item name="components" xsi:type="array"> + <item name="lac-not-allowed-popup" xsi:type="array"> + <item name="component" xsi:type="string">Magento_LoginAsCustomerAssistance/js/not-allowed-popup</item> + </item> + </item> + </argument> + </arguments> + </block> + </referenceContainer> +</layout> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/templates/not-allowed-popup.phtml b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/templates/not-allowed-popup.phtml new file mode 100644 index 0000000000000..42e19f9db4931 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/templates/not-allowed-popup.phtml @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** @var \Magento\LoginAsCustomerAssistance\Block\Adminhtml\NotAllowedPopup $block */ +?> + +<script type="text/x-magento-init"> + { + "*": { + "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout();?> + } + } + </script> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/ui_component/customer_form.xml b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/ui_component/customer_form.xml new file mode 100644 index 0000000000000..b677becd66064 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/ui_component/customer_form.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <fieldset name="customer"> + <field name="extension_attributes.assistance_allowed" sortOrder="85" formElement="checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="source" xsi:type="string">customer</item> + </item> + </argument> + <settings> + <dataType>boolean</dataType> + <label translate="true">Allow remote shopping assistance</label> + </settings> + <formElements> + <checkbox> + <settings> + <valueMap> + <map name="false" xsi:type="number">1</map> + <map name="true" xsi:type="number">2</map> + </valueMap> + <prefer>toggle</prefer> + </settings> + </checkbox> + </formElements> + </field> + </fieldset> +</form> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/web/js/not-allowed-popup.js b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/web/js/not-allowed-popup.js new file mode 100644 index 0000000000000..59d8dd4a7ed49 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/web/js/not-allowed-popup.js @@ -0,0 +1,55 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'Magento_Ui/js/modal/confirm', + 'mage/translate' +], function (Component, confirm, $t) { + + 'use strict'; + + return Component.extend({ + /** + * Initialize Component + */ + initialize: function () { + var self = this, + content; + + this._super(); + + content = '<div class="message message-warning">' + self.content + '</div>'; + + /** + * Not Allowed popup + * + * @returns {Boolean} + */ + window.lacNotAllowedPopup = function () { + confirm({ + title: self.title, + content: content, + modalClass: 'confirm lac-confirm', + buttons: [ + { + text: $t('Cancel'), + class: 'action-secondary action-dismiss', + + /** + * Click handler. + */ + click: function (event) { + this.closeModal(event); + } + } + ] + }); + + return false; + }; + } + }); +}); diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_create.xml b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_create.xml new file mode 100644 index 0000000000000..121d20395e295 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_create.xml @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="customer_form_register"> + <container name="fieldset.create.info.additional" as="fieldset_create_info_additional"/> + </referenceBlock> + <referenceContainer name="fieldset.create.info.additional"> + <block name="login_as_customer_opt_in_create" + template="Magento_LoginAsCustomerAssistance::shopping-assistance.phtml"> + <arguments> + <argument name="view_model" xsi:type="object"> + Magento\LoginAsCustomerAssistance\ViewModel\ShoppingAssistanceViewModel + </argument> + </arguments> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_edit.xml b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_edit.xml new file mode 100644 index 0000000000000..15b52cb6cf784 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_edit.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="customer_edit"> + <container name="fieldset.edit.info.additional" as="fieldset_edit_info_additional"/> + </referenceBlock> + <referenceContainer name="fieldset.edit.info.additional"> + <block name="login_as_customer_opt_in_edit" + template="Magento_LoginAsCustomerAssistance::shopping-assistance.phtml"> + <arguments> + <argument name="view_model" xsi:type="object"> + Magento\LoginAsCustomerAssistance\ViewModel\ShoppingAssistanceViewModel + </argument> + </arguments> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml new file mode 100644 index 0000000000000..7765975863485 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Escaper; +use Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface; +use Magento\LoginAsCustomerAssistance\ViewModel\ShoppingAssistanceViewModel; + +/** @var Escaper $escaper */ +/** @var ShoppingAssistanceViewModel $viewModel */ +$viewModel = $block->getViewModel(); +?> + +<script type="text/x-magento-init"> +{ + ".form-create-account, .form-edit-account": { + "Magento_LoginAsCustomerAssistance/js/opt-in": { + "allowAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::ALLOWED ?>", + "denyAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::DENIED ?>" + } + } +} +</script> + +<?php if ($viewModel->isLoginAsCustomerEnabled()): ?> + <div class="field choice"> + <input type="checkbox" + name="assistance_allowed_checkbox" + title="<?= $escaper->escapeHtmlAttr(__($viewModel->getAssistanceCheckboxTitle())) ?>" + value="1" + id="assistance_allowed_checkbox" + <?php if ($viewModel->isAssistanceAllowed()): ?>checked="checked"<?php endif; ?> + class="checkbox"> + <label for="assistance_allowed_checkbox" class="label"> + <span><?= $escaper->escapeHtmlAttr(__($viewModel->getAssistanceCheckboxTitle())) ?></span> + </label> + + <input type="hidden" name="assistance_allowed" value=""/> + + <div class="field-tooltip toggle"> + <span id="tooltip-label" class="label"><span>Tooltip</span></span> + <span id="tooltip" class="field-tooltip-action action-help" tabindex="0" data-toggle="dropdown" + data-bind="mageInit: {'dropdown':{'activeClass': '_active', 'parent': '.field-tooltip.toggle'}}" + aria-labelledby="tooltip-label" aria-haspopup="true" aria-expanded="false" role="button"> + </span> + <div class="field-tooltip-content" data-target="dropdown" + aria-hidden="true"> + <?= $escaper->escapeHtmlAttr(__($viewModel->getAssistanceCheckboxTooltip())) ?> + </div> + </div> + </div> +<?php endif ?> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/web/js/opt-in.js b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/web/js/opt-in.js new file mode 100644 index 0000000000000..d225d298b7771 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/web/js/opt-in.js @@ -0,0 +1,18 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery' +], function ($) { + 'use strict'; + + return function (config, element) { + $(element).on('submit', function () { + this.elements['assistance_allowed'].value = + this.elements['assistance_allowed_checkbox'].checked ? + config.allowAccess : config.denyAccess; + }); + }; +}); diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php b/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php index 6303989c0c667..140f31e3467f1 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php +++ b/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php @@ -9,7 +9,9 @@ use Magento\Customer\CustomerData\SectionSourceInterface; use Magento\Customer\Model\Session; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -29,16 +31,25 @@ class LoginAsCustomerUi implements SectionSourceInterface */ private $storeManager; + /** + * @var GetLoggedAsCustomerAdminIdInterface + */ + private $getLoggedAsCustomerAdminId; + /** * @param Session $customerSession * @param StoreManagerInterface $storeManager + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId */ public function __construct( Session $customerSession, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + ?GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId = null ) { $this->customerSession = $customerSession; $this->storeManager = $storeManager; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId + ?? ObjectManager::getInstance()->get(GetLoggedAsCustomerAdminIdInterface::class); } /** @@ -49,12 +60,14 @@ public function __construct( */ public function getSectionData(): array { - if (!$this->customerSession->getCustomerId()) { + $adminId = $this->getLoggedAsCustomerAdminId->execute(); + + if (!$adminId || !$this->customerSession->getCustomerId()) { return []; } return [ - 'adminUserId' => $this->customerSession->getLoggedAsCustomerAdmindId(), + 'adminUserId' => $adminId, 'websiteName' => $this->storeManager->getWebsite()->getName() ]; } diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php b/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php index b68e871c5f955..7c0682440b4dc 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php +++ b/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php @@ -9,6 +9,7 @@ use Magento\Customer\Model\Session; use Magento\Framework\App\ActionInterface; use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerSessionActiveInterface; /** @@ -31,19 +32,27 @@ class InvalidateExpiredSessionPlugin */ private $isLoginAsCustomerSessionActive; + /** + * @var GetLoggedAsCustomerAdminIdInterface + */ + private $getLoggedAsCustomerAdminId; + /** * @param ConfigInterface $config * @param Session $session * @param IsLoginAsCustomerSessionActiveInterface $isLoginAsCustomerSessionActive + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId */ public function __construct( ConfigInterface $config, Session $session, - IsLoginAsCustomerSessionActiveInterface $isLoginAsCustomerSessionActive + IsLoginAsCustomerSessionActiveInterface $isLoginAsCustomerSessionActive, + GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId ) { $this->session = $session; $this->isLoginAsCustomerSessionActive = $isLoginAsCustomerSessionActive; $this->config = $config; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId; } /** @@ -56,13 +65,17 @@ public function __construct( */ public function beforeExecute(ActionInterface $subject) { - if ($this->config->isEnabled()) { - $adminId = (int)$this->session->getLoggedAsCustomerAdmindId(); - $customerId = (int)$this->session->getCustomerId(); - if ($adminId && $customerId) { - if (!$this->isLoginAsCustomerSessionActive->execute($customerId, $adminId)) { - $this->session->destroy(); - } + if (!$this->config->isEnabled()) { + return; + } + + $adminId = $this->getLoggedAsCustomerAdminId->execute(); + $customerId = (int)$this->session->getCustomerId(); + if ($adminId && $customerId) { + if (!$this->isLoginAsCustomerSessionActive->execute($customerId, $adminId)) { + $this->session->clearStorage(); + $this->session->expireSessionCookie(); + $this->session->regenerateId(); } } } diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/KeepLoginAsCustomerSessionDataPlugin.php b/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/KeepLoginAsCustomerSessionDataPlugin.php new file mode 100644 index 0000000000000..9519f3a54077b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/KeepLoginAsCustomerSessionDataPlugin.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\LoginAsCustomerFrontendUi\Plugin; + +use Magento\Framework\Session\SessionManagerInterface; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; +use Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerAdminIdInterface; + +/** + * Keep adminId in customer session if session data is cleared. + */ +class KeepLoginAsCustomerSessionDataPlugin +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var GetLoggedAsCustomerAdminIdInterface + */ + private $getLoggedAsCustomerAdminId; + + /** + * @var SetLoggedAsCustomerAdminIdInterface + */ + private $setLoggedAsCustomerAdminId; + + /** + * @param ConfigInterface $config + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId + * @param SetLoggedAsCustomerAdminIdInterface $setLoggedAsCustomerAdminId + */ + public function __construct( + ConfigInterface $config, + GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId, + SetLoggedAsCustomerAdminIdInterface $setLoggedAsCustomerAdminId + ) { + $this->config = $config; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId; + $this->setLoggedAsCustomerAdminId = $setLoggedAsCustomerAdminId; + } + + /** + * Keep adminId in customer session if session data is cleared. + * + * @param SessionManagerInterface $subject + * @param \Closure $proceed + * @return SessionManagerInterface + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundClearStorage( + SessionManagerInterface $subject, + \Closure $proceed + ): SessionManagerInterface { + $enabled = $this->config->isEnabled(); + $adminId = $enabled ? $this->getLoggedAsCustomerAdminId->execute() : null; + $result = $proceed(); + if ($enabled && $adminId) { + $this->setLoggedAsCustomerAdminId->execute($adminId); + } + + return $result; + } +} diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php b/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php index 7d8738d06f54f..357ede238585b 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php +++ b/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php @@ -8,7 +8,10 @@ namespace Magento\LoginAsCustomerFrontendUi\ViewModel; use Magento\Customer\Model\Context; +use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Framework\App\ObjectManager; use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; /** * View model to get extension configuration in the template @@ -21,20 +24,29 @@ class Configuration implements \Magento\Framework\View\Element\Block\ArgumentInt private $config; /** - * @var \Magento\Framework\App\Http\Context + * @var HttpContext */ private $httpContext; + /** + * @var GetLoggedAsCustomerAdminIdInterface + */ + private $getLoggedAsCustomerAdminId; + /** * @param ConfigInterface $config - * @param \Magento\Framework\App\Http\Context $httpContext + * @param HttpContext $httpContext + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId */ public function __construct( ConfigInterface $config, - \Magento\Framework\App\Http\Context $httpContext + HttpContext $httpContext, + ?GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId = null ) { $this->config = $config; $this->httpContext = $httpContext; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId + ?? ObjectManager::getInstance()->get(GetLoggedAsCustomerAdminIdInterface::class); } /** @@ -44,7 +56,7 @@ public function __construct( */ public function isEnabled(): bool { - return $this->config->isEnabled() && $this->isLoggedIn(); + return $this->config->isEnabled() && $this->isLoggedIn() && $this->getLoggedAsCustomerAdminId->execute(); } /** diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml index 2204402b7dd30..bff511b6bb6e6 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml +++ b/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml @@ -17,4 +17,8 @@ <plugin name="invalidate_expired_session_plugin" type="Magento\LoginAsCustomerFrontendUi\Plugin\InvalidateExpiredSessionPlugin"/> </type> + <type name="Magento\Framework\Session\SessionManagerInterface"> + <plugin name="keep_login_as_customer_session_data" + type="Magento\LoginAsCustomerFrontendUi\Plugin\KeepLoginAsCustomerSessionDataPlugin"/> + </type> </config> diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/layout/loginascustomer_login_index.xml b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/layout/loginascustomer_login_index.xml index efb866690c401..768b63cbbecea 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/layout/loginascustomer_login_index.xml +++ b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/layout/loginascustomer_login_index.xml @@ -13,7 +13,7 @@ </action> </referenceBlock> <referenceContainer name="content"> - <block class="Magento\Framework\View\Element\Template" name="loginascustomer_login" template="Magento_LoginAsCustomerFrontendUi::login.phtml"/> + <block class="Magento\Framework\View\Element\Template" name="loginascustomer_login" template="Magento_LoginAsCustomerFrontendUi::login.phtml" cacheable="false"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/templates/html/notices.phtml b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/templates/html/notices.phtml index aa64e78aa234f..b2e0aaf20ce34 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/templates/html/notices.phtml +++ b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/templates/html/notices.phtml @@ -11,7 +11,9 @@ $viewFileUrl = $block->getViewFileUrl('Magento_LoginAsCustomerFrontendUi::images/magento-icon.svg'); ?> <?php if ($block->getConfig()->isEnabled()): ?> - <div data-bind="scope: 'loginAsCustomer'" > + <div class="lac-notification-sticky" + data-mage-init='{"sticky":{"container": "body"}}' + data-bind="scope: 'loginAsCustomer'" > <div class="lac-notification clearfix" data-bind="visible: isVisible" style="display: none"> <div class="top-container"> <div class="lac-notification-icon wrapper"> diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/css/source/_module.less b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/css/source/_module.less index d630ff06c3e34..c42f5143b4fda 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/css/source/_module.less +++ b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/css/source/_module.less @@ -16,43 +16,47 @@ // --------------------------------------------- & when (@media-common = true) { - .lac-notification { - background-color: @lac-notification-background-color; - color: @lac-notification-color; - font-size: 16px; + .lac-notification-sticky { + position: relative; + z-index: 999; + .lac-notification { + background-color: @lac-notification-background-color; + color: @lac-notification-color; + font-size: 16px; - .lac-notification-icon { - float: left; - margin: 10px 25px 10px 10px; + .lac-notification-icon { + float: left; + margin: 10px 25px 10px 10px; - .logo-img { - display: block + .logo-img { + display: block + } } - } - .lac-notification-text { - float: left; - padding: 15px 0; - } + .lac-notification-text { + float: left; + padding: 15px 0; + } - .lac-notification-links { - float: right; - padding: 15px 0; + .lac-notification-links { + float: right; + padding: 15px 0; - a { - color: @lac-notification-links-color; - font-size: 14px; - } + a { + color: @lac-notification-links-color; + font-size: 14px; + } - .lac-notification-close-link { - &:after { - background: url('../Magento_LoginAsCustomerFrontendUi/images/close.svg'); - content: ' '; - display: inline-block; - height: 12px; - margin-left: 5px; - vertical-align: middle; - width: 12px; + .lac-notification-close-link { + &:after { + background: url('../Magento_LoginAsCustomerFrontendUi/images/close.svg'); + content: ' '; + display: inline-block; + height: 12px; + margin-left: 5px; + vertical-align: middle; + width: 12px; + } } } } diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/js/view/loginAsCustomer.js b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/js/view/loginAsCustomer.js index 7f6cad6ce3f2d..c19adbf0dfb4f 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/js/view/loginAsCustomer.js +++ b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/js/view/loginAsCustomer.js @@ -5,10 +5,11 @@ define([ 'jquery', + 'underscore', 'uiComponent', 'Magento_Customer/js/customer-data', 'mage/translate' -], function ($, Component, customerData) { +], function ($, _, Component, customer) { 'use strict'; return Component.extend({ @@ -19,23 +20,53 @@ define([ /** @inheritdoc */ initialize: function () { + var customerData, loggedAsCustomerData; + this._super(); - this.customer = customerData.get('customer'); - this.loginAsCustomer = customerData.get('loggedAsCustomer'); - this.isVisible(this.loginAsCustomer().adminUserId); + customerData = customer.get('customer'); + loggedAsCustomerData = customer.get('loggedAsCustomer'); + + customerData.subscribe(function (data) { + this.fullname = data.fullname; + this.updateBanner(); + }.bind(this)); + loggedAsCustomerData.subscribe(function (data) { + this.adminUserId = data.adminUserId; + this.websiteName = data.websiteName; + this.updateBanner(); + }.bind(this)); + + this.fullname = customerData().fullname; + this.adminUserId = loggedAsCustomerData().adminUserId; + this.websiteName = loggedAsCustomerData().websiteName; - this.notificationText = $.mage.__('You are connected as <strong>%1</strong> on %2') - .replace('%1', this.customer().fullname) - .replace('%2', this.loginAsCustomer().websiteName); + this.updateBanner(); }, /** @inheritdoc */ initObservable: function () { this._super() - .observe('isVisible'); + .observe(['isVisible', 'notificationText']); return this; + }, + + /** + * Update banner area + * + * @returns void + */ + updateBanner: function () { + if (this.adminUserId !== undefined) { + this.isVisible(this.adminUserId); + } + + if (this.fullname !== undefined && this.websiteName !== undefined) { + this.notificationText($.mage.__('You are connected as <strong>%1</strong> on %2') + .replace('%1', _.escape(this.fullname)) + .replace('%2', _.escape(this.websiteName))); + } } }); }); diff --git a/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php index 10d793d6c4c0b..7a058ecfc5df8 100644 --- a/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php +++ b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php @@ -13,6 +13,7 @@ * Data interface for login as customer log. * * @api + * @since 100.4.0 */ interface LogInterface extends ExtensibleDataInterface { @@ -28,6 +29,7 @@ interface LogInterface extends ExtensibleDataInterface * * @param int $logId * @return void + * @since 100.4.0 */ public function setLogId(int $logId): void; @@ -35,6 +37,7 @@ public function setLogId(int $logId): void; * Retrieve login as customer log id. * * @return null|int + * @since 100.4.0 */ public function getLogId(): ?int; @@ -43,6 +46,7 @@ public function getLogId(): ?int; * * @param string $time * @return void + * @since 100.4.0 */ public function setTime(string $time): void; @@ -50,6 +54,7 @@ public function setTime(string $time): void; * Retrieve login as customer log time. * * @return null|string + * @since 100.4.0 */ public function getTime(): ?string; @@ -58,6 +63,7 @@ public function getTime(): ?string; * * @param int $userId * @return void + * @since 100.4.0 */ public function setUserId(int $userId): void; @@ -65,6 +71,7 @@ public function setUserId(int $userId): void; * Retrieve login as customer log user id. * * @return null|int + * @since 100.4.0 */ public function getUserId(): ?int; @@ -73,6 +80,7 @@ public function getUserId(): ?int; * * @param string $userName * @return void + * @since 100.4.0 */ public function setUserName(string $userName): void; @@ -80,6 +88,7 @@ public function setUserName(string $userName): void; * Retrieve login as customer log user name. * * @return null|string + * @since 100.4.0 */ public function getUserName(): ?string; @@ -88,6 +97,7 @@ public function getUserName(): ?string; * * @param int $customerId * @return void + * @since 100.4.0 */ public function setCustomerId(int $customerId): void; @@ -95,6 +105,7 @@ public function setCustomerId(int $customerId): void; * Retrieve login as customer log customer id. * * @return null|int + * @since 100.4.0 */ public function getCustomerId(): ?int; @@ -103,6 +114,7 @@ public function getCustomerId(): ?int; * * @param string $customerEmail * @return void + * @since 100.4.0 */ public function setCustomerEmail(string $customerEmail): void; @@ -110,6 +122,7 @@ public function setCustomerEmail(string $customerEmail): void; * Retrieve login as customer log customer email. * * @return null|string + * @since 100.4.0 */ public function getCustomerEmail(): ?string; @@ -118,6 +131,7 @@ public function getCustomerEmail(): ?string; * * @param \Magento\LoginAsCustomerLog\Api\Data\LogExtensionInterface $extensionAttributes * @return void + * @since 100.4.0 */ public function setExtensionAttributes(LogExtensionInterface $extensionAttributes): void; @@ -125,6 +139,7 @@ public function setExtensionAttributes(LogExtensionInterface $extensionAttribute * Retrieve log extension attributes. * * @return \Magento\LoginAsCustomerLog\Api\Data\LogExtensionInterface + * @since 100.4.0 */ public function getExtensionAttributes(): LogExtensionInterface; } diff --git a/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php index 5b08d28af6335..0f6f06a3b3e43 100644 --- a/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php +++ b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php @@ -13,6 +13,7 @@ * Login as customer log entity search results interface. * * @api + * @since 100.4.0 */ interface LogSearchResultsInterface extends SearchResultsInterface { @@ -20,6 +21,7 @@ interface LogSearchResultsInterface extends SearchResultsInterface * Get log list. * * @return \Magento\LoginAsCustomerLog\Api\Data\LogInterface[] + * @since 100.4.0 */ public function getItems(); @@ -28,6 +30,7 @@ public function getItems(); * * @param \Magento\LoginAsCustomerLog\Api\Data\LogInterface[] $items * @return void + * @since 100.4.0 */ public function setItems(array $items); } diff --git a/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php index 4b5ee382c908a..58b232f26e656 100644 --- a/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php +++ b/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php @@ -14,6 +14,7 @@ * Get login as customer log list considering search criteria. * * @api + * @since 100.4.0 */ interface GetLogsListInterface { @@ -22,6 +23,7 @@ interface GetLogsListInterface * * @param SearchCriteriaInterface $searchCriteria * @return LogSearchResultsInterface + * @since 100.4.0 */ public function execute(SearchCriteriaInterface $searchCriteria): LogSearchResultsInterface; } diff --git a/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php index 67e1ece477727..0d76db641b421 100644 --- a/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php +++ b/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php @@ -11,6 +11,7 @@ * Save login as custom logs entities. * * @api + * @since 100.4.0 */ interface SaveLogsInterface { @@ -19,6 +20,7 @@ interface SaveLogsInterface * * @param \Magento\LoginAsCustomerLog\Api\Data\LogInterface[] $logs * @return void + * @since 100.4.0 */ public function execute(array $logs): void; } diff --git a/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml b/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml index 077fd6e18db7c..fdd1bf55c91b9 100644 --- a/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml +++ b/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml @@ -38,7 +38,6 @@ </settings> <bookmark name="bookmarks"/> <columnsControls name="columns_controls"/> - <filterSearch name="name"/> <filters name="listing_filters"> <settings> <templates> diff --git a/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php b/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php index 6b36a0720ecb3..2b98c1f6c119e 100644 --- a/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php +++ b/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php @@ -7,8 +7,8 @@ namespace Magento\LoginAsCustomerPageCache\Plugin\PageCache\Model\Config; -use Magento\Customer\Model\Session; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; use Magento\PageCache\Model\Config; use Magento\Store\Model\ScopeInterface; @@ -27,20 +27,20 @@ class DisablePageCacheIfNeededPlugin private $scopeConfig; /** - * @var Session + * @var GetLoggedAsCustomerAdminIdInterface */ - private $customerSession; + private $getLoggedAsCustomerAdminId; /** * @param ScopeConfigInterface $scopeConfig - * @param Session $customerSession + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId */ public function __construct( ScopeConfigInterface $scopeConfig, - Session $customerSession + GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId ) { $this->scopeConfig = $scopeConfig; - $this->customerSession = $customerSession; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId; } /** @@ -54,11 +54,11 @@ public function __construct( public function afterIsEnabled(Config $subject, $isEnabled): bool { if ($isEnabled) { - $disable = $this->scopeConfig->getValue( + $disable = $this->scopeConfig->isSetFlag( 'login_as_customer/general/disable_page_cache', ScopeInterface::SCOPE_STORE ); - $adminId = $this->customerSession->getLoggedAsCustomerAdmindId(); + $adminId = $this->getLoggedAsCustomerAdminId->execute(); if ($disable && $adminId) { $isEnabled = false; } diff --git a/app/code/Magento/LoginAsCustomerPageCache/composer.json b/app/code/Magento/LoginAsCustomerPageCache/composer.json index 195a08fc19d83..84d7f2e2a6730 100644 --- a/app/code/Magento/LoginAsCustomerPageCache/composer.json +++ b/app/code/Magento/LoginAsCustomerPageCache/composer.json @@ -4,8 +4,8 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", - "magento/module-customer": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-login-as-customer-api": "*" }, "suggest": { "magento/module-page-cache": "*" diff --git a/app/code/Magento/LoginAsCustomerQuote/Plugin/LoginAsCustomerApi/ProcessShoppingCartPlugin.php b/app/code/Magento/LoginAsCustomerQuote/Plugin/LoginAsCustomerApi/ProcessShoppingCartPlugin.php index cf25962a104b2..4aa068a0ccc61 100644 --- a/app/code/Magento/LoginAsCustomerQuote/Plugin/LoginAsCustomerApi/ProcessShoppingCartPlugin.php +++ b/app/code/Magento/LoginAsCustomerQuote/Plugin/LoginAsCustomerApi/ProcessShoppingCartPlugin.php @@ -15,7 +15,7 @@ use Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface; /** - * Remove all items from guest shopping cart before execute. Mark customer cart as not-guest after execute + * Remove all items from guest shopping cart and mark cart as not-guest * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ @@ -60,7 +60,7 @@ public function __construct( } /** - * Remove all items from guest shopping cart + * Remove all items from guest shopping cart and mark cart as not-guest * * @param AuthenticateCustomerBySecretInterface $subject * @param string $secret @@ -77,31 +77,9 @@ public function beforeExecute( $quote = $this->checkoutSession->getQuote(); /* Remove items from guest cart */ $quote->removeAllItems(); + $quote->setCustomerIsGuest(0); $this->quoteRepository->save($quote); } return null; } - - /** - * Mark customer cart as not-guest - * - * @param AuthenticateCustomerBySecretInterface $subject - * @param void $result - * @param string $secret - * @return void - * @throws LocalizedException - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterExecute( - AuthenticateCustomerBySecretInterface $subject, - $result, - string $secret - ) { - $this->checkoutSession->loadCustomerQuote(); - $quote = $this->checkoutSession->getQuote(); - - $quote->setCustomerIsGuest(0); - $this->quoteRepository->save($quote); - } } diff --git a/app/code/Magento/LoginAsCustomerSales/Plugin/AdminAddCommentOnOrderPlacementPlugin.php b/app/code/Magento/LoginAsCustomerSales/Plugin/AdminAddCommentOnOrderPlacementPlugin.php index 2ae982e536f49..3a27a5ef9e561 100644 --- a/app/code/Magento/LoginAsCustomerSales/Plugin/AdminAddCommentOnOrderPlacementPlugin.php +++ b/app/code/Magento/LoginAsCustomerSales/Plugin/AdminAddCommentOnOrderPlacementPlugin.php @@ -25,9 +25,8 @@ class AdminAddCommentOnOrderPlacementPlugin /** * @param Session $session */ - public function __construct( - Session $session - ) { + public function __construct(Session $session) + { $this->userSession = $session; } diff --git a/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php b/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php index dc7b295f61c4d..87ffe81998d58 100644 --- a/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php +++ b/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php @@ -7,7 +7,7 @@ namespace Magento\LoginAsCustomerSales\Plugin; -use Magento\Customer\Model\Session; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; use Magento\Sales\Model\Order; use Magento\User\Model\UserFactory; @@ -19,25 +19,25 @@ class FrontAddCommentOnOrderPlacementPlugin { /** - * @var Session + * @var UserFactory */ - private $customerSession; + private $userFactory; /** - * @var UserFactory + * @var GetLoggedAsCustomerAdminIdInterface */ - private $userFactory; + private $getLoggedAsCustomerAdminId; /** - * @param Session $session * @param UserFactory $userFactory + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId */ public function __construct( - Session $session, - UserFactory $userFactory + UserFactory $userFactory, + GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId ) { - $this->customerSession = $session; $this->userFactory = $userFactory; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId; } /** @@ -49,7 +49,7 @@ public function __construct( */ public function afterPlace(Order $subject, Order $result): Order { - $adminId = $this->customerSession->getLoggedAsCustomerAdmindId(); + $adminId = $this->getLoggedAsCustomerAdminId->execute(); if ($adminId) { $adminUser = $this->userFactory->create()->load($adminId); $subject->addCommentToStatusHistory( diff --git a/app/code/Magento/LoginAsCustomerSales/composer.json b/app/code/Magento/LoginAsCustomerSales/composer.json index 3965e8acf87d8..3891504e54092 100644 --- a/app/code/Magento/LoginAsCustomerSales/composer.json +++ b/app/code/Magento/LoginAsCustomerSales/composer.json @@ -5,8 +5,8 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-backend": "*", - "magento/module-customer": "*", - "magento/module-user": "*" + "magento/module-user": "*", + "magento/module-login-as-customer-api": "*" }, "suggest": { "magento/module-sales": "*" diff --git a/app/code/Magento/LoginAsCustomerSales/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerSales/etc/frontend/di.xml new file mode 100644 index 0000000000000..1a010fcdead85 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/etc/frontend/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\Sales\Model\Order"> + <plugin name="front-order-placement-comment" type="Magento\LoginAsCustomerSales\Plugin\FrontAddCommentOnOrderPlacementPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml b/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml index 1a010fcdead85..6dda349f1e60d 100644 --- a/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml +++ b/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\Sales\Model\Order"> - <plugin name="front-order-placement-comment" type="Magento\LoginAsCustomerSales\Plugin\FrontAddCommentOnOrderPlacementPlugin"/> + <plugin name="rest-order-placement-comment" type="Magento\LoginAsCustomerSales\Plugin\FrontAddCommentOnOrderPlacementPlugin"/> </type> </config> diff --git a/app/code/Magento/MediaContent/etc/di.xml b/app/code/Magento/MediaContent/etc/di.xml index f2a9459447001..df84ad7bb0f70 100644 --- a/app/code/Magento/MediaContent/etc/di.xml +++ b/app/code/Magento/MediaContent/etc/di.xml @@ -16,6 +16,7 @@ <preference for="Magento\MediaContentApi\Api\Data\ContentIdentityInterface" type="Magento\MediaContent\Model\ContentIdentity"/> <preference for="Magento\MediaContentApi\Api\Data\ContentAssetLinkInterface" type="Magento\MediaContent\Model\ContentAssetLink"/> <preference for="Magento\MediaContentApi\Model\SearchPatternConfigInterface" type="Magento\MediaContent\Model\Content\SearchPatternConfig"/> + <preference for="Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface" type="Magento\MediaContentApi\Model\Composite\GetAssetIdsByContentField"/> <type name="Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface"> <plugin name="remove_media_content_after_asset_is_removed_by_path" type="Magento\MediaContent\Plugin\MediaGalleryAssetDeleteByPath" /> </type> diff --git a/app/code/Magento/MediaContentApi/Api/Data/ContentAssetLinkInterface.php b/app/code/Magento/MediaContentApi/Api/Data/ContentAssetLinkInterface.php index 5ff490655d464..c438d90aeb4f0 100644 --- a/app/code/Magento/MediaContentApi/Api/Data/ContentAssetLinkInterface.php +++ b/app/code/Magento/MediaContentApi/Api/Data/ContentAssetLinkInterface.php @@ -14,6 +14,7 @@ /** * Data interface representing the identificator of content. I.e. short description field of product entity with id 42 * @api + * @since 100.4.0 */ interface ContentAssetLinkInterface extends ExtensibleDataInterface { @@ -21,6 +22,7 @@ interface ContentAssetLinkInterface extends ExtensibleDataInterface * Return the object that represent content identity * * @return ContentIdentityInterface + * @since 100.4.0 */ public function getContentId(): ContentIdentityInterface; @@ -28,6 +30,7 @@ public function getContentId(): ContentIdentityInterface; * Array of assets related to the content entity * * @return int + * @since 100.4.0 */ public function getAssetId(): int; @@ -35,6 +38,7 @@ public function getAssetId(): int; * Retrieve existing extension attributes object or create a new one. * * @return \Magento\MediaContentApi\Api\Data\ContentAssetLinkExtensionInterface|null + * @since 100.4.0 */ public function getExtensionAttributes(): ?ContentAssetLinkExtensionInterface; @@ -43,6 +47,7 @@ public function getExtensionAttributes(): ?ContentAssetLinkExtensionInterface; * * @param \Magento\MediaContentApi\Api\Data\ContentAssetLinkExtensionInterface|null $extensionAttributes * @return void + * @since 100.4.0 */ public function setExtensionAttributes(?ContentAssetLinkExtensionInterface $extensionAttributes): void; } diff --git a/app/code/Magento/MediaContentApi/Api/Data/ContentIdentityInterface.php b/app/code/Magento/MediaContentApi/Api/Data/ContentIdentityInterface.php index f1b701fe9d964..16851b657ab90 100644 --- a/app/code/Magento/MediaContentApi/Api/Data/ContentIdentityInterface.php +++ b/app/code/Magento/MediaContentApi/Api/Data/ContentIdentityInterface.php @@ -14,6 +14,7 @@ /** * Data interface representing the identificator of content. I.e. short description field of product entity with id 42 * @api + * @since 100.4.0 */ interface ContentIdentityInterface extends ExtensibleDataInterface { @@ -21,6 +22,7 @@ interface ContentIdentityInterface extends ExtensibleDataInterface * Type of entity that can have a content with media. I.e. catalog_product or cms_page * * @return string + * @since 100.4.0 */ public function getEntityType(): string; @@ -28,6 +30,7 @@ public function getEntityType(): string; * Id of the entity containing content with media * * @return string + * @since 100.4.0 */ public function getEntityId(): string; @@ -35,6 +38,7 @@ public function getEntityId(): string; * Field of the entity where the content can be stored. I.e. short_description for product * * @return string + * @since 100.4.0 */ public function getField(): string; @@ -42,6 +46,7 @@ public function getField(): string; * Retrieve existing extension attributes object or create a new one. * * @return \Magento\MediaContentApi\Api\Data\ContentIdentityExtensionInterface|null + * @since 100.4.0 */ public function getExtensionAttributes(): ?ContentIdentityExtensionInterface; @@ -50,6 +55,7 @@ public function getExtensionAttributes(): ?ContentIdentityExtensionInterface; * * @param \Magento\MediaContentApi\Api\Data\ContentIdentityExtensionInterface|null $extensionAttributes * @return void + * @since 100.4.0 */ public function setExtensionAttributes(?ContentIdentityExtensionInterface $extensionAttributes): void; } diff --git a/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksByAssetIdsInterface.php b/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksByAssetIdsInterface.php index 8997e4b6e7e77..753dcc3ed7aae 100644 --- a/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksByAssetIdsInterface.php +++ b/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksByAssetIdsInterface.php @@ -13,6 +13,7 @@ /** * Delete the relation between media asset and the piece of content. I.e media asset no longer part of the content * @api + * @since 100.4.0 */ interface DeleteContentAssetLinksByAssetIdsInterface { @@ -21,6 +22,7 @@ interface DeleteContentAssetLinksByAssetIdsInterface * * @param int[] $assetIds * @throws \Magento\Framework\Exception\CouldNotDeleteException + * @since 100.4.0 */ public function execute(array $assetIds): void; } diff --git a/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksInterface.php b/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksInterface.php index 9c50793f51303..433db3923ceb4 100644 --- a/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksInterface.php +++ b/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksInterface.php @@ -13,6 +13,7 @@ /** * Remove the relation between media asset and the piece of content. I.e media asset no longer part of the content * @api + * @since 100.4.0 */ interface DeleteContentAssetLinksInterface { @@ -21,6 +22,7 @@ interface DeleteContentAssetLinksInterface * * @param ContentAssetLinkInterface[] $contentAssetLinks * @throws \Magento\Framework\Exception\CouldNotDeleteException + * @since 100.4.0 */ public function execute(array $contentAssetLinks): void; } diff --git a/app/code/Magento/MediaContentApi/Api/ExtractAssetsFromContentInterface.php b/app/code/Magento/MediaContentApi/Api/ExtractAssetsFromContentInterface.php index 4f95571f30ffd..06ac7a5bd0778 100644 --- a/app/code/Magento/MediaContentApi/Api/ExtractAssetsFromContentInterface.php +++ b/app/code/Magento/MediaContentApi/Api/ExtractAssetsFromContentInterface.php @@ -12,6 +12,7 @@ /** * Parse the content string for references to media assets and return the list of identified media assets * @api + * @since 100.4.0 */ interface ExtractAssetsFromContentInterface { @@ -20,6 +21,7 @@ interface ExtractAssetsFromContentInterface * * @param string $content * @return AssetInterface[] + * @since 100.4.0 */ public function execute(string $content): array; } diff --git a/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentFieldInterface.php b/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentFieldInterface.php new file mode 100644 index 0000000000000..f2f9ddbf11956 --- /dev/null +++ b/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentFieldInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentApi\Api; + +use Magento\Framework\Exception\InvalidArgumentException; + +/** + * Interface used to return Asset id by content field. + */ +interface GetAssetIdsByContentFieldInterface +{ + /** + * This function returns asset ids by content field + * + * @param string $field + * @param string $value + * @throws InvalidArgumentException + * @return int[] + */ + public function execute(string $field, string $value): array; +} diff --git a/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentIdentityInterface.php b/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentIdentityInterface.php index 4316a0d6ee33d..cca5127c3eb4a 100644 --- a/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentIdentityInterface.php +++ b/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentIdentityInterface.php @@ -13,6 +13,7 @@ /** * Get media asset ids that are used in the piece of content identified by the specified content identity * @api + * @since 100.4.0 */ interface GetAssetIdsByContentIdentityInterface { @@ -22,6 +23,7 @@ interface GetAssetIdsByContentIdentityInterface * @param ContentIdentityInterface $contentIdentity * @return int[] * @throws \Magento\Framework\Exception\IntegrationException + * @since 100.4.0 */ public function execute(ContentIdentityInterface $contentIdentity): array; } diff --git a/app/code/Magento/MediaContentApi/Api/GetContentByAssetIdsInterface.php b/app/code/Magento/MediaContentApi/Api/GetContentByAssetIdsInterface.php index cb117545c257e..93d5901044e36 100644 --- a/app/code/Magento/MediaContentApi/Api/GetContentByAssetIdsInterface.php +++ b/app/code/Magento/MediaContentApi/Api/GetContentByAssetIdsInterface.php @@ -13,6 +13,7 @@ /** * Get list of content identifiers for pieces of content that include the specified media asset * @api + * @since 100.4.0 */ interface GetContentByAssetIdsInterface { @@ -22,6 +23,7 @@ interface GetContentByAssetIdsInterface * @param int[] $assetIds * @return ContentIdentityInterface[] * @throws \Magento\Framework\Exception\IntegrationException + * @since 100.4.0 */ public function execute(array $assetIds): array; } diff --git a/app/code/Magento/MediaContentApi/Api/SaveContentAssetLinksInterface.php b/app/code/Magento/MediaContentApi/Api/SaveContentAssetLinksInterface.php index 1c86953ce6f84..8363a85ea83be 100644 --- a/app/code/Magento/MediaContentApi/Api/SaveContentAssetLinksInterface.php +++ b/app/code/Magento/MediaContentApi/Api/SaveContentAssetLinksInterface.php @@ -13,6 +13,7 @@ /** * Save a media asset to content relation. * @api + * @since 100.4.0 */ interface SaveContentAssetLinksInterface { @@ -21,6 +22,7 @@ interface SaveContentAssetLinksInterface * * @param ContentAssetLinkInterface[] $contentAssetLinks * @throws \Magento\Framework\Exception\CouldNotSaveException + * @since 100.4.0 */ public function execute(array $contentAssetLinks): void; } diff --git a/app/code/Magento/MediaContentApi/Model/Composite/GetAssetIdsByContentField.php b/app/code/Magento/MediaContentApi/Model/Composite/GetAssetIdsByContentField.php new file mode 100644 index 0000000000000..61df8504b4c77 --- /dev/null +++ b/app/code/Magento/MediaContentApi/Model/Composite/GetAssetIdsByContentField.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentApi\Model\Composite; + +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface as GetAssetIdsByContentFieldApiInterface; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to return Asset ids by content field + */ +class GetAssetIdsByContentField implements GetAssetIdsByContentFieldApiInterface +{ + /** + * @var array + */ + private $fieldHandlers; + + /** + * GetAssetIdsByContentField constructor. + * + * @param array $fieldHandlers + */ + public function __construct(array $fieldHandlers = []) + { + $this->fieldHandlers = $fieldHandlers; + } + + /** + * @inheritDoc + */ + public function execute(string $field, string $value): array + { + if (!array_key_exists($field, $this->fieldHandlers)) { + throw new InvalidArgumentException(__('The field argument is invalid.')); + } + $ids = []; + /** @var GetAssetIdsByContentFieldInterface $fieldHandler */ + foreach ($this->fieldHandlers[$field] as $fieldHandler) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $ids = array_merge($ids, $fieldHandler->execute($value)); + } + return array_unique($ids); + } +} diff --git a/app/code/Magento/MediaContentApi/Model/Config.php b/app/code/Magento/MediaContentApi/Model/Config.php new file mode 100644 index 0000000000000..ab2b4e7ae0dbe --- /dev/null +++ b/app/code/Magento/MediaContentApi/Model/Config.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaContentApi\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Class responsible to provide access to system configuration related to the Media Gallery + */ +class Config +{ + /** + * Path to enable/disable media gallery in the system settings. + */ + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Config constructor. + * + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check if new media gallery enabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED); + } +} diff --git a/app/code/Magento/MediaContentApi/Model/GetAssetIdsByContentFieldInterface.php b/app/code/Magento/MediaContentApi/Model/GetAssetIdsByContentFieldInterface.php new file mode 100644 index 0000000000000..f38ffecedc202 --- /dev/null +++ b/app/code/Magento/MediaContentApi/Model/GetAssetIdsByContentFieldInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentApi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Interface used to return Asset id by content field. + */ +interface GetAssetIdsByContentFieldInterface +{ + /** + * This function returns asset ids by content field + * + * @param string $value + * @return int[] + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function execute(string $value): array; +} diff --git a/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php b/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php index c58d543a597b7..ac24a32d35475 100644 --- a/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php +++ b/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php @@ -12,6 +12,7 @@ /** * Get Entity Contents. * @api + * @since 100.4.0 */ interface GetEntityContentsInterface { @@ -20,6 +21,7 @@ interface GetEntityContentsInterface * * @param ContentIdentityInterface $contentIdentity * @return string[] + * @since 100.4.0 */ public function execute(ContentIdentityInterface $contentIdentity): array; } diff --git a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByCategoryStore.php b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByCategoryStore.php new file mode 100644 index 0000000000000..232577b77c802 --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByCategoryStore.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Model\ResourceModel; + +use Magento\Catalog\Api\CategoryManagementInterface; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; +use Magento\Store\Api\GroupRepositoryInterface; +use Magento\Store\Api\StoreRepositoryInterface; + +/** + * Class responsible to return Asset id by category store + */ +class GetAssetIdsByCategoryStore implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + private const TABLE_CATALOG_CATEGORY = 'catalog_category_entity'; + private const ENTITY_TYPE = 'catalog_category'; + private const ID_COLUMN = 'entity_id'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var GroupRepositoryInterface + */ + private $storeGroupRepository; + + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + /** + * GetAssetIdsByCategoryStore constructor. + * + * @param ResourceConnection $resource + * @param StoreRepositoryInterface $storeRepository + * @param GroupRepositoryInterface $storeGroupRepository + * @param CategoryRepositoryInterface $categoryRepository + */ + public function __construct( + ResourceConnection $resource, + StoreRepositoryInterface $storeRepository, + GroupRepositoryInterface $storeGroupRepository, + CategoryRepositoryInterface $categoryRepository + ) { + $this->connection = $resource; + $this->storeRepository = $storeRepository; + $this->storeGroupRepository = $storeGroupRepository; + $this->categoryRepository = $categoryRepository; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + try { + $storeView = $this->storeRepository->getById($value); + $storeGroup = $this->storeGroupRepository->get($storeView->getStoreGroupId()); + $rootCategory = $this->categoryRepository->get($storeGroup->getRootCategoryId()); + } catch (NoSuchEntityException $exception) { + return []; + } + + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->joinInner( + ['category_table' => $this->connection->getTableName(self::TABLE_CATALOG_CATEGORY)], + 'asset_content_table.entity_id = category_table.' . self::ID_COLUMN, + [] + )->where( + 'entity_type = ?', + self::ENTITY_TYPE + )->where( + 'path LIKE ?', + $rootCategory->getPath() . '%' + ); + + return $this->connection->getConnection()->fetchCol($sql); + } +} diff --git a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByEavContentField.php b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByEavContentField.php new file mode 100644 index 0000000000000..00c6e2f180a6f --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByEavContentField.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Model\ResourceModel; + +use Magento\Eav\Model\Config; +use Magento\Framework\App\ResourceConnection; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to return Asset id by eav content field + */ +class GetAssetIdsByEavContentField implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var Config + */ + private $config; + + /** + * @var string + */ + private $attributeCode; + + /** + * @var string + */ + private $entityType; + + /** + * @var string + */ + private $entityTable; + + /** + * @var array + */ + private $valueMap; + + /** + * GetAssetIdsByEavContentField constructor. + * + * @param ResourceConnection $resource + * @param Config $config + * @param string $attributeCode + * @param string $entityType + * @param string $entityTable + * @param array $valueMap + */ + public function __construct( + ResourceConnection $resource, + Config $config, + string $attributeCode, + string $entityType, + string $entityTable, + array $valueMap = [] + ) { + $this->connection = $resource; + $this->config = $config; + $this->attributeCode = $attributeCode; + $this->entityType = $entityType; + $this->entityTable = $entityTable; + $this->valueMap = $valueMap; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + $attribute = $this->config->getAttribute($this->entityType, $this->attributeCode); + + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + $this->entityType + )->joinInner( + ['entity_table' => $this->connection->getTableName($this->entityTable)], + 'asset_content_table.entity_id = entity_table.entity_id', + [] + )->joinInner( + ['entity_eav_type' => $this->connection->getTableName($attribute->getBackendTable())], + 'entity_table.' . $attribute->getEntityIdField() . ' = entity_eav_type.' . $attribute->getEntityIdField() . + ' AND entity_eav_type.attribute_id = ' . $attribute->getAttributeId(), + [] + )->where( + 'entity_eav_type.value = ?', + $this->getValueFromMap($value) + ); + + return $this->connection->getConnection()->fetchCol($sql); + } + + /** + * Get a value from a value map + * + * @param string $value + * @return string + */ + private function getValueFromMap(string $value): string + { + return $this->valueMap[$value] ?? $value; + } +} diff --git a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByProductStore.php b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByProductStore.php new file mode 100644 index 0000000000000..6548b2964caaf --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByProductStore.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; +use Magento\Store\Api\StoreRepositoryInterface; + +/** + * Class responsible to return Asset ids by product store + */ +class GetAssetIdsByProductStore implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + private const ENTITY_TYPE = 'catalog_product'; + private const FIELD_TABLE = 'catalog_product_website'; + private const ID_COLUMN = 'product_id'; + private const FIELD_COLUMN = 'website_id'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * GetAssetIdsByProductStore constructor. + * + * @param ResourceConnection $resource + * @param StoreRepositoryInterface $storeRepository + */ + public function __construct( + ResourceConnection $resource, + StoreRepositoryInterface $storeRepository + ) { + $this->connection = $resource; + $this->storeRepository = $storeRepository; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + $store = $this->storeRepository->getById($value); + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + self::ENTITY_TYPE + )->joinInner( + ['field_table' => $this->connection->getTableName(self::FIELD_TABLE)], + 'asset_content_table.entity_id = field_table.' . self::ID_COLUMN, + [] + )->where( + 'field_table.' . self::FIELD_COLUMN . ' = ?', + $store->getWebsiteId() + ); + + return $this->connection->getConnection()->fetchCol($sql); + } +} diff --git a/app/code/Magento/MediaContentCatalog/Observer/Category.php b/app/code/Magento/MediaContentCatalog/Observer/Category.php index 5c2deeab258df..53afae53a2aa7 100644 --- a/app/code/Magento/MediaContentCatalog/Observer/Category.php +++ b/app/code/Magento/MediaContentCatalog/Observer/Category.php @@ -13,6 +13,7 @@ use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Model\Config; /** * Observe the catalog_category_save_after event and run processing relation between category content and media asset. @@ -29,6 +30,11 @@ class Category implements ObserverInterface */ private $updateContentAssetLinks; + /** + * @var Config + */ + private $config; + /** * @var array */ @@ -50,17 +56,20 @@ class Category implements ObserverInterface * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param GetEntityContentsInterface $getContent * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param Config $config * @param array $fields */ public function __construct( ContentIdentityInterfaceFactory $contentIdentityFactory, GetEntityContentsInterface $getContent, UpdateContentAssetLinksInterface $updateContentAssetLinks, + Config $config, array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; $this->getContent = $getContent; $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->config = $config; $this->fields = $fields; } @@ -72,6 +81,10 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } + $model = $observer->getEvent()->getData('category'); if ($model instanceof CatalogCategory) { diff --git a/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php new file mode 100644 index 0000000000000..8722f6568310c --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Observer; + +use Magento\Catalog\Model\Category as CatalogCategory; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\MediaContentApi\Model\Config; + +/** + * Observe the catalog_category_delete_after event and deletes relation between category content and media asset. + */ +class CategoryDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'catalog_category'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var Config + */ + private $config; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param Config $config + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + Config $config, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->config = $config; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + if (!$this->config->isEnabled()) { + return; + } + + $category = $observer->getEvent()->getData('category'); + $contentAssetLinks = []; + + if ($category instanceof CatalogCategory) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $category->getEntityId(), + ] + ); + $content = implode(PHP_EOL, $this->getContent->execute($contentIdentity)); + $assets = $this->extractAssetsFromContent->execute($content); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCatalog/Observer/Product.php b/app/code/Magento/MediaContentCatalog/Observer/Product.php index 306bcc0b466c2..b7f52d95068fb 100644 --- a/app/code/Magento/MediaContentCatalog/Observer/Product.php +++ b/app/code/Magento/MediaContentCatalog/Observer/Product.php @@ -13,6 +13,7 @@ use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Model\Config; /** * Observe the catalog_product_save_after event and run processing relation between product content and media asset @@ -34,6 +35,11 @@ class Product implements ObserverInterface */ private $fields; + /** + * @var Config + */ + private $config; + /** * @var ContentIdentityInterfaceFactory */ @@ -45,22 +51,25 @@ class Product implements ObserverInterface private $getContent; /** - * * Create links for product content + * Product observer constructor * * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param GetEntityContentsInterface $getContent * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param Config $config * @param array $fields */ public function __construct( ContentIdentityInterfaceFactory $contentIdentityFactory, GetEntityContentsInterface $getContent, UpdateContentAssetLinksInterface $updateContentAssetLinks, + Config $config, array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; $this->getContent = $getContent; $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->config = $config; $this->fields = $fields; } @@ -72,6 +81,10 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } + $model = $observer->getEvent()->getData('product'); if ($model instanceof CatalogProduct) { foreach ($this->fields as $field) { diff --git a/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php new file mode 100644 index 0000000000000..38622178e2119 --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Observer; + +use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\MediaContentApi\Model\Config; + +/** + * Observe the catalog_product_delete_before event and deletes relation between category content and media asset. + */ +class ProductDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'catalog_product'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var Config + */ + private $config; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param Config $config + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + Config $config, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->config = $config; + $this->fields = $fields; + } + + /** + * Retrieve the deleted product and remove relation betwen product and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + if (!$this->config->isEnabled()) { + return; + } + + $product = $observer->getEvent()->getData('product'); + $contentAssetLinks = []; + + if ($product instanceof CatalogProduct) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $product->getEntityId(), + ] + ); + $productContent = implode(PHP_EOL, $this->getContent->execute($contentIdentity)); + $assets = $this->extractAssetsFromContent->execute($productContent); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCatalog/composer.json b/app/code/Magento/MediaContentCatalog/composer.json index 21e23e6b18bdc..2b19bc95f6ed3 100644 --- a/app/code/Magento/MediaContentCatalog/composer.json +++ b/app/code/Magento/MediaContentCatalog/composer.json @@ -6,6 +6,7 @@ "magento/module-media-content-api": "*", "magento/module-catalog": "*", "magento/module-eav": "*", + "magento/module-store": "*", "magento/framework": "*" }, "type": "magento2-module", diff --git a/app/code/Magento/MediaContentCatalog/etc/di.xml b/app/code/Magento/MediaContentCatalog/etc/di.xml index a2d300a2bb208..8c606a3cae49f 100644 --- a/app/code/Magento/MediaContentCatalog/etc/di.xml +++ b/app/code/Magento/MediaContentCatalog/etc/di.xml @@ -14,6 +14,22 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentCatalog\Observer\ProductDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="description" xsi:type="string">description</item> + <item name="short_description" xsi:type="string">short_description</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentCatalog\Observer\CategoryDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="image" xsi:type="string">image</item> + <item name="description" xsi:type="string">description</item> + </argument> + </arguments> + </type> <type name="Magento\MediaContentCatalog\Observer\Category"> <arguments> <argument name="fields" xsi:type="array"> @@ -30,4 +46,36 @@ </argument> </arguments> </type> + <virtualType name="Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByProductStatus" type="Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByEavContentField"> + <arguments> + <argument name="attributeCode" xsi:type="string">status</argument> + <argument name="entityType" xsi:type="string">catalog_product</argument> + <argument name="entityTable" xsi:type="string">catalog_product_entity</argument> + <argument name="valueMap" xsi:type="array"> + <item name="1" xsi:type="string">1</item> + <item name="0" xsi:type="string">2</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByCategoryStatus" type="Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByEavContentField"> + <arguments> + <argument name="attributeCode" xsi:type="string">is_active</argument> + <argument name="entityType" xsi:type="string">catalog_category</argument> + <argument name="entityTable" xsi:type="string">catalog_category_entity</argument> + </arguments> + </virtualType> + <type name="Magento\MediaContentApi\Model\Composite\GetAssetIdsByContentField"> + <arguments> + <argument name="fieldHandlers" xsi:type="array"> + <item name="content_status" xsi:type="array"> + <item name="getAssetIdsByProductStatus" xsi:type="object">Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByProductStatus</item> + <item name="getAssetIdsByCategoryStatus" xsi:type="object">Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByCategoryStatus</item> + </item> + <item name="store_id" xsi:type="array"> + <item name="getAssetIdsByProductStore" xsi:type="object">Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByProductStore</item> + <item name="getAssetIdsByCategoryStore" xsi:type="object">Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByCategoryStore</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaContentCatalog/etc/events.xml b/app/code/Magento/MediaContentCatalog/etc/events.xml index f68d66eb3cc40..8ec7a30b961ba 100644 --- a/app/code/Magento/MediaContentCatalog/etc/events.xml +++ b/app/code/Magento/MediaContentCatalog/etc/events.xml @@ -9,6 +9,12 @@ <event name="catalog_category_save_after"> <observer name="media_content_catalog_category_save_after" instance="Magento\MediaContentCatalog\Observer\Category" /> </event> + <event name="catalog_product_delete_before"> + <observer name="media_content_catalog_product_delete_before" instance="Magento\MediaContentCatalog\Observer\ProductDelete" /> + </event> + <event name="catalog_category_delete_before"> + <observer name="media_content_catalog_category_delete_before" instance="Magento\MediaContentCatalog\Observer\CategoryDelete" /> + </event> <event name="catalog_product_save_after"> <observer name="media_content_catalog_product_save_after" instance="Magento\MediaContentCatalog\Observer\Product" /> </event> diff --git a/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByBlockStore.php b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByBlockStore.php new file mode 100644 index 0000000000000..f1f8d81ec32f2 --- /dev/null +++ b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByBlockStore.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Model\ResourceModel; + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to return Asset id by content field + */ +class GetAssetIdsByBlockStore implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + private const ENTITY_TYPE = 'cms_block'; + private const STORE_FIELD = 'store_id'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * GetAssetIdsByContentField constructor. + * + * @param ResourceConnection $resource + * @param BlockRepositoryInterface $blockRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + */ + public function __construct( + ResourceConnection $resource, + BlockRepositoryInterface $blockRepository, + SearchCriteriaBuilder $searchCriteriaBuilder + ) { + $this->connection = $resource; + $this->blockRepository = $blockRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + self::ENTITY_TYPE + )->where( + 'entity_id IN (?)', + $this->getBlockIdsByStore((int) $value) + ); + + return $this->connection->getConnection()->fetchCol($sql); + } + + /** + * Get block ids by store + * + * @param int $storeId + * @return array + * @throws LocalizedException + */ + private function getBlockIdsByStore(int $storeId): array + { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(self::STORE_FIELD, $storeId) + ->create(); + + $searchResult = $this->blockRepository->getList($searchCriteria); + + return array_map(function (BlockInterface $block) { + return $block->getId(); + }, $searchResult->getItems()); + } +} diff --git a/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentField.php b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentField.php new file mode 100644 index 0000000000000..9c223fd870645 --- /dev/null +++ b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentField.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to return Asset id by content field + */ +class GetAssetIdsByContentField implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var string + */ + private $entityType; + + /** + * @var string + */ + private $fieldTable; + + /** + * @var string + */ + private $fieldColumn; + + /** + * @var string + */ + private $idColumn; + + /** + * GetAssetIdsByContentField constructor. + * + * @param ResourceConnection $resource + * @param string $entityType + * @param string $fieldTable + * @param string $idColumn + * @param string $fieldColumn + */ + public function __construct( + ResourceConnection $resource, + string $entityType, + string $fieldTable, + string $idColumn, + string $fieldColumn + ) { + $this->connection = $resource; + $this->entityType = $entityType; + $this->fieldTable = $fieldTable; + $this->idColumn = $idColumn; + $this->fieldColumn = $fieldColumn; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + $this->entityType + )->joinInner( + ['field_table' => $this->connection->getTableName($this->fieldTable)], + 'asset_content_table.entity_id = field_table.' . $this->idColumn, + [] + )->where( + 'field_table.' . $this->fieldColumn . ' = ?', + $value + ); + + return $this->connection->getConnection()->fetchCol($sql); + } +} diff --git a/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByPageStore.php b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByPageStore.php new file mode 100644 index 0000000000000..92cf67e7d03e4 --- /dev/null +++ b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByPageStore.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Model\ResourceModel; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to return Asset id by content field + */ +class GetAssetIdsByPageStore implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + private const ENTITY_TYPE = 'cms_page'; + private const STORE_FIELD = 'store_id'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * GetAssetIdsByContentField constructor. + * + * @param ResourceConnection $resource + * @param PageRepositoryInterface $pageRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + */ + public function __construct( + ResourceConnection $resource, + PageRepositoryInterface $pageRepository, + SearchCriteriaBuilder $searchCriteriaBuilder + ) { + $this->connection = $resource; + $this->pageRepository = $pageRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + self::ENTITY_TYPE + )->where( + 'entity_id IN (?)', + $this->getPageIdsByStore((int) $value) + ); + + return $this->connection->getConnection()->fetchCol($sql); + } + + /** + * Get page ids by store + * + * @param int $storeId + * @return array + * @throws LocalizedException + */ + private function getPageIdsByStore(int $storeId): array + { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(self::STORE_FIELD, $storeId) + ->create(); + + $searchResult = $this->pageRepository->getList($searchCriteria); + + return array_map(function (PageInterface $page) { + return $page->getId(); + }, $searchResult->getItems()); + } +} diff --git a/app/code/Magento/MediaContentCms/Observer/Block.php b/app/code/Magento/MediaContentCms/Observer/Block.php index ccd1abb98bc60..48db872f4056f 100644 --- a/app/code/Magento/MediaContentCms/Observer/Block.php +++ b/app/code/Magento/MediaContentCms/Observer/Block.php @@ -12,6 +12,7 @@ use Magento\Framework\Event\ObserverInterface; use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Model\Config; /** * Observe cms_block_save_after event and run processing relation between cms block content and media asset @@ -28,6 +29,11 @@ class Block implements ObserverInterface */ private $updateContentAssetLinks; + /** + * @var Config + */ + private $config; + /** * @var array */ @@ -41,14 +47,17 @@ class Block implements ObserverInterface /** * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param Config $config * @param array $fields */ public function __construct( ContentIdentityInterfaceFactory $contentIdentityFactory, UpdateContentAssetLinksInterface $updateContentAssetLinks, + Config $config, array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; + $this->config = $config; $this->updateContentAssetLinks = $updateContentAssetLinks; $this->fields = $fields; } @@ -60,6 +69,9 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } $model = $observer->getEvent()->getData('object'); if ($model instanceof CmsBlock) { diff --git a/app/code/Magento/MediaContentCms/Observer/BlockDelete.php b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php new file mode 100644 index 0000000000000..b9be5c54d79bd --- /dev/null +++ b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Observer; + +use Magento\Cms\Model\Block as CmsBlock; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\MediaContentApi\Model\Config; + +/** + * Observe the adminhtml_cmspage_on_delete event and deletes relation between page content and media asset. + */ +class BlockDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'cms_block'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var Config + */ + private $config; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param Config $config + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + Config $config, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->config = $config; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + if (!$this->config->isEnabled()) { + return; + } + + $block = $observer->getEvent()->getData('object'); + $contentAssetLinks = []; + + if ($block instanceof CmsBlock) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $block->getId(), + ] + ); + $assets = $this->extractAssetsFromContent->execute((string) $block->getData($field)); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCms/Observer/Page.php b/app/code/Magento/MediaContentCms/Observer/Page.php index 4c0ed5c628d1c..ef83d6bb58718 100644 --- a/app/code/Magento/MediaContentCms/Observer/Page.php +++ b/app/code/Magento/MediaContentCms/Observer/Page.php @@ -12,6 +12,7 @@ use Magento\Framework\Event\ObserverInterface; use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Model\Config; /** * Observe cms_page_save_after event and run processing relation between cms page content and media asset. @@ -28,6 +29,11 @@ class Page implements ObserverInterface */ private $updateContentAssetLinks; + /** + * @var Config + */ + private $config; + /** * @var array */ @@ -41,15 +47,18 @@ class Page implements ObserverInterface /** * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param Config $config * @param array $fields */ public function __construct( ContentIdentityInterfaceFactory $contentIdentityFactory, UpdateContentAssetLinksInterface $updateContentAssetLinks, + Config $config, array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->config = $config; $this->fields = $fields; } @@ -60,6 +69,10 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } + $model = $observer->getEvent()->getData('object'); if ($model instanceof CmsPage) { diff --git a/app/code/Magento/MediaContentCms/Observer/PageDelete.php b/app/code/Magento/MediaContentCms/Observer/PageDelete.php new file mode 100644 index 0000000000000..e3e59f0991a75 --- /dev/null +++ b/app/code/Magento/MediaContentCms/Observer/PageDelete.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Observer; + +use Magento\Cms\Model\Page as CmsPage; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\MediaContentApi\Model\Config; + +/** + * Observe the cms_page_delete_before event and deletes relation between page content and media asset. + */ +class PageDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'cms_page'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @var Config + */ + private $config; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param Config $config + * @param arry $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + Config $config, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->config = $config; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + if (!$this->config->isEnabled()) { + return; + } + + $page = $observer->getEvent()->getData('object'); + $contentAssetLinks = []; + + if ($page instanceof CmsPage) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $page->getId(), + ] + ); + + $assets = $this->extractAssetsFromContent->execute((string) $page->getData($field)); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCms/etc/di.xml b/app/code/Magento/MediaContentCms/etc/di.xml index f980936465faf..c157fbf22b7ad 100644 --- a/app/code/Magento/MediaContentCms/etc/di.xml +++ b/app/code/Magento/MediaContentCms/etc/di.xml @@ -20,4 +20,48 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentCms\Observer\PageDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentCms\Observer\BlockDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> + <virtualType name="Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByPageStatus" type="Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByContentField"> + <arguments> + <argument name="entityType" xsi:type="string">cms_page</argument> + <argument name="fieldTable" xsi:type="string">cms_page</argument> + <argument name="idColumn" xsi:type="string">page_id</argument> + <argument name="fieldColumn" xsi:type="string">is_active</argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByBlockStatus" type="Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByContentField"> + <arguments> + <argument name="entityType" xsi:type="string">cms_block</argument> + <argument name="fieldTable" xsi:type="string">cms_block</argument> + <argument name="idColumn" xsi:type="string">block_id</argument> + <argument name="fieldColumn" xsi:type="string">is_active</argument> + </arguments> + </virtualType> + <type name="Magento\MediaContentApi\Model\Composite\GetAssetIdsByContentField"> + <arguments> + <argument name="fieldHandlers" xsi:type="array"> + <item name="content_status" xsi:type="array"> + <item name="getAssetIdsByPageStatus" xsi:type="object">Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByPageStatus</item> + <item name="getAssetIdsByBlockStatus" xsi:type="object">Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByBlockStatus</item> + </item> + <item name="store_id" xsi:type="array"> + <item name="getAssetIdsByPageStore" xsi:type="object">Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByPageStore</item> + <item name="getAssetIdsByBlockStore" xsi:type="object">Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByBlockStore</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaContentCms/etc/events.xml b/app/code/Magento/MediaContentCms/etc/events.xml index 7e9abe3bf19c4..94f963f40be15 100644 --- a/app/code/Magento/MediaContentCms/etc/events.xml +++ b/app/code/Magento/MediaContentCms/etc/events.xml @@ -6,8 +6,14 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="cms_page_delete_before"> + <observer name="media_content_cms_page_delete_before" instance="Magento\MediaContentCms\Observer\PageDelete" /> + </event> <event name="cms_page_save_after"> <observer name="media_content_cms_page_save_after" instance="Magento\MediaContentCms\Observer\Page" /> + </event> + <event name="cms_block_delete_before"> + <observer name="media_content_cms_block_delete_before" instance="Magento\MediaContentCms\Observer\BlockDelete" /> </event> <event name="cms_block_save_after"> <observer name="media_content_cms_block_save_after" instance="Magento\MediaContentCms\Observer\Block" /> diff --git a/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php new file mode 100644 index 0000000000000..e591b4f2339b1 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Console\Command; + +use Magento\Framework\Console\Cli; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Synchronize content with assets + */ +class Synchronize extends Command +{ + /** + * @var SynchronizeInterface + */ + private $synchronizeContent; + + /** + * @param SynchronizeInterface $synchronizeContent + */ + public function __construct( + SynchronizeInterface $synchronizeContent + ) { + $this->synchronizeContent = $synchronizeContent; + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $this->setName('media-content:sync'); + $this->setDescription('Synchronize content with assets'); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Synchronizing content with assets...'); + $this->synchronizeContent->execute(); + $output->writeln('Completed content synchronization.'); + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/LICENSE.txt b/app/code/Magento/MediaContentSynchronization/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/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/MediaContentSynchronization/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronization/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/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/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php new file mode 100644 index 0000000000000..b01c02cae4234 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +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; + +/** + * Media content synchronization queue consumer. + */ +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( + 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(OperationInterface $operation) : void + { + $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 new file mode 100644 index 0000000000000..d9e89fea7d4d2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +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. + */ +class Publish +{ + /** + * Media content synchronization queue topic name. + */ + 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( + 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 + * + * @param array $contentIdentities + */ + 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, + $operation + ); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php b/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php new file mode 100644 index 0000000000000..e81817282dcc0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Model; + +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentSynchronization\Model\ResourceModel\GetOutdatedRelations; +use Magento\MediaContentSynchronizationApi\Model\GetEntitiesInterface; + +/** + * Remove obsolete content asset from deleted entities + */ +class RemoveObsoleteContentAsset +{ + /** + * @var GetEntitiesInterface + */ + private $getEntities; + + /** + * @var GetOutdatedRelations + */ + private $getOutdatedRelations; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param GetEntitiesInterface $getEntities + * @param GetOutdatedRelations $getOutdatedRelations + */ + public function __construct( + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + GetEntitiesInterface $getEntities, + GetOutdatedRelations $getOutdatedRelations + ) { + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->getEntities = $getEntities; + $this->getOutdatedRelations = $getOutdatedRelations; + } + + /** + * Remove media content if entity already deleted. + */ + public function execute(): void + { + foreach ($this->getEntities->execute() as $entity) { + $assetsLinks = $this->getOutdatedRelations->execute($entity); + if (!empty($assetsLinks)) { + $this->deleteContentAssetLinks->execute($assetsLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php b/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php new file mode 100644 index 0000000000000..37271ce469715 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\CouldNotDeleteException; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterface; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Psr\Log\LoggerInterface; + +/** + * Returns asset links which entities has been deleted. + */ +class GetOutdatedRelations +{ + private const MEDIA_CONTENT_ASSET_TABLE = 'media_content_asset'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param MetadataPool $metadataPool + * @param ResourceConnection $resourceConnection + * @param LoggerInterface $logger + */ + public function __construct( + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + MetadataPool $metadataPool, + ResourceConnection $resourceConnection, + LoggerInterface $logger + ) { + $this->contentIdentityFactory = $contentIdentityFactory; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->metadataPool = $metadataPool; + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + } + + /** + * Returns content asset links wichs entity_id not exist anymore. + * + * @param string $entityType + * @throws CouldNotDeleteException + * @return ContentAssetLinkInterface[] + */ + public function execute(string $entityType): array + { + $contentAssetLinks= []; + try { + $entityData = $this->metadataPool->getMetadata($entityType); + $connection = $this->resourceConnection->getConnection(); + $mediaContentTable = $this->resourceConnection->getTableName(self::MEDIA_CONTENT_ASSET_TABLE); + $select = $connection->select(); + + $select->from(['mca' => $mediaContentTable], ['asset_id', 'entity_id', 'entity_type', 'field']); + $select->joinLeft( + ['et' => $entityData->getEntityTable()], + 'et.' . $entityData->getIdentifierField() . ' = mca.entity_id ', + [$entityData->getIdentifierField() . ' AS entity_identifier'] + ); + $select->where('et.' . $entityData->getIdentifierField() . ' IS NULL'); + $select->where('mca.entity_type = ?', $entityData->getEavEntityType() ?? $entityData->getEntityTable()); + $assets = $connection->fetchAll($select); + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new LocalizedException(__('Could not fetch media content links data'), $exception); + } + + foreach ($assets as $asset) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => $asset['entity_type'], + 'entityId' => $asset['entity_id'], + 'field' => $asset['field'] + ] + ); + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset['asset_id'], + 'contentIdentity' => $contentIdentity + ] + ); + } + + return $contentAssetLinks; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php b/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php new file mode 100644 index 0000000000000..cea8cc6ad44da --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php @@ -0,0 +1,109 @@ +<?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\SynchronizeInterface; +use Magento\MediaContentSynchronizationApi\Model\SynchronizerPool; +use Psr\Log\LoggerInterface; + +/** + * Synchronize content with assets + */ +class Synchronize implements SynchronizeInterface +{ + private const LAST_EXECUTION_TIME_CODE = 'media_content_last_execution'; + + /** + * @var DateTimeFactory + */ + private $dateFactory; + + /** + * @var FlagManager + */ + private $flagManager; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var SynchronizerPool + */ + private $synchronizerPool; + + /** + * @var RemoveObsoleteContentAsset + */ + private $removeObsoleteContent; + + /** + * @param RemoveObsoleteContentAsset $removeObsoleteContent + * @param DateTimeFactory $dateFactory + * @param FlagManager $flagManager + * @param LoggerInterface $log + * @param SynchronizerPool $synchronizerPool + */ + public function __construct( + RemoveObsoleteContentAsset $removeObsoleteContent, + DateTimeFactory $dateFactory, + FlagManager $flagManager, + LoggerInterface $log, + SynchronizerPool $synchronizerPool + ) { + $this->removeObsoleteContent = $removeObsoleteContent; + $this->dateFactory = $dateFactory; + $this->flagManager = $flagManager; + $this->log = $log; + $this->synchronizerPool = $synchronizerPool; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $failed = []; + + foreach ($this->synchronizerPool->get() as $name => $synchronizer) { + try { + $synchronizer->execute(); + } 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) + ] + ) + ); + } + + $this->setLastExecutionTime(); + $this->removeObsoleteContent->execute(); + } + + /** + * Set last synchronizer execution time + */ + private function setLastExecutionTime(): void + { + $currentTime = $this->dateFactory->create()->gmtDate(); + $this->flagManager->saveFlag(self::LAST_EXECUTION_TIME_CODE, $currentTime); + } +} 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/Plugin/SynchronizeMediaContent.php b/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php new file mode 100644 index 0000000000000..e428f7d273bb4 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Plugin; + +use Magento\MediaContentSynchronization\Model\Publish; +use Magento\MediaGallerySynchronization\Model\Consume; + +/** + * Run media content synchronization after the media files consumer finish files synchronization. + */ +class SynchronizeMediaContent +{ + /** + * @var Publish + */ + private $publish; + + /** + * @param Publish $publish + */ + public function __construct(Publish $publish) + { + $this->publish = $publish; + } + + /** + * Publish content synchronization request message to the queue. + * + * @param Consume $subject + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute(Consume $subject): void + { + $this->publish->execute(); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/README.md b/app/code/Magento/MediaContentSynchronization/README.md new file mode 100644 index 0000000000000..69098ab02eb0b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/README.md @@ -0,0 +1,14 @@ +# Magento_MediaContentSynchronization module + +The Magento_MediaContentSynchronization module represents implementation of synchronization between data and objects contains +media asset information. + +## Extensibility + +Extension developers can interact with the Magento_MediaContentSynchronization 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_MediaContentSynchronization 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/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 new file mode 100644 index 0000000000000..9f0f4f9588ad6 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-media-content-synchronization", + "description": "Magento module provides implementation of the media content data synchronization.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/framework-bulk": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-content-api": "*", + "magento/module-asynchronous-operations": "*" + }, + "suggest": { + "magento/module-media-gallery-synchronization": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronization\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/etc/communication.xml b/app/code/Magento/MediaContentSynchronization/etc/communication.xml new file mode 100644 index 0000000000000..05641b7432564 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/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.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> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml new file mode 100644 index 0000000000000..e5347f1a11561 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml @@ -0,0 +1,22 @@ +<?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\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"> + <item name="mediaContentSynchronization" xsi:type="object">Magento\MediaContentSynchronization\Console\Command\Synchronize</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronization\Model\Consume"> + <plugin name="synchronize_media_content" + type="Magento\MediaContentSynchronization\Plugin\SynchronizeMediaContent"/> + </type> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/etc/module.xml b/app/code/Magento/MediaContentSynchronization/etc/module.xml new file mode 100644 index 0000000000000..7f04d9b57d8a0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/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_MediaContentSynchronization" /> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml new file mode 100644 index 0000000000000..6a141c04c59a0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/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.content.synchronization" queue="media.content.synchronization" + connection="db" handler="Magento\MediaContentSynchronization\Model\Consume::execute"/> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml new file mode 100644 index 0000000000000..9751d1161b2f2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/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.content.synchronization"> + <connection name="db" exchange="magento-db" disabled="false" /> + </publisher> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml new file mode 100644 index 0000000000000..4dc43ef1ac13f --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/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="MediaContentSynchronization" topic="media.content.synchronization" + destinationType="queue" destination="media.content.synchronization"/> + </exchange> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/registration.php b/app/code/Magento/MediaContentSynchronization/registration.php new file mode 100644 index 0000000000000..a157f7ec90a6a --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/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_MediaContentSynchronization', + __DIR__ +); 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/Api/SynchronizeInterface.php b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeInterface.php new file mode 100644 index 0000000000000..759f226660278 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Api; + +/** + * Synchronize assets and contents + */ +interface SynchronizeInterface +{ + /** + * Synchronize assets and contents + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(): void; +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/LICENSE.txt b/app/code/Magento/MediaContentSynchronizationApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/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/MediaContentSynchronizationApi/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/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/MediaContentSynchronizationApi/Model/GetEntities.php b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php new file mode 100644 index 0000000000000..38129b2b1c6b9 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Model; + +/** + * Configuration of entities used for media content. + */ +class GetEntities implements GetEntitiesInterface +{ + /** + * @var array + */ + private $entities; + + /** + * @param array $entities + */ + public function __construct( + array $entities = [] + ) { + $this->entities = $entities; + } + + /** + * Get all entities configuration used in media content. + * + * @return array + */ + public function execute(): array + { + return $this->entities; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php new file mode 100644 index 0000000000000..ad62ae4136378 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Model; + +/** + * Get entities for media content by provided configuration. + */ +interface GetEntitiesInterface +{ + /** + * Get entities that used for media content + * + * @return array + */ + public function execute(): array; +} 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/Model/SynchronizerPool.php b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerPool.php new file mode 100644 index 0000000000000..ca18214201c6a --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerPool.php @@ -0,0 +1,51 @@ +<?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\SynchronizeInterface; + +/** + * A pool that handles content and assets synchronization. + * @see SynchronizeFilesInterface + */ +class SynchronizerPool +{ + /** + * Content with assets synchronizers + * + * @var SynchronizeInterface[] + */ + private $synchronizers; + + /** + * @param SynchronizeInterface[] $synchronizers + */ + public function __construct( + array $synchronizers = [] + ) { + foreach ($synchronizers as $synchronizer) { + if (!$synchronizer instanceof SynchronizeInterface) { + throw new \InvalidArgumentException( + get_class($synchronizer) . ' must implement ' . SynchronizeInterface::class + ); + } + } + + $this->synchronizers = $synchronizers; + } + + /** + * Get all synchronizers from the pool + * + * @return SynchronizeInterface[] + */ + public function get(): array + { + return $this->synchronizers; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/README.md b/app/code/Magento/MediaContentSynchronizationApi/README.md new file mode 100644 index 0000000000000..25ceae24452f1 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentSynchronizationApi module + +The Magento_MediaContentSynchronizationApi module is responsible for the media gallery data synchronization implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaContentSynchronizationApi 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_MediaContentSynchronizationApi 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/MediaContentSynchronizationApi/composer.json b/app/code/Magento/MediaContentSynchronizationApi/composer.json new file mode 100644 index 0000000000000..398aaf1de8071 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-media-content-synchronization-api", + "description": "Magento module responsible for the media content synchronization implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml b/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml new file mode 100644 index 0000000000000..76bdd9b1cb162 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/etc/di.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:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaContentSynchronizationApi\Model\GetEntitiesInterface" type="Magento\MediaContentSynchronizationApi\Model\GetEntities"/> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml b/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml new file mode 100644 index 0000000000000..3a149b31da3cb --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/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_MediaContentSynchronizationApi" /> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationApi/registration.php b/app/code/Magento/MediaContentSynchronizationApi/registration.php new file mode 100644 index 0000000000000..965e31fa45516 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/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_MediaContentSynchronizationApi', + __DIR__ +); diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE.txt b/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/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/MediaContentSynchronizationCatalog/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/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/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php new file mode 100644 index 0000000000000..6b8f99ee6721c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php @@ -0,0 +1,112 @@ +<?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\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; + +/** + * Synchronize category content with assets + */ +class Category implements SynchronizeInterface +{ + private const CONTENT_TYPE = 'catalog_category'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + private const CATEGORY_TABLE = 'catalog_category_entity'; + private const CATEGORY_IDENTITY_FIELD = 'entity_id'; + private const CATEGORY_UPDATED_AT_FIELD = 'updated_at'; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetEntityContentsInterface + */ + private $getEntityContents; + + /** + * @var FetchBatchesInterface + */ + private $fetchBatches; + + /** + * @var array + */ + private $fields; + + /** + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param GetEntityContentsInterface $getEntityContents + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param FetchBatchesInterface $fetchBatches + * @param array $fields + */ + public function __construct( + ContentIdentityInterfaceFactory $contentIdentityFactory, + GetEntityContentsInterface $getEntityContents, + UpdateContentAssetLinksInterface $updateContentAssetLinks, + FetchBatchesInterface $fetchBatches, + array $fields = [] + ) { + $this->contentIdentityFactory = $contentIdentityFactory; + $this->getEntityContents = $getEntityContents; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + $this->fetchBatches = $fetchBatches; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = [ + self::CATEGORY_IDENTITY_FIELD, + self::CATEGORY_UPDATED_AT_FIELD + ]; + foreach ($this->fetchBatches->execute(self::CATEGORY_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize product entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CATEGORY_IDENTITY_FIELD] + ] + ); + $this->updateContentAssetLinks->execute( + $contentIdentity, + implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)) + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php new file mode 100644 index 0000000000000..486f3482b592d --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php @@ -0,0 +1,109 @@ +<?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\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; + +/** + * Synchronize product content with assets + */ +class Product implements SynchronizeInterface +{ + private const CONTENT_TYPE = 'catalog_product'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + private const PRODUCT_TABLE = 'catalog_product_entity'; + private const PRODUCT_TABLE_ENTITY_ID = 'entity_id'; + private const PRODUCT_TABLE_UPDATED_AT_FIELD = 'updated_at'; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetEntityContentsInterface + */ + private $getEntityContents; + + /** + * @var array + */ + private $fields; + + /** + * @var FetchBatchesInterface + */ + private $fetchBatches; + + /** + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param GetEntityContentsInterface $getEntityContents + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param FetchBatchesInterface $fetchBatches + * @param array $fields + */ + public function __construct( + ContentIdentityInterfaceFactory $contentIdentityFactory, + GetEntityContentsInterface $getEntityContents, + UpdateContentAssetLinksInterface $updateContentAssetLinks, + FetchBatchesInterface $fetchBatches, + array $fields = [] + ) { + $this->contentIdentityFactory = $contentIdentityFactory; + $this->getEntityContents = $getEntityContents; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fetchBatches = $fetchBatches; + $this->fields = $fields; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = [self::PRODUCT_TABLE_ENTITY_ID, self::PRODUCT_TABLE_UPDATED_AT_FIELD]; + foreach ($this->fetchBatches->execute(self::PRODUCT_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize product entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::PRODUCT_TABLE_ENTITY_ID] + ] + ); + $this->updateContentAssetLinks->execute( + $contentIdentity, + implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)) + ); + } + } +} 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/README.md b/app/code/Magento/MediaContentSynchronizationCatalog/README.md new file mode 100644 index 0000000000000..8395ffc10d4d2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentCatalog module + +The Magento_MediaContentCatalog provides the implementation of MediaContentSyncronization functionality for Magento_Catalog module + +## Extensibility + +Extension developers can interact with the Magento_MediaContent 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_MediaContent 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/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php new file mode 100644 index 0000000000000..b8f12bad6bd77 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php @@ -0,0 +1,85 @@ +<?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\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Category; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for categories synchronization + */ +class CategoryTest extends TestCase +{ + /** + * @var Category + */ + private $synchronizer; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->synchronizer = Bootstrap::getObjectManager()->get(Category::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between category and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $categoryId = 28767; + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => $categoryId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertEquals($categoryId, $syncedContentIdentity->getEntityId()); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php new file mode 100644 index 0000000000000..247fdf4a770ee --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php @@ -0,0 +1,85 @@ +<?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\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Product; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for products synchronization + */ +class ProductTest extends TestCase +{ + /** + * @var Product + */ + private $synchronizer; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->synchronizer = Bootstrap::getObjectManager()->get(Product::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between products and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $productId = 1567; + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => $productId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertEquals($productId, $syncedContentIdentity->getEntityId()); + } + } +} 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/composer.json b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json new file mode 100644 index 0000000000000..733f29d3a42c2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-content-synchronization-catalog", + "description": "Magento module provides the implementation of MediaContentSynchronization functionality for Magento_Catalog module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationCatalog\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml new file mode 100644 index 0000000000000..070f25f501712 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml @@ -0,0 +1,50 @@ +<?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\MediaContentSynchronizationCatalog\Model\Synchronizer\Category"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="image" xsi:type="string">image</item> + <item name="description" xsi:type="string">description</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationApi\Model\GetEntitiesInterface"> + <arguments> + <argument name="entities" xsi:type="array"> + <item name="catalog_product" xsi:type="string">Magento\Catalog\Api\Data\ProductInterface</item> + <item name="catalog_category" xsi:type="string">Magento\Catalog\Api\Data\CategoryInterface</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Product"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="description" xsi:type="string">description</item> + <item name="short_description" xsi:type="string">short_description</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizerPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_content_category_synchronizer" xsi:type="object">Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Category</item> + <item name="media_content_product_synchronizer" xsi:type="object">Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Product</item> + </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/MediaContentSynchronizationCatalog/etc/module.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml new file mode 100644 index 0000000000000..9660dcb107b45 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/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_MediaContentSynchronizationCatalog" /> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/registration.php b/app/code/Magento/MediaContentSynchronizationCatalog/registration.php new file mode 100644 index 0000000000000..1e8b47dc15b50 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/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_MediaContentSynchronizationCatalog', + __DIR__ +); diff --git a/app/code/Magento/MediaContentSynchronizationCms/LICENSE.txt b/app/code/Magento/MediaContentSynchronizationCms/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/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/MediaContentSynchronizationCms/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationCms/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/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/MediaContentSynchronizationCms/Model/Synchronizer/Block.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php new file mode 100644 index 0000000000000..c3da5d4ae5785 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Model\Synchronizer; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; + +/** + * Synchronize block content with assets + */ +class Block implements SynchronizeInterface +{ + private const CONTENT_TYPE = 'cms_block'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + private const CMS_BLOCK_TABLE = 'cms_block'; + private const CMS_BLOCK_TABLE_ENTITY_ID = 'block_id'; + private const CMS_BLOCK_TABLE_UPDATED_AT_FIELD = 'update_time'; + + /** + * @var FetchBatchesInterface + */ + private $fetchBatches; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var array + */ + private $fields; + + /** + * Synchronize block content with assets + * + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param FetchBatchesInterface $fetchBatches + * @param array $fields + */ + public function __construct( + ContentIdentityInterfaceFactory $contentIdentityFactory, + UpdateContentAssetLinksInterface $updateContentAssetLinks, + FetchBatchesInterface $fetchBatches, + array $fields = [] + ) { + $this->contentIdentityFactory = $contentIdentityFactory; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + $this->fetchBatches = $fetchBatches; + } + + /** + * Synchronize assets and contents + */ + public function execute(): void + { + $columns = array_merge( + [ + self::CMS_BLOCK_TABLE_ENTITY_ID, + self::CMS_BLOCK_TABLE_UPDATED_AT_FIELD + ], + array_values($this->fields) + ); + foreach ($this->fetchBatches->execute(self::CMS_BLOCK_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize block entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $this->updateContentAssetLinks->execute( + $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CMS_BLOCK_TABLE_ENTITY_ID] + ] + ), + (string) $item[$field] + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php new file mode 100644 index 0000000000000..2d1b04d295973 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Model\Synchronizer; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; + +/** + * Synchronize page content with assets + */ +class Page implements SynchronizeInterface +{ + private const CONTENT_TYPE = 'cms_page'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + private const CMS_PAGE_TABLE = 'cms_page'; + private const CMS_PAGE_TABLE_ENTITY_ID = 'page_id'; + private const CMS_PAGE_TABLE_UPDATED_AT_FIELD = 'update_time'; + + /** + * @var FetchBatchesInterface + */ + private $fetchBatches; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var array + */ + private $fields; + + /** + * Synchronize page content with assets + * + * @param FetchBatchesInterface $fetchBatches + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param array $fields + */ + public function __construct( + FetchBatchesInterface $fetchBatches, + ContentIdentityInterfaceFactory $contentIdentityFactory, + UpdateContentAssetLinksInterface $updateContentAssetLinks, + array $fields = [] + ) { + $this->fetchBatches = $fetchBatches; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = array_merge( + [ + self::CMS_PAGE_TABLE_ENTITY_ID, + self::CMS_PAGE_TABLE_UPDATED_AT_FIELD + ], + array_values($this->fields) + ); + foreach ($this->fetchBatches->execute(self::CMS_PAGE_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize page entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $this->updateContentAssetLinks->execute( + $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CMS_PAGE_TABLE_ENTITY_ID] + ] + ), + (string) $item[$field] + ); + } + } +} 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/README.md b/app/code/Magento/MediaContentSynchronizationCms/README.md new file mode 100644 index 0000000000000..58582b1b2d706 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentCms module + +The Magento_MediaContentCms provides the implementation of MediaContentSyncronization functionality for Magento_Cms module + +## Extensibility + +Extension developers can interact with the Magento_MediaContent 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_MediaContent 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/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php new file mode 100644 index 0000000000000..2737ab524584b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php @@ -0,0 +1,109 @@ +<?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\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationCms\Model\Synchronizer\Block; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for blocks synchronization + */ +class BlockTest extends TestCase +{ + /** + * @var Block + */ + private $synchronizer; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->synchronizer = Bootstrap::getObjectManager()->get(Block::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between blocks and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $blockId = $this->getBlock('fixture_block_with_asset')->getId(); + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'cms_block', + 'field' => 'content', + 'entityId' => $blockId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + $this->assertEquals($blockId, $synchronizedContentIdentities[0]->getEntityId()); + } + + /** + * 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()); + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php new file mode 100644 index 0000000000000..1dcbb96dc7914 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php @@ -0,0 +1,109 @@ +<?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\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationCms\Model\Synchronizer\Page; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for pages synchronization + */ +class PageTest extends TestCase +{ + /** + * @var Page + */ + private $synchronizer; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->synchronizer = Bootstrap::getObjectManager()->get(Page::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between pages and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $pageId = $this->getPage('fixture_page_with_asset')->getId(); + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'cms_page', + 'field' => 'content', + 'entityId' => $pageId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + $this->assertEquals($pageId, $synchronizedContentIdentities[0]->getEntityId()); + } + + /** + * 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/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/composer.json b/app/code/Magento/MediaContentSynchronizationCms/composer.json new file mode 100644 index 0000000000000..9028b9dacd0a2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-content-synchronization-cms", + "description": "Magento module provides the implementation of MediaContentSynchronization functionality for Magento_Cms module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationCms\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml new file mode 100644 index 0000000000000..d6e7604c71d97 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml @@ -0,0 +1,48 @@ +<?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\MediaContentSynchronizationApi\Model\SynchronizerPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_content_block_synchronizer" xsi:type="object">Magento\MediaContentSynchronizationCms\Model\Synchronizer\Block</item> + <item name="media_content_page_synchronizer" xsi:type="object">Magento\MediaContentSynchronizationCms\Model\Synchronizer\Page</item> + </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"> + <item name="cms_block" xsi:type="string">Magento\Cms\Api\Data\BlockInterface</item> + <item name="cms_page" xsi:type="string">Magento\Cms\Api\Data\PageInterface</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationCms\Model\Synchronizer\Block"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationCms\Model\Synchronizer\Page"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml new file mode 100644 index 0000000000000..58497b81a2174 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/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_MediaContentSynchronizationCms" /> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationCms/registration.php b/app/code/Magento/MediaContentSynchronizationCms/registration.php new file mode 100644 index 0000000000000..13ed4b73f70ee --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/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_MediaContentSynchronizationCms', + __DIR__ +); diff --git a/app/code/Magento/MediaGallery/Model/Asset.php b/app/code/Magento/MediaGallery/Model/Asset.php index 78b9477a70b08..7a4e51709dc0a 100644 --- a/app/code/Magento/MediaGallery/Model/Asset.php +++ b/app/code/Magento/MediaGallery/Model/Asset.php @@ -32,11 +32,21 @@ class Asset implements AssetInterface */ private $title; + /** + * @var string|null + */ + private $description; + /** * @var string|null */ private $source; + /** + * @var string|null + */ + private $hash; + /** * @var string */ @@ -80,7 +90,9 @@ class Asset implements AssetInterface * @param int $size * @param int|null $id * @param string|null $title + * @param string|null $description * @param string|null $source + * @param string|null $hash * @param string|null $createdAt * @param string|null $updatedAt * @param AssetExtensionInterface|null $extensionAttributes @@ -93,7 +105,9 @@ public function __construct( int $size, ?int $id = null, ?string $title = null, + ?string $description = null, ?string $source = null, + ?string $hash = null, ?string $createdAt = null, ?string $updatedAt = null, ?AssetExtensionInterface $extensionAttributes = null @@ -105,7 +119,9 @@ public function __construct( $this->size = $size; $this->id = $id; $this->title = $title; + $this->description = $description; $this->source = $source; + $this->hash = $hash; $this->createdAt = $createdAt; $this->updatedAt = $updatedAt; $this->extensionAttributes = $extensionAttributes; @@ -135,6 +151,14 @@ public function getTitle(): ?string return $this->title; } + /** + * @inheritdoc + */ + public function getDescription(): ?string + { + return $this->description; + } + /** * @inheritdoc */ @@ -143,6 +167,14 @@ public function getSource(): ?string return $this->source; } + /** + * @inheritdoc + */ + public function getHash(): ?string + { + return $this->hash; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByDirectoryPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByDirectoryPath.php index 3abe4cb50f2ea..51e4b0c1cb24d 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByDirectoryPath.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByDirectoryPath.php @@ -15,7 +15,7 @@ /** * Remove asset(s) that correspond the provided directory path - * @deprecated use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead * @see \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterfac */ class DeleteByDirectoryPath implements DeleteByDirectoryPathInterface diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php index fc8e5d7c84bfd..898d31a304804 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php @@ -16,7 +16,7 @@ /** * Delete media asset by path * - * @deprecated use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead * @see \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface */ class DeleteByPath implements DeleteByPathInterface diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php index b2f900233e46a..81bcbb7fe28a8 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php @@ -17,7 +17,7 @@ /** * Get media asset by id - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead * @see \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface */ class GetById implements GetByIdInterface @@ -94,7 +94,9 @@ public function execute(int $mediaAssetId): AssetInterface 'id' => $mediaAssetData['id'], 'path' => $mediaAssetData['path'], 'title' => $mediaAssetData['title'], + 'description' => $mediaAssetData['description'], 'source' => $mediaAssetData['source'], + 'hash' => $mediaAssetData['hash'], 'contentType' => $mediaAssetData['content_type'], 'width' => $mediaAssetData['width'], 'height' => $mediaAssetData['height'], diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php index d9faad62b2cd1..aabc3986a21d4 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php @@ -18,7 +18,7 @@ /** * Provide media asset by path * - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface instead * @see \Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface */ class GetByPath implements GetByPathInterface @@ -86,7 +86,9 @@ public function execute(string $path): AssetInterface 'id' => $data['id'], 'path' => $data['path'], 'title' => $data['title'], + 'description' => $data['description'], 'source' => $data['source'], + 'hash' => $data['hash'], 'contentType' => $data['content_type'], 'width' => $data['width'], 'height' => $data['height'], diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php b/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php index 1710176c1b3af..ba8497fe49205 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php @@ -16,7 +16,7 @@ /** * Save media asset * - * @deprecated use \Magento\MediaGalleryApi\Api\SaveAssetsInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\SaveAssetsInterface instead * @see \Magento\MediaGalleryApi\Api\SaveAssetsInterface */ class Save implements SaveInterface diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php index 4d87c1aa95285..d0ba786c7084e 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php @@ -10,7 +10,7 @@ use Magento\Cms\Model\Wysiwyg\Images\Storage; use Magento\Framework\Exception\CouldNotSaveException; use Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; use Psr\Log\LoggerInterface; /** @@ -29,23 +29,23 @@ class CreateByPaths implements CreateDirectoriesByPathsInterface private $storage; /** - * @var IsPathBlacklistedInterface + * @var IsPathExcludedInterface */ - private $isPathBlacklisted; + private $isPathExcluded; /** * @param LoggerInterface $logger * @param Storage $storage - * @param IsPathBlacklistedInterface $isPathBlacklisted + * @param IsPathExcludedInterface $isPathExcluded */ public function __construct( LoggerInterface $logger, Storage $storage, - IsPathBlacklistedInterface $isPathBlacklisted + IsPathExcludedInterface $isPathExcluded ) { $this->logger = $logger; $this->storage = $storage; - $this->isPathBlacklisted = $isPathBlacklisted; + $this->isPathExcluded = $isPathExcluded; } /** @@ -55,7 +55,7 @@ public function execute(array $paths): void { $failedPaths = []; foreach ($paths as $path) { - if ($this->isPathBlacklisted->execute($path)) { + if ($this->isPathExcluded->execute($path)) { $failedPaths[] = $path; continue; } @@ -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/Directory/Command/DeleteByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php index d46fb854fff22..2e45000c07225 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php @@ -10,7 +10,7 @@ use Magento\Cms\Model\Wysiwyg\Images\Storage; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; use Psr\Log\LoggerInterface; /** @@ -29,23 +29,23 @@ class DeleteByPaths implements DeleteDirectoriesByPathsInterface private $storage; /** - * @var IsPathBlacklistedInterface + * @var IsPathExcludedInterface */ - private $isPathBlacklisted; + private $isPathExcluded; /** * @param LoggerInterface $logger * @param Storage $storage - * @param IsPathBlacklistedInterface $isPathBlacklisted + * @param IsPathExcludedInterface $isPathExcluded */ public function __construct( LoggerInterface $logger, Storage $storage, - IsPathBlacklistedInterface $isPathBlacklisted + IsPathExcludedInterface $isPathExcluded ) { $this->logger = $logger; $this->storage = $storage; - $this->isPathBlacklisted = $isPathBlacklisted; + $this->isPathExcluded = $isPathExcluded; } /** @@ -55,7 +55,7 @@ public function execute(array $paths): void { $failedPaths = []; foreach ($paths as $path) { - if ($this->isPathBlacklisted->execute($path)) { + if ($this->isPathExcluded->execute($path)) { $failedPaths[] = $path; continue; } diff --git a/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php b/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php index 91f16d246f636..3d9911c805efb 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php @@ -15,9 +15,9 @@ class Converter implements ConverterInterface { /** - * Blacklist tag name + * Excluded list tag name */ - private const BLACKLIST_TAG_NAME = 'blacklist'; + private const EXCLUDED_LIST_TAG_NAME = 'exclude'; /** * Patterns tag name @@ -43,12 +43,12 @@ public function convert($source): array throw new \InvalidArgumentException('The source should be instance of DOMDocument'); } - foreach ($source->getElementsByTagName(self::BLACKLIST_TAG_NAME) as $blacklist) { - $result[self::BLACKLIST_TAG_NAME] = []; - foreach ($blacklist->getElementsByTagName(self::PATTERNS_TAG_NAME) as $patterns) { - $result[self::BLACKLIST_TAG_NAME][self::PATTERNS_TAG_NAME] = []; + foreach ($source->getElementsByTagName(self::EXCLUDED_LIST_TAG_NAME) as $excludedList) { + $result[self::EXCLUDED_LIST_TAG_NAME] = []; + foreach ($excludedList->getElementsByTagName(self::PATTERNS_TAG_NAME) as $patterns) { + $result[self::EXCLUDED_LIST_TAG_NAME][self::PATTERNS_TAG_NAME] = []; foreach ($patterns->getElementsByTagName(self::PATTERN_TAG_NAME) as $pattern) { - $result[self::BLACKLIST_TAG_NAME][self::PATTERNS_TAG_NAME] + $result[self::EXCLUDED_LIST_TAG_NAME][self::PATTERNS_TAG_NAME] [$pattern->attributes->getNamedItem('name')->nodeValue] = $pattern->nodeValue; } } diff --git a/app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php b/app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php similarity index 68% rename from app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php rename to app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php index 8fdd4f70d5060..29ed5fbf04ecd 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php +++ b/app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php @@ -8,14 +8,14 @@ namespace Magento\MediaGallery\Model\Directory; use Magento\Framework\Config\DataInterface; -use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface; +use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface; /** * Media gallery directory config */ -class BlacklistPatternsConfig implements BlacklistPatternsConfigInterface +class ExcludedPatternsConfig implements ExcludedPatternsConfigInterface { - private const XML_PATH_BLACKLIST_PATTERNS = 'blacklist/patterns'; + private const XML_PATH_EXCLUDED_PATTERNS = 'exclude/patterns'; /** * @var DataInterface @@ -37,6 +37,6 @@ public function __construct(DataInterface $data) */ public function get() : array { - return $this->data->get(self::XML_PATH_BLACKLIST_PATTERNS); + return $this->data->get(self::XML_PATH_EXCLUDED_PATTERNS); } } diff --git a/app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php b/app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php similarity index 61% rename from app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php rename to app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php index 0191b357aaefa..8fb0e03b76548 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php +++ b/app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php @@ -7,23 +7,23 @@ namespace Magento\MediaGallery\Model\Directory; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; -use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface; /** - * Check if the path is blacklisted for media gallery. Directory path may be blacklisted if it's reserved by the system + * Check if the path is excluded for media gallery. Directory path may be blacklisted if it's reserved by the system */ -class IsBlacklisted implements IsPathBlacklistedInterface +class IsExcluded implements IsPathExcludedInterface { /** - * @var BlacklistPatternsConfigInterface + * @var ExcludedPatternsConfigInterface */ private $config; /** - * @param BlacklistPatternsConfigInterface $config + * @param ExcludedPatternsConfigInterface $config */ - public function __construct(BlacklistPatternsConfigInterface $config) + public function __construct(ExcludedPatternsConfigInterface $config) { $this->config = $config; } diff --git a/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php b/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php index 4118fd1495dbb..a37eaa8fc11fa 100644 --- a/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php +++ b/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php @@ -16,7 +16,7 @@ /** * Retrieve keywords for the media asset - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead */ class GetAssetKeywords implements GetAssetKeywordsInterface { diff --git a/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php b/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php index f21db25bac767..aa9f05af70b2f 100644 --- a/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php +++ b/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php @@ -18,7 +18,7 @@ /** * Save media asset keywords to database - * @deprecated use \Magento\MediaGalleryApi\Api\SaveAssetKeywordsInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\SaveAssetKeywordsInterface instead */ class SaveAssetKeywords implements SaveAssetKeywordsInterface { diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php index 53185939b2283..f73162b775683 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php @@ -65,7 +65,9 @@ public function execute(array $ids): array 'id' => $assetData['id'], 'path' => $assetData['path'], 'title' => $assetData['title'], + 'description' => $assetData['description'], 'source' => $assetData['source'], + 'hash' => $assetData['hash'], 'contentType' => $assetData['content_type'], 'width' => $assetData['width'], 'height' => $assetData['height'], diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php index 5593083d9673a..b25d2e22aabd4 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php @@ -66,7 +66,9 @@ public function execute(array $paths): array 'id' => $assetData['id'], 'path' => $assetData['path'], 'title' => $assetData['title'], + 'description' => $assetData['description'], 'source' => $assetData['source'], + 'hash' => $assetData['hash'], 'contentType' => $assetData['content_type'], 'width' => $assetData['width'], 'height' => $assetData['height'], diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsBySearchCriteria.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsBySearchCriteria.php new file mode 100644 index 0000000000000..3f3aaac17947d --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsBySearchCriteria.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model\ResourceModel; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\ResourceConnection; +use Psr\Log\LoggerInterface; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Api\Search\SearchResultFactory; +use Magento\Framework\DB\Select; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Get assets data by searchCriteria + */ +class GetAssetsBySearchCriteria +{ + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var SearchResultFactory + */ + private $searchResultFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param SearchResultFactory $searchResultFactory + * @param ResourceConnection $resourceConnection + * @param LoggerInterface $logger + */ + public function __construct( + SearchResultFactory $searchResultFactory, + ResourceConnection $resourceConnection, + LoggerInterface $logger + ) { + $this->searchResultFactory = $searchResultFactory; + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + } + + /** + * Retrieve assets data from database + * + * @param SearchCriteriaInterface $searchCriteria + * @return SearchResultInterface + */ + public function execute(SearchCriteriaInterface $searchCriteria): SearchResultInterface + { + $searchResult = $this->searchResultFactory->create(); + $fields = []; + $conditions = []; + + foreach ($searchCriteria->getFilterGroups() as $filterGroup) { + foreach ($filterGroup->getFilters() as $filter) { + $condition = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; + $fields[] = $filter->getField(); + + if ($condition === 'fulltext') { + $condition = 'like'; + $filter->setValue('%' . $filter->getValue() . '%'); + } + + $conditions[] = [$condition => $filter->getValue()]; + } + } + + if ($fields) { + $resultCondition = $this->getResultCondition($fields, $conditions); + $select = $this->resourceConnection->getConnection()->select() + ->from( + $this->resourceConnection->getTableName(self::TABLE_MEDIA_GALLERY_ASSET) + ) + ->where($resultCondition, null, Select::TYPE_CONDITION); + + if ($searchCriteria->getPageSize() || $searchCriteria->getCurrentPage()) { + $select->limit( + $searchCriteria->getPageSize(), + $searchCriteria->getCurrentPage() * $searchCriteria->getPageSize() + ); + } + + $data = $this->resourceConnection->getConnection()->fetchAll($select); + } + + $searchResult->setSearchCriteria($searchCriteria); + $searchResult->setItems($data); + + return $searchResult; + } + + /** + * Get conditions data by searchCriteria + * + * @param string|array $field + * @param null|string|array $condition + */ + public function getResultCondition($field, $condition = null) + { + $resourceConnection = $this->resourceConnection->getConnection(); + if (is_array($field)) { + $conditions = []; + foreach ($field as $key => $value) { + $conditions[] = $resourceConnection->prepareSqlCondition( + $resourceConnection->quoteIdentifier($value), + isset($condition[$key]) ? $condition[$key] : null + ); + } + + $resultCondition = '(' . implode(') ' . Select::SQL_OR . ' (', $conditions) . ')'; + } else { + $resultCondition = $resourceConnection->prepareSqlCondition( + $resourceConnection->quoteIdentifier($field), + $condition + ); + } + return $resultCondition; + } +} diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php index 3437cc1c519e8..87f9359d4fc37 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php @@ -10,8 +10,10 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; +use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; use Psr\Log\LoggerInterface; /** @@ -22,55 +24,90 @@ class SaveAssetLinks private const TABLE_ASSET_KEYWORD = 'media_gallery_asset_keyword'; private const FIELD_ASSET_ID = 'asset_id'; private const FIELD_KEYWORD_ID = 'keyword_id'; + private const TABLE_MEDIA_ASSET = 'media_gallery_asset'; /** * @var ResourceConnection */ private $resourceConnection; + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetsKeywords; + /** * @var LoggerInterface */ private $logger; /** + * @param GetAssetsKeywordsInterface $getAssetsKeywords * @param ResourceConnection $resourceConnection * @param LoggerInterface $logger */ public function __construct( + GetAssetsKeywordsInterface $getAssetsKeywords, ResourceConnection $resourceConnection, LoggerInterface $logger ) { + $this->getAssetsKeywords = $getAssetsKeywords; $this->resourceConnection = $resourceConnection; $this->logger = $logger; } /** - * Save asset keywords links + * Process insert and deletion of asset keywords links * * @param int $assetId * @param KeywordInterface[] $keywordIds * + * @throws CouldNotDeleteException * @throws CouldNotSaveException */ public function execute(int $assetId, array $keywordIds): void { + $currentKeywordIds = $this->getCurrentKeywordIds($assetId); + + $obsoleteKeywordIds = array_diff($currentKeywordIds, $keywordIds); + $newKeywordIds = array_diff($keywordIds, $currentKeywordIds); + + $this->deleteAssetKeywords($assetId, $obsoleteKeywordIds); + $this->insertAssetKeywords($assetId, $newKeywordIds); + + if ($obsoleteKeywordIds || $newKeywordIds) { + $this->setAssetUpdatedAt($assetId); + } + } + + /** + * Save new asset keyword links + * + * @param int $assetId + * @param int[] $keywordIds + * + * @throws CouldNotSaveException + */ + private function insertAssetKeywords(int $assetId, array $keywordIds): void + { + if (empty($keywordIds)) { + return; + } try { $values = []; + foreach ($keywordIds as $keywordId) { $values[] = [$assetId, $keywordId]; } - if (!empty($values)) { - /** @var Mysql $connection */ - $connection = $this->resourceConnection->getConnection(); - $connection->insertArray( - $this->resourceConnection->getTableName(self::TABLE_ASSET_KEYWORD), - [self::FIELD_ASSET_ID, self::FIELD_KEYWORD_ID], - $values, - AdapterInterface::INSERT_IGNORE - ); - } + /** @var Mysql $connection */ + $connection = $this->resourceConnection->getConnection(); + $connection->insertArray( + $this->resourceConnection->getTableName(self::TABLE_ASSET_KEYWORD), + [self::FIELD_ASSET_ID, self::FIELD_KEYWORD_ID], + $values, + AdapterInterface::INSERT_IGNORE + ); } catch (\Exception $exception) { $this->logger->critical($exception); throw new CouldNotSaveException( @@ -79,4 +116,96 @@ public function execute(int $assetId, array $keywordIds): void ); } } + + /** + * Delete obsolete asset keyword links + * + * @param int $assetId + * @param int[] $obsoleteKeywordIds + * @throws CouldNotDeleteException + */ + private function deleteAssetKeywords(int $assetId, array $obsoleteKeywordIds): void + { + if (empty($obsoleteKeywordIds)) { + return; + } + try { + /** @var Mysql $connection */ + $connection = $this->resourceConnection->getConnection(); + $connection->delete( + $this->resourceConnection->getTableName( + self::TABLE_ASSET_KEYWORD + ), + [ + self::FIELD_KEYWORD_ID . ' in (?)' => $obsoleteKeywordIds, + self::FIELD_ASSET_ID . ' = ?' => $assetId + ] + ); + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new CouldNotDeleteException( + __('Could not delete obsolete asset keyword links'), + $exception + ); + } + } + + /** + * Get current keyword ids of an asset + * + * @param int $assetId + * @return int[] + */ + private function getCurrentKeywordIds(int $assetId): array + { + $currentKeywordsData = $this->getAssetsKeywords->execute([$assetId]); + + if (empty($currentKeywordsData)) { + return []; + } + + return $this->getKeywordIdsFromKeywordData( + $currentKeywordsData[$assetId]->getKeywords() + ); + } + + /** + * Get keyword ids from keyword data + * + * @param KeywordInterface[] $keywordsData + * @return int[] + */ + private function getKeywordIdsFromKeywordData(array $keywordsData): array + { + return array_map( + function (KeywordInterface $keyword): int { + return $keyword->getId(); + }, + $keywordsData + ); + } + + /** + * Updates modified date of media asset + * + * @param int $assetId + * @throws CouldNotSaveException + */ + private function setAssetUpdatedAt(int $assetId): void + { + try { + $connection = $this->resourceConnection->getConnection(); + $connection->update( + $this->resourceConnection->getTableName(self::TABLE_MEDIA_ASSET), + ['updated_at' => null], + ['id =?' => $assetId] + ); + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new CouldNotSaveException( + __('Could not update assets modified date'), + $exception + ); + } + } } diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetsKeywords.php b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetsKeywords.php index a97c5f602c5c7..56bdfda49d84c 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetsKeywords.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetsKeywords.php @@ -93,19 +93,17 @@ private function saveAssetKeywords(array $keywords, int $assetId): void $data[] = $keyword->getKeyword(); } - if (empty($data)) { - return; + if (!empty($data)) { + /** @var Mysql $connection */ + $connection = $this->resourceConnection->getConnection(); + $connection->insertArray( + $this->resourceConnection->getTableName(self::TABLE_KEYWORD), + [self::KEYWORD], + $data, + AdapterInterface::INSERT_IGNORE + ); } - /** @var Mysql $connection */ - $connection = $this->resourceConnection->getConnection(); - $connection->insertArray( - $this->resourceConnection->getTableName(self::TABLE_KEYWORD), - [self::KEYWORD], - $data, - AdapterInterface::INSERT_IGNORE - ); - $this->saveAssetLinks->execute($assetId, $this->getKeywordIds($data)); } diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php b/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php index ec08addf93462..801279aa7fd7d 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php @@ -60,7 +60,9 @@ public function execute(array $assets): void 'id' => $asset->getId(), 'path' => $asset->getPath(), 'title' => $asset->getTitle(), + 'description' => $asset->getDescription(), 'source' => $asset->getSource(), + 'hash' => $asset->getHash(), 'content_type' => $asset->getContentType(), 'width' => $asset->getWidth(), 'height' => $asset->getHeight(), diff --git a/app/code/Magento/MediaGallery/Model/SearchAssets.php b/app/code/Magento/MediaGallery/Model/SearchAssets.php new file mode 100644 index 0000000000000..69678e3cacc13 --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/SearchAssets.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Psr\Log\LoggerInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\MediaGallery\Model\ResourceModel\GetAssetsBySearchCriteria; +use Magento\MediaGalleryApi\Api\SearchAssetsInterface; + +/** + * Get media assets by searchCriteria + */ +class SearchAssets implements SearchAssetsInterface +{ + /** + * @var GetAssetsBySearchCriteria + */ + private $getAssetsBySearchCriteria; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var AssetInterfaceFactory + */ + private $mediaAssetFactory; + + /** + * @param GetAssetsBySearchCriteria $getAssetsBySearchCriteria + * @param AssetInterfaceFactory $mediaAssetFactory + * @param LoggerInterface $logger + */ + public function __construct( + GetAssetsBySearchCriteria $getAssetsBySearchCriteria, + AssetInterfaceFactory $mediaAssetFactory, + LoggerInterface $logger + ) { + $this->getAssetsBySearchCriteria = $getAssetsBySearchCriteria; + $this->mediaAssetFactory = $mediaAssetFactory; + $this->logger = $logger; + } + + /** + * @inheritdoc + */ + public function execute(SearchCriteriaInterface $searchCriteria): array + { + $assets = []; + try { + foreach ($this->getAssetsBySearchCriteria->execute($searchCriteria)->getItems() as $assetData) { + $assets[] = $this->mediaAssetFactory->create( + [ + 'id' => $assetData['id'], + 'path' => $assetData['path'], + 'title' => $assetData['title'], + 'description' => $assetData['description'], + 'source' => $assetData['source'], + 'hash' => $assetData['hash'], + 'contentType' => $assetData['content_type'], + 'width' => $assetData['width'], + 'height' => $assetData['height'], + 'size' => $assetData['size'], + 'createdAt' => $assetData['created_at'], + 'updatedAt' => $assetData['updated_at'], + ] + ); + } + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new LocalizedException(__('Could not retrieve media assets'), $exception->getMessage()); + } + return $assets; + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php index 09ce7ffe8ff20..5f99163db8f12 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php @@ -28,7 +28,9 @@ class GetByIdExceptionDuringMediaAssetInitializationTest extends TestCase 'id' => 45, 'path' => 'img.jpg', 'title' => 'Img', + 'description' => 'Img Description', 'source' => 'Adobe Stock', + 'hash' => 'hash', 'content_type' => 'image/jpeg', 'width' => 420, 'height' => 240, diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php index 89efae07360b4..3b47b0036224b 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php @@ -29,7 +29,9 @@ class GetByIdExceptionOnGetDataTest extends TestCase 'id' => 45, 'path' => 'img.jpg', 'title' => 'Img', + 'description' => 'Img Description', 'source' => 'Adobe Stock', + 'hash' => 'hash', 'content_type' => 'image/jpeg', 'width' => 420, 'height' => 240, diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php index 8b805d0256e37..2c24899746473 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php @@ -29,7 +29,9 @@ class GetByIdSuccessfulTest extends TestCase 'id' => 45, 'path' => 'img.jpg', 'title' => 'Img', + 'description' => 'Img Description', 'source' => 'Adobe Stock', + 'hash' => 'hash', 'content_type' => 'image/jpeg', 'width' => 420, 'height' => 240, diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php similarity index 70% rename from app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php rename to app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php index c96fd2ee54512..cc57b043954d7 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php @@ -8,45 +8,45 @@ namespace Magento\MediaGallery\Test\Unit\Model\Directory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\MediaGallery\Model\Directory\IsBlacklisted; -use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface; +use Magento\MediaGallery\Model\Directory\IsExcluded; +use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** - * Test for IsBlacklisted + * Test for IsExcluded */ -class IsBlacklistedTest extends TestCase +class IsExcludedTest extends TestCase { /** - * @var IsBlacklisted + * @var IsExcluded */ private $object; /** - * @var BlacklistPatternsConfigInterface|MockObject + * @var ExcludedPatternsConfigInterface|MockObject */ - private $config; + private $configMock; /** * Initialize basic test class mocks */ protected function setUp(): void { - $this->config = $this->getMockBuilder(BlacklistPatternsConfigInterface::class) + $this->configMock = $this->getMockBuilder(ExcludedPatternsConfigInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->config->expects($this->at(0))->method('get')->willReturn([ + $this->configMock->expects($this->at(0))->method('get')->willReturn([ 'tmp' => '/pub\/media\/tmp/', 'captcha' => '/pub\/media\/captcha/' ]); - $this->object = (new ObjectManager($this))->getObject(IsBlacklisted::class, [ - 'config' => $this->config + $this->object = (new ObjectManager($this))->getObject(IsExcluded::class, [ + 'config' => $this->configMock ]); } /** - * Test if the directory path is blacklisted + * Test if the directory path is excluded * * @param string $path * @param bool $isExcluded 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 95fdac5bdafa5..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 @@ -11,6 +11,7 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Exception\CouldNotSaveException; use Magento\MediaGallery\Model\ResourceModel\Keyword\SaveAssetLinks; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -32,6 +33,11 @@ class SaveAssetLinksTest extends TestCase */ private $resourceConnectionMock; + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetsKeywords; + /** * @var LoggerInterface|MockObject */ @@ -44,9 +50,11 @@ protected function setUp(): void { $this->connectionMock = $this->getMockForAbstractClass(AdapterInterface::class); $this->resourceConnectionMock = $this->createMock(ResourceConnection::class); + $this->getAssetsKeywords = $this->getMockForAbstractClass(GetAssetsKeywordsInterface::class); $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); $this->sut = new SaveAssetLinks( + $this->getAssetsKeywords, $this->resourceConnectionMock, $this->loggerMock ); @@ -60,19 +68,24 @@ protected function setUp(): void * @param int $assetId * @param array $keywordIds * @param array $values + * @throws CouldNotSaveException */ public function testAssetKeywordsSave(int $assetId, array $keywordIds, array $values): void { $expectedCalls = (int) (count($keywordIds)); if ($expectedCalls) { - $this->resourceConnectionMock->expects($this->once()) + $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 31a764ef00c4d..1a9b0dc96a655 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema.xml +++ b/app/code/Magento/MediaGallery/etc/db_schema.xml @@ -8,9 +8,11 @@ <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"/> + <column xsi:type="varchar" name="hash" length="255" nullable="true" comment="File hash"/> <column xsi:type="varchar" name="content_type" length="255" nullable="true" comment="Content Type"/> <column xsi:type="int" name="width" unsigned="true" nullable="false" identity="false" default="0" comment="Width"/> <column xsi:type="int" name="height" unsigned="true" nullable="false" identity="false" default="0" comment="Height"/> @@ -23,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 8f5098caa9753..e958d630b7e3f 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json +++ b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json @@ -4,7 +4,9 @@ "id": true, "path": true, "title": true, + "description": true, "source": true, + "hash": true, "content_type": true, "width": true, "height": true, @@ -18,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/MediaGallery/etc/di.xml b/app/code/Magento/MediaGallery/etc/di.xml index a85c26e275226..b040bf9b35da3 100644 --- a/app/code/Magento/MediaGallery/etc/di.xml +++ b/app/code/Magento/MediaGallery/etc/di.xml @@ -21,7 +21,7 @@ <preference for="Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface" type="Magento\MediaGallery\Model\Directory\Command\CreateByPaths"/> <preference for="Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface" type="Magento\MediaGallery\Model\Directory\Command\DeleteByPaths"/> - <preference for="Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface" type="Magento\MediaGallery\Model\Directory\IsBlacklisted"/> + <preference for="Magento\MediaGalleryApi\Api\IsPathExcludedInterface" type="Magento\MediaGallery\Model\Directory\IsExcluded"/> <preference for="Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface" type="Magento\MediaGallery\Model\ResourceModel\DeleteAssetsByPaths"/> <preference for="Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface" type="Magento\MediaGallery\Model\ResourceModel\GetAssetsByIds"/> @@ -29,6 +29,7 @@ <preference for="Magento\MediaGalleryApi\Api\SaveAssetsInterface" type="Magento\MediaGallery\Model\ResourceModel\SaveAssets"/> <preference for="Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface" type="Magento\MediaGallery\Model\ResourceModel\Keyword\GetAssetsKeywords"/> <preference for="Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface" type="Magento\MediaGallery\Model\ResourceModel\Keyword\SaveAssetsKeywords"/> + <preference for="Magento\MediaGalleryApi\Api\SearchAssetsInterface" type="Magento\MediaGallery\Model\SearchAssets"/> <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> <plugin name="media_gallery_image_remove_metadata_after_wysiwyg" type="Magento\MediaGallery\Plugin\Wysiwyg\Images\Storage" @@ -40,7 +41,7 @@ <argument name="converter" xsi:type="object">Magento\MediaGallery\Model\Directory\Config\Converter</argument> <argument name="schemaLocator" xsi:type="object">Magento\MediaGallery\Model\Directory\Config\SchemaLocator</argument> <argument name="idAttributes" xsi:type="array"> - <item name="/config/blacklist/patterns/pattern" xsi:type="string">name</item> + <item name="/config/exclude/patterns/pattern" xsi:type="string">name</item> </argument> </arguments> </virtualType> @@ -50,11 +51,10 @@ <argument name="cacheId" xsi:type="string">Media_Gallery_Patterns_CacheId</argument> </arguments> </virtualType> - <type name="Magento\MediaGallery\Model\Directory\BlacklistPatternsConfig"> + <type name="Magento\MediaGallery\Model\Directory\ExcludedPatternsConfig"> <arguments> <argument name="data" xsi:type="object">Magento\MediaGallery\Model\Directory\Config\Data</argument> </arguments> </type> - - <preference for="Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface" type="Magento\MediaGallery\Model\Directory\BlacklistPatternsConfig"/> + <preference for="Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface" type="Magento\MediaGallery\Model\Directory\ExcludedPatternsConfig"/> </config> diff --git a/app/code/Magento/MediaGallery/etc/directory.xml b/app/code/Magento/MediaGallery/etc/directory.xml index 92f50b2dd0a30..42094aff72640 100644 --- a/app/code/Magento/MediaGallery/etc/directory.xml +++ b/app/code/Magento/MediaGallery/etc/directory.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_MediaGalleryApi:etc/directory.xsd"> - <blacklist> + <exclude> <patterns> <pattern name="captcha">/^captcha/</pattern> <pattern name="customer">/^customer/</pattern> @@ -17,5 +17,5 @@ <pattern name="tmp">/^tmp/</pattern> <pattern name="directories-with-dots">/^\./</pattern> </patterns> - </blacklist> + </exclude> </config> diff --git a/app/code/Magento/MediaGalleryApi/Api/CreateDirectoriesByPathsInterface.php b/app/code/Magento/MediaGalleryApi/Api/CreateDirectoriesByPathsInterface.php index a0a1ec891237f..20e57cfa2d138 100644 --- a/app/code/Magento/MediaGalleryApi/Api/CreateDirectoriesByPathsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/CreateDirectoriesByPathsInterface.php @@ -10,6 +10,7 @@ /** * Create folders by provided paths * @api + * @since 101.0.0 */ interface CreateDirectoriesByPathsInterface { @@ -19,6 +20,7 @@ interface CreateDirectoriesByPathsInterface * @param string[] $paths * @return void * @throws \Magento\Framework\Exception\CouldNotSaveException + * @since 101.0.0 */ public function execute(array $paths): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php index 5df420a274933..41d682ed1bc6b 100644 --- a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php @@ -14,6 +14,7 @@ * Represents a media gallery asset which contains information about a media asset entity such * as path to the media storage, media asset title and its content type, etc. * @api + * @since 100.3.0 */ interface AssetInterface extends ExtensibleDataInterface { @@ -21,6 +22,7 @@ interface AssetInterface extends ExtensibleDataInterface * Get ID * * @return int|null + * @since 100.3.0 */ public function getId(): ?int; @@ -28,6 +30,7 @@ public function getId(): ?int; * Get Path * * @return string + * @since 100.3.0 */ public function getPath(): string; @@ -35,20 +38,37 @@ public function getPath(): string; * Get title * * @return string|null + * @since 100.3.0 */ public function getTitle(): ?string; + /** + * Get description + * + * @return string|null + */ + public function getDescription(): ?string; + /** * Get the name of the channel/stock/integration file was retrieved from. null if not identified. * * @return string|null + * @since 100.3.0 */ public function getSource(): ?string; + /** + * Get file hash + * + * @return string|null + */ + public function getHash(): ?string; + /** * Get content type * * @return string + * @since 100.3.0 */ public function getContentType(): string; @@ -56,6 +76,7 @@ public function getContentType(): string; * Retrieve full licensed asset's height * * @return int + * @since 100.3.0 */ public function getHeight(): int; @@ -63,6 +84,7 @@ public function getHeight(): int; * Retrieve full licensed asset's width * * @return int + * @since 100.3.0 */ public function getWidth(): int; @@ -70,6 +92,7 @@ public function getWidth(): int; * Retrieve asset file size in bytes * * @return int + * @since 101.0.0 */ public function getSize(): int; @@ -77,6 +100,7 @@ public function getSize(): int; * Get created at * * @return string|null + * @since 100.3.0 */ public function getCreatedAt(): ?string; @@ -84,6 +108,7 @@ public function getCreatedAt(): ?string; * Get updated at * * @return string|null + * @since 100.3.0 */ public function getUpdatedAt(): ?string; @@ -91,6 +116,7 @@ public function getUpdatedAt(): ?string; * Retrieve existing extension attributes object or create a new one. * * @return \Magento\MediaGalleryApi\Api\Data\AssetExtensionInterface|null + * @since 100.3.0 */ public function getExtensionAttributes(): ?AssetExtensionInterface; @@ -99,6 +125,7 @@ public function getExtensionAttributes(): ?AssetExtensionInterface; * * @param \Magento\MediaGalleryApi\Api\Data\AssetExtensionInterface|null $extensionAttributes * @return void + * @since 100.3.0 */ public function setExtensionAttributes(?AssetExtensionInterface $extensionAttributes): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/AssetKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/AssetKeywordsInterface.php index 1c18225470493..f303f723981d5 100644 --- a/app/code/Magento/MediaGalleryApi/Api/Data/AssetKeywordsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/Data/AssetKeywordsInterface.php @@ -13,6 +13,7 @@ /** * Interface for asset's keywords aggregation * @api + * @since 101.0.0 */ interface AssetKeywordsInterface extends ExtensibleDataInterface { @@ -20,6 +21,7 @@ interface AssetKeywordsInterface extends ExtensibleDataInterface * Get ID * * @return int + * @since 101.0.0 */ public function getAssetId(): int; @@ -27,6 +29,7 @@ public function getAssetId(): int; * Get the keyword * * @return KeywordInterface[] + * @since 101.0.0 */ public function getKeywords(): array; @@ -34,6 +37,7 @@ public function getKeywords(): array; * Get extension attributes * * @return \Magento\MediaGalleryApi\Api\Data\AssetKeywordsExtensionInterface|null + * @since 101.0.0 */ public function getExtensionAttributes(): ?AssetKeywordsExtensionInterface; @@ -42,6 +46,7 @@ public function getExtensionAttributes(): ?AssetKeywordsExtensionInterface; * * @param \Magento\MediaGalleryApi\Api\Data\AssetKeywordsExtensionInterface|null $extensionAttributes * @return void + * @since 101.0.0 */ public function setExtensionAttributes(?AssetKeywordsExtensionInterface $extensionAttributes): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php index 3cba118e03a1a..3f3c583fc182c 100644 --- a/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php @@ -13,6 +13,7 @@ /** * Represents a media gallery keyword. This object contains information about a media asset keyword entity. * @api + * @since 100.3.0 */ interface KeywordInterface extends ExtensibleDataInterface { @@ -20,6 +21,7 @@ interface KeywordInterface extends ExtensibleDataInterface * Get ID * * @return int|null + * @since 100.3.0 */ public function getId(): ?int; @@ -27,6 +29,7 @@ public function getId(): ?int; * Get the keyword * * @return string + * @since 100.3.0 */ public function getKeyword(): string; @@ -34,6 +37,7 @@ public function getKeyword(): string; * Get extension attributes * * @return \Magento\MediaGalleryApi\Api\Data\KeywordExtensionInterface|null + * @since 100.3.0 */ public function getExtensionAttributes(): ?KeywordExtensionInterface; @@ -42,6 +46,7 @@ public function getExtensionAttributes(): ?KeywordExtensionInterface; * * @param \Magento\MediaGalleryApi\Api\Data\KeywordExtensionInterface|null $extensionAttributes * @return void + * @since 100.3.0 */ public function setExtensionAttributes(?KeywordExtensionInterface $extensionAttributes): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/DeleteAssetsByPathsInterface.php b/app/code/Magento/MediaGalleryApi/Api/DeleteAssetsByPathsInterface.php index 5370235a31b95..3e824bdaffd6f 100644 --- a/app/code/Magento/MediaGalleryApi/Api/DeleteAssetsByPathsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/DeleteAssetsByPathsInterface.php @@ -11,6 +11,7 @@ /** * Delete media assets by exact or directory paths * @api + * @since 101.0.0 */ interface DeleteAssetsByPathsInterface { @@ -19,6 +20,7 @@ interface DeleteAssetsByPathsInterface * * @param string[] $paths * @return void + * @since 101.0.0 */ public function execute(array $paths): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/DeleteDirectoriesByPathsInterface.php b/app/code/Magento/MediaGalleryApi/Api/DeleteDirectoriesByPathsInterface.php index fe3be88fa0073..c3c1c0ad577a7 100644 --- a/app/code/Magento/MediaGalleryApi/Api/DeleteDirectoriesByPathsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/DeleteDirectoriesByPathsInterface.php @@ -10,6 +10,7 @@ /** * Delete folders by provided paths * @api + * @since 101.0.0 */ interface DeleteDirectoriesByPathsInterface { @@ -19,6 +20,7 @@ interface DeleteDirectoriesByPathsInterface * @param string[] $paths * @return void * @throws \Magento\Framework\Exception\CouldNotDeleteException + * @since 101.0.0 */ public function execute(array $paths): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/GetAssetsByIdsInterface.php b/app/code/Magento/MediaGalleryApi/Api/GetAssetsByIdsInterface.php index 5df6722a190d4..0c0ea7c812ce9 100644 --- a/app/code/Magento/MediaGalleryApi/Api/GetAssetsByIdsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/GetAssetsByIdsInterface.php @@ -11,6 +11,7 @@ /** * Get media gallery assets by id attribute * @api + * @since 101.0.0 */ interface GetAssetsByIdsInterface { @@ -20,6 +21,7 @@ interface GetAssetsByIdsInterface * @param int[] $ids * @return \Magento\MediaGalleryApi\Api\Data\AssetInterface[] * @throws \Magento\Framework\Exception\LocalizedException + * @since 101.0.0 */ public function execute(array $ids): array; } diff --git a/app/code/Magento/MediaGalleryApi/Api/GetAssetsByPathsInterface.php b/app/code/Magento/MediaGalleryApi/Api/GetAssetsByPathsInterface.php index dbaed6e0e9123..458d004fe74f8 100644 --- a/app/code/Magento/MediaGalleryApi/Api/GetAssetsByPathsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/GetAssetsByPathsInterface.php @@ -10,6 +10,7 @@ /** * Get media gallery assets by paths in media storage * @api + * @since 101.0.0 */ interface GetAssetsByPathsInterface { @@ -19,6 +20,7 @@ interface GetAssetsByPathsInterface * @param string[] $paths * @return \Magento\MediaGalleryApi\Api\Data\AssetInterface[] * @throws \Magento\Framework\Exception\LocalizedException + * @since 101.0.0 */ public function execute(array $paths): array; } diff --git a/app/code/Magento/MediaGalleryApi/Api/GetAssetsKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Api/GetAssetsKeywordsInterface.php index 99b05291f32a0..317559e447b60 100644 --- a/app/code/Magento/MediaGalleryApi/Api/GetAssetsKeywordsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/GetAssetsKeywordsInterface.php @@ -10,6 +10,7 @@ /** * Get a media gallery asset keywords related to media gallery asset ids provided * @api + * @since 101.0.0 */ interface GetAssetsKeywordsInterface { @@ -18,6 +19,7 @@ interface GetAssetsKeywordsInterface * * @param int[] $assetIds * @return \Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterface[] + * @since 101.0.0 */ public function execute(array $assetIds): array; } diff --git a/app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php b/app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php similarity index 71% rename from app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php rename to app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php index cbd23ec3fbde7..1e41debb1b1c5 100644 --- a/app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php @@ -8,12 +8,12 @@ namespace Magento\MediaGalleryApi\Api; /** - * Check if the path is blacklisted for media gallery. + * Check if the path is excluded for media gallery. * - * Directory path may be blacklisted if it's reserved by the system. + * Directory path may be excluded if it's reserved by the system. * @api */ -interface IsPathBlacklistedInterface +interface IsPathExcludedInterface { /** * Check if the path is excluded from displaying and processing in the media gallery diff --git a/app/code/Magento/MediaGalleryApi/Api/SaveAssetsInterface.php b/app/code/Magento/MediaGalleryApi/Api/SaveAssetsInterface.php index c63f7bd8c0818..823c858342a62 100644 --- a/app/code/Magento/MediaGalleryApi/Api/SaveAssetsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/SaveAssetsInterface.php @@ -11,6 +11,7 @@ /** * Save media gallery assets to the database * @api + * @since 101.0.0 */ interface SaveAssetsInterface { @@ -20,6 +21,7 @@ interface SaveAssetsInterface * @param \Magento\MediaGalleryApi\Api\Data\AssetInterface[] $assets * @return void * @throws \Magento\Framework\Exception\CouldNotSaveException + * @since 101.0.0 */ public function execute(array $assets): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/SaveAssetsKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Api/SaveAssetsKeywordsInterface.php index 04efe7d32ccc1..714a4bc605423 100644 --- a/app/code/Magento/MediaGalleryApi/Api/SaveAssetsKeywordsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/SaveAssetsKeywordsInterface.php @@ -10,6 +10,7 @@ /** * Save keywords related to assets to the database * @api + * @since 101.0.0 */ interface SaveAssetsKeywordsInterface { @@ -19,6 +20,7 @@ interface SaveAssetsKeywordsInterface * @param \Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterface[] $assetKeywords * @return void * @throws \Magento\Framework\Exception\CouldNotSaveException + * @since 101.0.0 */ public function execute(array $assetKeywords): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/SearchAssetsInterface.php b/app/code/Magento/MediaGalleryApi/Api/SearchAssetsInterface.php new file mode 100644 index 0000000000000..19c1a04f663e5 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Api/SearchAssetsInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Api; + +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Search media gallery assets by search criteria + */ +interface SearchAssetsInterface +{ + /** + * Search media gallery assets + * + * @param SearchCriteriaInterface $searchCriteria + * @return AssetsSearchResultInterface[] + * @throws LocalizedException + */ + public function execute(SearchCriteriaInterface $searchCriteria): array; +} diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByDirectoryPathInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByDirectoryPathInterface.php index 79b209823aeb0..1ed46566cfb21 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByDirectoryPathInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByDirectoryPathInterface.php @@ -11,7 +11,7 @@ /** * A command represents the media gallery assets delete action. A media gallery asset is filtered by directory * path value. - * @deprecated use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface instead * @see \Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface */ interface DeleteByDirectoryPathInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php index f33022e75d2fe..7a307a2940a0e 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php @@ -10,7 +10,7 @@ /** * A command represents the media gallery asset delete action. A media gallery asset is filtered by path value. - * @deprecated use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead * @see \Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface */ interface DeleteByPathInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php index 65cc2e3eae109..db8fd7e2baa6c 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php @@ -10,7 +10,7 @@ /** * A command represents the get media gallery asset by using media gallery asset id as a filter parameter. - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead * @see \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface */ interface GetByIdInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php index d8d5b6773fbbc..3163574336061 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php @@ -10,7 +10,7 @@ /** * A command represents the get media gallery asset by using media gallery asset path as a filter parameter. - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsByPathInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\GetAssetsByPathInterface instead * @see \Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface */ interface GetByPathInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php index 610ecf0cd22bf..f00486116b9be 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php @@ -12,7 +12,7 @@ /** * A command which executes the media gallery asset save operation. - * @deprecated use \Magento\MediaGalleryApi\Api\SaveAssetsInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\SaveAssetsInterface instead * @see \Magento\MediaGalleryApi\Api\SaveAssetsInterface */ interface SaveInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php b/app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php similarity index 75% rename from app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php rename to app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php index b4710f32e0c46..dd82f87780a49 100644 --- a/app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php @@ -7,9 +7,9 @@ namespace Magento\MediaGalleryApi\Model; /** - * Returns list of blacklist regexp patterns + * Returns list of excluded regexp patterns */ -interface BlacklistPatternsConfigInterface +interface ExcludedPatternsConfigInterface { /** * Get regexp patterns diff --git a/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php index e42c370c1c6f7..acb18f268d167 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php @@ -9,7 +9,7 @@ /** * A command represents functionality to get a media gallery asset keywords filtered by media gallery asset id. - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface instead * @see \Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface */ interface GetAssetKeywordsInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php index 824cbca178988..03cc76cc1760b 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php @@ -9,7 +9,7 @@ /** * A command represents the media gallery asset keywords save operation. - * @deprecated use \Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface instead * @see \Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface */ interface SaveAssetKeywordsInterface diff --git a/app/code/Magento/MediaGalleryApi/etc/directory.xsd b/app/code/Magento/MediaGalleryApi/etc/directory.xsd index 2ad76c8fcc9f2..2fb4fed028469 100644 --- a/app/code/Magento/MediaGalleryApi/etc/directory.xsd +++ b/app/code/Magento/MediaGalleryApi/etc/directory.xsd @@ -11,14 +11,14 @@ <xs:complexType name="configType"> <xs:sequence> - <xs:element type="blacklistType" name="blacklist" maxOccurs="unbounded" minOccurs="1"/> + <xs:element type="excludeType" name="exclude" maxOccurs="unbounded" minOccurs="1"/> </xs:sequence> </xs:complexType> - <xs:complexType name="blacklistType"> + <xs:complexType name="excludeType"> <xs:annotation> <xs:documentation> - Blacklist used for excluding directories from media gallery rendering and operations + List used for excluding directories from media gallery rendering and operations </xs:documentation> </xs:annotation> <xs:sequence> diff --git a/app/code/Magento/MediaGalleryCatalog/etc/directory.xml b/app/code/Magento/MediaGalleryCatalog/etc/directory.xml index eaced3f642f70..f1ec76a877368 100644 --- a/app/code/Magento/MediaGalleryCatalog/etc/directory.xml +++ b/app/code/Magento/MediaGalleryCatalog/etc/directory.xml @@ -6,9 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_MediaGalleryApi:etc/directory.xsd"> - <blacklist> + <exclude> <patterns> <pattern name="catalog">/^catalog\/product/</pattern> </patterns> - </blacklist> + </exclude> </config> diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE.txt b/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/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/MediaGalleryCatalogIntegration/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/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/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php new file mode 100644 index 0000000000000..b683ec8fe9d91 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCatalogIntegration\Plugin; + +use Magento\Catalog\Model\ImageUploader; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; + +/** + * Save base category image by SaveAssetsInterface. + */ +class SaveBaseCategoryImageInformation +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var DeleteAssetsByPathsInterface + */ + private $deleteAssetsByPaths; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @var Storage + */ + private $storage; + + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param DeleteAssetsByPathsInterface $deleteAssetsByPath + * @param Filesystem $filesystem + * @param GetAssetsByPathsInterface $getAssetsByPaths + * @param Storage $storage + * @param SynchronizeFilesInterface $synchronizeFiles + * @param ConfigInterface $config + */ + public function __construct( + DeleteAssetsByPathsInterface $deleteAssetsByPath, + Filesystem $filesystem, + GetAssetsByPathsInterface $getAssetsByPaths, + Storage $storage, + SynchronizeFilesInterface $synchronizeFiles, + ConfigInterface $config + ) { + $this->deleteAssetsByPaths = $deleteAssetsByPath; + $this->filesystem = $filesystem; + $this->getAssetsByPaths = $getAssetsByPaths; + $this->storage = $storage; + $this->synchronizeFiles = $synchronizeFiles; + $this->config = $config; + } + + /** + * Saves base category image information after moving from tmp folder. + * + * @param ImageUploader $subject + * @param string $imagePath + * @param string $initialImageName + * @return string + * @throws LocalizedException + */ + 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() . '/' . $initialImageName; + $tmpAssets = $this->getAssetsByPaths->execute([$tmpPath]); + + if (!empty($tmpAssets)) { + $this->deleteAssetsByPaths->execute([$tmpAssets[0]->getPath()]); + } + + $this->synchronizeFiles->execute( + [ + $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getRelativePath($absolutePath) + ] + ); + + return $imagePath; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/README.md b/app/code/Magento/MediaGalleryCatalogIntegration/README.md new file mode 100644 index 0000000000000..bcb37bd486dab --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryCatalogIntegration + +The purpose of this module is for extending catalog image uploader functionality. 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/MediaGalleryCatalogIntegration/composer.json b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json new file mode 100644 index 0000000000000..efabb70da9f39 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-media-gallery-catalog-integration", + "description": "Magento module responsible for extending catalog image uploader functionality", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-gallery-ui-api": "*" + }, + "suggest": { + "magento/module-catalog": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCatalogIntegration\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..2f8fab34911d6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/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\Catalog\Model\ImageUploader"> + <plugin name="save_category_image" type="Magento\MediaGalleryCatalogIntegration\Plugin\SaveBaseCategoryImageInformation"/> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml b/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml new file mode 100644 index 0000000000000..c9f1164121e91 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/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_MediaGalleryCatalogIntegration" /> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/registration.php b/app/code/Magento/MediaGalleryCatalogIntegration/registration.php new file mode 100644 index 0000000000000..9495790092df1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/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_MediaGalleryCatalogIntegration', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php new file mode 100644 index 0000000000000..a541e9999b784 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCatalogUi\Controller\Adminhtml\Category; + +use Magento\Backend\App\Action; +use Magento\Backend\Model\View\Result\Page; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller serving the media gallery content + */ +class Index extends Action implements HttpGetActionInterface +{ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * Get the media gallery layout + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + /** @var Page $resultPage */ + $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $resultPage->getConfig()->getTitle()->prepend(__('Categories')); + + return $resultPage; + } +} 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/LICENSE.txt b/app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/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/MediaGalleryCatalogUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/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/MediaGalleryCatalogUi/Model/Listing/DataProvider.php b/app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php new file mode 100644 index 0000000000000..e17b02ec40737 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php @@ -0,0 +1,199 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryCatalogUi\Model\Listing; + +use Magento\Catalog\Api\CategoryListInterface; +use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\Document; +use Magento\Framework\Api\Search\DocumentFactory; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\Api\Search\ReportingInterface; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchCriteriaInterface; +use Magento\Framework\Api\Search\SearchResultFactory; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider as UiComponentDataProvider; + +/** + * DataProvider of category grid. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DataProvider extends UiComponentDataProvider +{ + private const ENTITY_ID = 'entity_id'; + + /** + * @var SearchResultFactory + */ + private $searchResultFactory; + + /** + * @var CategoryListInterface + */ + private $categoryList; + + /** + * @var AttributeValueFactory + */ + private $attributeValueFactory; + + /** + * @var DocumentFactory + */ + private $documentFactory; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param ReportingInterface $reporting + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param RequestInterface $request + * @param FilterBuilder $filterBuilder + * @param SearchResultFactory $searchResultFactory + * @param CategoryListInterface $categoryList + * @param AttributeValueFactory $attributeValueFactory + * @param DocumentFactory $documentFactory + * @param FilterGroupBuilder $filterGroupBuilder + * @param array $meta + * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + string $name, + string $primaryFieldName, + string $requestFieldName, + ReportingInterface $reporting, + SearchCriteriaBuilder $searchCriteriaBuilder, + RequestInterface $request, + FilterBuilder $filterBuilder, + SearchResultFactory $searchResultFactory, + CategoryListInterface $categoryList, + AttributeValueFactory $attributeValueFactory, + DocumentFactory $documentFactory, + FilterGroupBuilder $filterGroupBuilder, + array $meta = [], + array $data = [] + ) { + parent::__construct( + $name, + $primaryFieldName, + $requestFieldName, + $reporting, + $searchCriteriaBuilder, + $request, + $filterBuilder, + $meta, + $data + ); + $this->categoryList = $categoryList; + $this->searchResultFactory = $searchResultFactory; + $this->attributeValueFactory = $attributeValueFactory; + $this->documentFactory = $documentFactory; + $this->filterGroupBuilder = $filterGroupBuilder; + } + + /** + * @inheritdoc + */ + public function getData() + { + try { + return $this->searchResultToOutput($this->getSearchResult()); + } catch (\Exception $exception) { + return [ + 'items' => [], + 'totalRecords' => 0, + 'errorMessage' => $exception->getMessage() + ]; + } + } + + /** + * @inheritDoc + */ + public function getSearchResult(): SearchResultInterface + { + $searchCriteria = $this->getSearchCriteria(); + $searchCriteria = $this->skipRootCategory($searchCriteria); + $collection = $this->categoryList->getList($searchCriteria); + $items = []; + + foreach ($collection->getItems() as $category) { + $items[] = $this->createDocument( + [ + 'entity_id' => $category->getEntityId(), + 'name' => $category->getName(), + 'image' => $category->getImage(), + 'path' => $category->getPath(), + 'display_mode' => $category->getDisplayMode(), + 'products' => $category->getProductCount(), + 'include_in_menu' => $category->getIncludeInMenu(), + 'is_active' => $category->getIsActive() + ] + ); + } + + $searchResult = $this->searchResultFactory->create(); + $searchResult->setSearchCriteria($searchCriteria); + $searchResult->setItems($items); + $searchResult->setTotalCount($collection->getTotalCount()); + + return $searchResult; + } + + /** + * Skip empty root category in collection + * + * @param SearchCriteriaInterface $searchCriteria + * @return SearchCriteriaInterface + */ + private function skipRootCategory(SearchCriteriaInterface $searchCriteria): SearchCriteriaInterface + { + $filterGroups = $searchCriteria->getFilterGroups(); + + $filters[] = $this->filterBuilder + ->setField(self::ENTITY_ID) + ->setConditionType('neq') + ->setValue(1) + ->create(); + $filterGroups[] = $this->filterGroupBuilder->setFilters($filters)->create(); + $searchCriteria->setFilterGroups($filterGroups); + return $searchCriteria; + } + + /** + * Add attributes to grid result + * + * @param array $attributes [code => value] + */ + private function createDocument(array $attributes): Document + { + $item = $this->documentFactory->create(); + $customAttributes = []; + + foreach ($attributes as $code => $value) { + $attribute = $this->attributeValueFactory->create(); + $attribute->setAttributeCode($code); + $attribute->setValue($value); + $customAttributes[$code] = $attribute; + } + + $item->setCustomAttributes($customAttributes); + + return $item; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/README.md b/app/code/Magento/MediaGalleryCatalogUi/README.md new file mode 100644 index 0000000000000..f47b031875f5d --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryCatalogUi module + +The Magento_MediaGalleryCatalogUi module that implement category grid for media gallery. + +## 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/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryPageTitleActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryPageTitleActionGroup.xml new file mode 100644 index 0000000000000..ee1d7bb5af5f4 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryPageTitleActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertCategoryPageTitleActionGroup"> + <annotations> + <description>Assert's category page title for Simple Sub Category</description> + </annotations> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeCategoryTitle"/> + </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/AdminEditCategoryInGridPageActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminEditCategoryInGridPageActionGroup.xml new file mode 100644 index 0000000000000..50ee9e890ad20 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminEditCategoryInGridPageActionGroup.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="AdminEditCategoryInGridPageActionGroup"> + <annotations> + <description>Clicks the Edit action from the Media Gallery Category Grid</description> + </annotations> + + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <click selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.edit(categoryName, 'Edit')}}" stepKey="clickOnCategoryRow"/> + <waitForPageLoad time="30" stepKey="waitForCategoryDetailsPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml new file mode 100644 index 0000000000000..2444cb314ad22 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.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="AdminOpenCategoryGridPageActionGroup"> + <annotations> + <description>Navigates to category grid page by link.</description> + </annotations> + + <amOnPage url="{{AdminMediaGalleryCatalogUiCategoryGridPage.url}}" stepKey="navigateToCategoryGridPage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + </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/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageImageColumnActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageImageColumnActionGroup.xml new file mode 100644 index 0000000000000..9cd627e900873 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageImageColumnActionGroup.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="AssertAdminCategoryGridPageImageColumnActionGroup"> + <arguments> + <argument name="file" type="string" defaultValue="magento"/> + </arguments> + <annotations> + <description>Assert category grid page image column a specific category</description> + </annotations> + + <grabAttributeFrom selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.image}}" userInput="src" + stepKey="getImageSrc"/> + <assertStringContainsString stepKey="assertImageSrc"> + <actualResult type="string">{$getImageSrc}</actualResult> + <expectedResult type="string">{{file}}</expectedResult> + </assertStringContainsString> + </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/ActionGroup/AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup.xml new file mode 100644 index 0000000000000..c9c9a25d8a2a3 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup.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="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup"> + <annotations> + <description>Assert asset filter placeholder value</description> + </annotations> + <arguments> + <argument name="filterPlaceholder" type="string"/> + </arguments> + + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.activeFilterPlaceholder(filterPlaceholder)}}" stepKey="assertFilterPLaceHolder" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml new file mode 100644 index 0000000000000..59775dd148712 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="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 new file mode 100644 index 0000000000000..96b4bad5d5add --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="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//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Image')]/preceding-sibling::th) +1]//img"/> + <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"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml new file mode 100644 index 0000000000000..2a606d8ab6a9e --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiEditCategoryGridPageTest"> + <annotations> + <features value="AdminMediaGalleryCategoryGrid"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1667"/> + <title value="User Edits Category from Category grid"/> + <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/5034526"/> + <description value="Edit Category from Media Gallery Category Grid"/> + <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> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="AdminAssertCategoryPageTitleActionGroup" stepKey="assertCategoryByName"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml new file mode 100644 index 0000000000000..a495e2ff07e6a --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.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="AdminMediaGalleryCatalogUiUsedInCategoryFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInCategoryFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in categories 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/4951846"/> + <description value="User filters assets used in categories"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <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"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <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"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploaderAgain"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Categories"/> + <argument name="optionName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml new file mode 100644 index 0000000000000..a66009e9d2045 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiUsedInProductFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInProductsFilter"/> + <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/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="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"/> + </after> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <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="AdminOpenProductIndexPageActionGroup" stepKey="openProductsGrid"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Products"/> + <argument name="optionName" value="$$product.name$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <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> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFiltersOnProductGrid"/> + + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <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}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clickDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml new file mode 100644 index 0000000000000..fde9597155d0c --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest"> + <annotations> + <features value="AdminMediaGalleryCategoryGrid"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="User sees category entities where asset is used in"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <description value="User sees category entities where asset is used in"/> + <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> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <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 new file mode 100644 index 0000000000000..f9ffda43d2547 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest"> + <annotations> + <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"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <description value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <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="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"> + <argument name="name" value="categoryImage"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"> + <argument name="name" value="categoryImage"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear" /> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetCategoryImageGalleryGridToDefaultView"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createCategoryImageFolder"> + <argument name="name" value="categoryImage"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertCategoryImageFolderCreated"> + <argument name="name" value="categoryImage"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </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="clickAddSelectedCategoryImage"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploaderToVerifyLink"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCategoryImageFolder"> + <argument name="name" value="categoryImage"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear2"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInCategories"> + <argument name="entityName" value="Categories"/> + </actionGroup> + <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <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="secondResetAdminDataGridToDefaultView"/> + + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="openCategoryImageFolder"> + <argument name="name" value="categoryImage"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerifyEmptyUsedIn"/> + <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/Columns/CategoryActions.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php new file mode 100644 index 0000000000000..0e7edd53bb45d --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\UrlInterface; + +/** + * Class CategoryActions for Category grid + */ +class CategoryActions extends Column +{ + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param UrlInterface $urlBuilder + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + UrlInterface $urlBuilder, + array $components = [], + array $data = [] + ) { + $this->urlBuilder = $urlBuilder; + parent::__construct($context, $uiComponentFactory, $components, $data); + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as &$item) { + $item[$this->getData('name')]['edit'] = [ + 'href' => $this->urlBuilder->getUrl( + 'catalog/category/edit', + [ + 'id' => $item['entity_id'] + ] + ), + 'label' => __('Edit'), + 'hidden' => false, + ]; + } + } + + return $dataSource; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/InMenu.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/InMenu.php new file mode 100644 index 0000000000000..fe4720b4a3e60 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/InMenu.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class InMenu column for Category grid + */ +class InMenu extends Column +{ + /** + * Prepare data source. + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName]) && $item[$fieldName] == 1) { + $item[$fieldName] = 'Yes'; + } else { + $item[$fieldName] = 'No'; + } + } + } + + return $dataSource; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/IsActive.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/IsActive.php new file mode 100644 index 0000000000000..c6f20c937d5b3 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/IsActive.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class IsActive column for Category grid + */ +class IsActive extends Column +{ + /** + * Prepare data source. + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName]) && $item[$fieldName] == 1) { + $item[$fieldName] = 'Yes'; + } else { + $item[$fieldName] = 'No'; + } + } + } + + return $dataSource; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php new file mode 100644 index 0000000000000..f780a116baf9e --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class Path column for Category grid + */ +class Path extends Column +{ + + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param CategoryRepositoryInterface $categoryRepository + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + CategoryRepositoryInterface $categoryRepository, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->categoryRepository = $categoryRepository; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName] = $this->getCategoryPathWithNames($item[$fieldName]); + } + } + } + + return $dataSource; + } + + /** + * Replace category path ids with category names + * + * @param string $pathWithIds + * @return string + * @throws NoSuchEntityException + */ + private function getCategoryPathWithNames(string $pathWithIds): string + { + $categoryPathWithName = ''; + $categoryIds = explode('/', $pathWithIds); + foreach ($categoryIds as $id) { + if ($id == Category::TREE_ROOT_ID) { + continue; + } + $categoryName = $this->categoryRepository->get($id)->getName(); + $categoryPathWithName .= ' / ' . $categoryName; + } + return $categoryPathWithName; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php new file mode 100644 index 0000000000000..dada8ee7acc19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Catalog\Model\Category\Image; +use Magento\Catalog\Model\CategoryRepository; +use Magento\Framework\View\Asset\Repository as AssetRepository; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class Thumbnail column for Category grid + */ +class Thumbnail extends Column +{ + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Image + */ + private $categoryImage; + + /** + * @var CategoryRepository + */ + private $categoryRepository; + + /** + * @var AssetRepository + */ + private $assetRepository; + + /** + * @var string[] + */ + private $defaultPlaceholder; + + /** + * Thumbnail constructor. + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param StoreManagerInterface $storeManager + * @param Image $categoryImage + * @param CategoryRepository $categoryRepository + * @param AssetRepository $assetRepository + * @param array $defaultPlaceholder + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + StoreManagerInterface $storeManager, + Image $categoryImage, + CategoryRepository $categoryRepository, + AssetRepository $assetRepository, + array $defaultPlaceholder = [], + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->storeManager = $storeManager; + $this->categoryImage = $categoryImage; + $this->categoryRepository = $categoryRepository; + $this->assetRepository = $assetRepository; + $this->defaultPlaceholder = $defaultPlaceholder; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function prepareDataSource(array $dataSource) + { + if (!isset($dataSource['data']['items'])) { + return $dataSource; + } + + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName . '_src'] = $this->getUrl($item[$fieldName]); + continue; + } + + if (isset($item['entity_id'])) { + $src = $this->categoryImage->getUrl( + $this->categoryRepository->get($item['entity_id']) + ); + + if (!empty($src)) { + $item[$fieldName . '_src'] = $src; + continue; + } + } + + $item[$fieldName . '_src'] = $this->assetRepository->getUrl($this->defaultPlaceholder['image']); + } + + return $dataSource; + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * @return string + * @throws LocalizedException + */ + private function getUrl(string $path): string + { + /** @var Store $store */ + $store = $this->storeManager->getStore(); + + return $store->getBaseUrl() . $path; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/composer.json b/app/code/Magento/MediaGalleryCatalogUi/composer.json new file mode 100644 index 0000000000000..985d581beff25 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/composer.json @@ -0,0 +1,26 @@ +{ + "name": "magento/module-media-gallery-catalog-ui", + "description": "Magento module that implement category grid for media gallery.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-store": "*", + "magento/module-ui": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCatalogUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..2aaf5a56cf837 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml @@ -0,0 +1,48 @@ +<?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"> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="product_id" xsi:type="object">Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Product</item> + <item name="category_id" xsi:type="object">Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Category</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Product" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_product</argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Category" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_category</argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\GetAssetUsageDetails"> + <arguments> + <argument name="contentTypes" xsi:type="array"> + <item name="catalog_category" xsi:type="array"> + <item name="name" xsi:type="string">Categories</item> + <item name="link" xsi:type="string">media_gallery_catalog/category/index</item> + </item> + <item name="catalog_product" xsi:type="array"> + <item name="name" xsi:type="string">Products</item> + <item name="link" xsi:type="string">catalog/product/index</item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\Thumbnail"> + <arguments> + <argument name="defaultPlaceholder" xsi:type="array"> + <item name="image" xsi:type="string">Magento_MediaGalleryCatalogUi::images/category/placeholder/image.jpg</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..45f1ccce1c64f --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery_catalog" frontName="media_gallery_catalog"> + <module name="Magento_MediaGalleryCatalogUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/module.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/module.xml new file mode 100644 index 0000000000000..4a593cbf10901 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/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_MediaGalleryCatalogUi" /> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/registration.php b/app/code/Magento/MediaGalleryCatalogUi/registration.php new file mode 100644 index 0000000000000..c0376e2a828d1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/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_MediaGalleryCatalogUi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml new file mode 100644 index 0000000000000..dad1cd8283eba --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer htmlTag="div" htmlClass="media-gallery-category-container" name="content"> + <uiComponent name="media_gallery_category_listing"/> + <block class="Magento\Backend\Block\Template" template="Magento_MediaGalleryCatalogUi::url_filter_applier.phtml" name="category_list_url_filter_applier"/> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/templates/url_filter_applier.phtml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/templates/url_filter_applier.phtml new file mode 100644 index 0000000000000..fa3abd419a691 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/templates/url_filter_applier.phtml @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var $block \Magento\Backend\Block\Template */ +/** @var \Magento\Framework\Escaper $escaper */ +?> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Ui/js/grid/url-filter-applier": { + "listingNamespace": "media_gallery_category_listing" + } + } + } +</script> 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 new file mode 100644 index 0000000000000..17fe33e5b2bf5 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml @@ -0,0 +1,187 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + media_gallery_category_listing.media_gallery_category_listing_data_source + </item> + </item> + </argument> + <settings> + <spinner>media_gallery_category_columns</spinner> + <deps> + <dep>media_gallery_category_listing.media_gallery_category_listing_data_source</dep> + </deps> + </settings> + <dataSource name="media_gallery_category_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">entity_id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryCatalogUi\Model\Listing\DataProvider" name="media_gallery_category_listing_data_source"> + <settings> + <requestFieldName>entity_id</requestFieldName> + <primaryFieldName>entity_id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="name" > + <settings> + <placeholder>Search by category name</placeholder> + <label>Name</label> + </settings> + </filterSearch> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + 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"> + <item name="entityType" xsi:type="string">catalog_category</item> + <item name="identityColumn" xsi:type="string">entity_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="selectedPlaceholders" xsi:type="array"> + <item name="defaultPlaceholder" xsi:type="string">Select</item> + </item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <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> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + <filterInput name="entity_id" provider="${ $.parentName }" sortOrder="20"> + <settings> + <dataScope>entity_id</dataScope> + <label translate="true">ID</label> + <placeholder>ID</placeholder> + </settings> + </filterInput> + <filterSelect name="display_mode" provider="${ $.parentName }" sortOrder="30"> + <settings> + <options class="Magento\Catalog\Model\Category\Attribute\Source\Mode"/> + <caption translate="true">Select</caption> + <label translate="true">Display Mode</label> + <dataScope>display_mode</dataScope> + </settings> + </filterSelect> + <filterSelect name="include_in_menu" provider="${ $.parentName }" sortOrder="40"> + <settings> + <options class="Magento\Config\Model\Config\Source\Yesno"/> + <caption translate="true">Select</caption> + <label translate="true">In Menu</label> + <dataScope>include_in_menu</dataScope> + </settings> + </filterSelect> + <filterSelect name="is_active" provider="${ $.parentName }" sortOrder="50"> + <settings> + <options class="Magento\Config\Model\Config\Source\Yesno"/> + <caption translate="true">Select</caption> + <label translate="true">Enabled</label> + <dataScope>is_active</dataScope> + </settings> + </filterSelect> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + </listingToolbar> + <columns name="media_gallery_category_columns"> + <column name="entity_id"> + <settings> + <label translate="true">ID</label> + </settings> + </column> + <column name="image" component="Magento_Ui/js/grid/columns/thumbnail" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\Thumbnail"> + <settings> + <sortable>false</sortable> + <label translate="true">Image</label> + </settings> + </column> + <column name="name"> + <settings> + <label translate="true">Name</label> + </settings> + </column> + <column name="path" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\Path"> + <settings> + <label translate="true">Path</label> + </settings> + </column> + <column name="display_mode"> + <settings> + <label translate="true">Display Mode</label> + </settings> + </column> + <column name="products"> + <settings> + <label translate="true">Products</label> + </settings> + </column> + <column name="include_in_menu" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\InMenu"> + <settings> + <label translate="true">In Menu</label> + </settings> + </column> + <column name="is_active" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\IsActive"> + <settings> + <label translate="true">Enabled</label> + </settings> + </column> + <actionsColumn name="actions" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\CategoryActions" sortOrder="1000"> + <settings> + <indexField>entity_id</indexField> + </settings> + </actionsColumn> + </columns> +</listing> 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 new file mode 100644 index 0000000000000..6976584c2e36c --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="product_id" + provider="${ $.parentName }" + sortOrder="110" + 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"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Product Name or SKU</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find products</item> + <item name="missingValuePlaceholder" xsi:type="string" translate="true">Product with ID: %s doesn\'t exist</item> + <item name="isDisplayMissingValuePlaceholder" xsi:type="boolean">true</item> + <item name="isDisplayEmptyPlaceholder" xsi:type="boolean">true</item> + <item name="isRemoveSelectedIcon" xsi:type="boolean">true</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <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="media_gallery_catalog/product/getSelected"/> + </item> + </argument> + <settings> + <label translate="true">Used in Products</label> + <dataScope>product_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="category_id" + provider="${ $.parentName }" + sortOrder="100" + component="Magento_Catalog/js/components/new-category" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Category Name</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find categories</item> + </item> + </argument> + <settings> + <options class="Magento\Catalog\Ui\Component\Product\Form\Categories\Options"/> + <label translate="true">Used in Categories</label> + <dataScope>category_id</dataScope> + <listens> + <link name="${ $.namespace }.${ $.namespace }:responseData">setParsed</link> + </listens> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> 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 new file mode 100644 index 0000000000000..6976584c2e36c --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="product_id" + provider="${ $.parentName }" + sortOrder="110" + 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"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Product Name or SKU</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find products</item> + <item name="missingValuePlaceholder" xsi:type="string" translate="true">Product with ID: %s doesn\'t exist</item> + <item name="isDisplayMissingValuePlaceholder" xsi:type="boolean">true</item> + <item name="isDisplayEmptyPlaceholder" xsi:type="boolean">true</item> + <item name="isRemoveSelectedIcon" xsi:type="boolean">true</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <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="media_gallery_catalog/product/getSelected"/> + </item> + </argument> + <settings> + <label translate="true">Used in Products</label> + <dataScope>product_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="category_id" + provider="${ $.parentName }" + sortOrder="100" + component="Magento_Catalog/js/components/new-category" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Category Name</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find categories</item> + </item> + </argument> + <settings> + <options class="Magento\Catalog\Ui\Component\Product\Form\Categories\Options"/> + <label translate="true">Used in Categories</label> + <dataScope>category_id</dataScope> + <listens> + <link name="${ $.namespace }.${ $.namespace }:responseData">setParsed</link> + </listens> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less new file mode 100644 index 0000000000000..51575bd496598 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,29 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +& when (@media-common = true) { + + .media-gallery-category-container { + + .admin__field-label { + text-align: left; + } + + .admin__action-dropdown-wrap._active .admin__action-dropdown-text::after { + margin-right: 6px; + } + + .admin__data-grid-action-bookmarks .admin__action-dropdown-menu { + left: auto; + right: 0; + } + + .admin__field:not(.admin__field-option) > .admin__field-label { + font-size: 1.3rem; + font-weight: bold; + line-height: 2.1rem; + } + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/images/category/placeholder/image.jpg b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/images/category/placeholder/image.jpg new file mode 100644 index 0000000000000..0d5ef7e1bd412 Binary files /dev/null and b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/images/category/placeholder/image.jpg differ 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/Block/Search.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php new file mode 100644 index 0000000000000..7beb95375073e --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php @@ -0,0 +1,97 @@ +<?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\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to search blocks for ui-select component + */ +class Search 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; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param JsonFactory $resultFactory + * @param BlockRepositoryInterface $blockRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + BlockRepositoryInterface $blockRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->blockRepository = $blockRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute() : ResultInterface + { + $searchKey = $this->getRequest()->getParam('searchKey'); + $currentPage = (int) $this->getRequest()->getParam('page'); + $limit = (int) $this->getRequest()->getParam('limit'); + + $searchResult = $this->blockRepository->getList( + $this->searchCriteriaBuilder->addFilter('title', '%' . $searchKey . '%', 'like') + ->setCurrentPage($currentPage) + ->setPageSize($limit) + ->create() + ); + + $options = []; + foreach ($searchResult->getItems() as $block) { + $id = $block->getId(); + $options[$id] = [ + 'value' => $id, + 'label' => $block->getTitle(), + 'is_active' => $block->isActive(), + 'optgroup' => false + ]; + } + + return $this->resultJsonFactory->create()->setData([ + 'options' => $options, + 'total' => $searchResult->getTotalCount() + ]); + } +} 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/Controller/Adminhtml/Page/Search.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php new file mode 100644 index 0000000000000..b211e58a0e8c6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php @@ -0,0 +1,97 @@ +<?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\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to search pages for ui-select component + */ +class Search 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; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param JsonFactory $resultFactory + * @param PageRepositoryInterface $pageRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + PageRepositoryInterface $pageRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->pageRepository = $pageRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $searchKey = $this->getRequest()->getParam('searchKey'); + $currentPage = (int) $this->getRequest()->getParam('page'); + $limit = (int) $this->getRequest()->getParam('limit'); + + $searchResult = $this->pageRepository->getList( + $this->searchCriteriaBuilder->addFilter('title', '%' . $searchKey . '%', 'like') + ->setCurrentPage($currentPage) + ->setPageSize($limit) + ->create() + ); + + $options = []; + foreach ($searchResult->getItems() as $page) { + $id = $page->getId(); + $options[$id] = [ + 'value' => $id, + 'label' => $page->getTitle(), + 'is_active' => $page->isActive(), + 'optgroup' => false + ]; + } + + return $this->resultJsonFactory->create()->setData([ + 'options' => $options, + 'total' => $searchResult->getTotalCount() + ]); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt b/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/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/MediaGalleryCmsUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/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/MediaGalleryCmsUi/README.md b/app/code/Magento/MediaGalleryCmsUi/README.md new file mode 100644 index 0000000000000..a5c2eb24c6c15 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryCmsUi module + +The Magento_MediaGalleryCmsUi module provides Magento_Cms related UI elements to the media gallery user interface + +## 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/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml new file mode 100644 index 0000000000000..f0938016d12f1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="FillOutCustomCMSPageContentActionGroup"> + <annotations> + <description>Fills out the Page details (Page Title, Content and URL Key)</description> + </annotations> + + <arguments> + <argument name="title" type="string"/> + <argument name="content" type="string"/> + <argument name="identifier" type="string"/> + </arguments> + + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{title}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContentTabForPage"/> + <fillField selector="{{CmsNewPagePageContentSection.contentHeading}}" userInput="{{content}}" stepKey="fillFieldContentHeading"/> + <scrollTo selector="{{CmsNewPagePageContentSection.content}}" stepKey="scrollToPageContent"/> + <fillField selector="{{CmsNewPagePageContentSection.content}}" userInput="{{content}}" stepKey="fillFieldContent"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{identifier}}" stepKey="fillFieldUrlKey"/> + </actionGroup> +</actionGroups> 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 new file mode 100644 index 0000000000000..fa6dc6c1a07fa --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCmsUiUsedInBlocksFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInBlocksFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in blocks 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/4951850"/> + <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"/> + <deleteData createDataKey="block" stepKey="deleteBlock"/> + </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="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Blocks"/> + <argument name="optionName" value="$$block.title$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <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/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml new file mode 100644 index 0000000000000..26de500970a2e --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.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="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"/> + <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/4934276"/> + <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="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </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"> + <argument name="filterName" value="Used in Pages"/> + <argument name="optionName" value="Unique page title MediaGalleryUi"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminSearchCmsPageInGridByUrlKeyActionGroup" stepKey="findCreatedCmsPage"> + <argument name="urlKey" value="test-page-1"/> + </actionGroup> + <actionGroup ref="AdminDeleteCmsPageFromGridActionGroup" stepKey="deleteCmsPage"> + <argument name="urlKey" value="test-page-1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/composer.json b/app/code/Magento/MediaGalleryCmsUi/composer.json new file mode 100644 index 0000000000000..1ecfb9a3c8855 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/composer.json @@ -0,0 +1,23 @@ +{ + "name": "magento/module-media-gallery-cms-ui", + "description": "Cms related UI elements in the magento media gallery", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-backend": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCmsUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..65ed3b7197f83 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml @@ -0,0 +1,41 @@ +<?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"> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="page_id" xsi:type="object">Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Page</item> + <item name="block_id" xsi:type="object">Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Block</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Page" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">cms_page</argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Block" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">cms_block</argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\GetAssetUsageDetails"> + <arguments> + <argument name="contentTypes" xsi:type="array"> + <item name="cms_block" xsi:type="array"> + <item name="name" xsi:type="string">Blocks</item> + <item name="link" xsi:type="string">cms/block/index</item> + </item> + <item name="cms_page" xsi:type="array"> + <item name="name" xsi:type="string">Pages</item> + <item name="link" xsi:type="string">cms/page/index</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..2dc8b3ade5be7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery_cms" frontName="media_gallery_cms"> + <module name="Magento_MediaGalleryCmsUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/module.xml b/app/code/Magento/MediaGalleryCmsUi/etc/module.xml new file mode 100644 index 0000000000000..8a39b8328b387 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/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_MediaGalleryCmsUi" /> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/registration.php b/app/code/Magento/MediaGalleryCmsUi/registration.php new file mode 100644 index 0000000000000..0e68935eba590 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/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_MediaGalleryCmsUi', + __DIR__ +); 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 new file mode 100644 index 0000000000000..e49ba7a98c8ce --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="page_id" + provider="${ $.parentName }" + sortOrder="120" + 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"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/page/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Page Title</item> + <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> + <label translate="true">Used in Pages</label> + <dataScope>page_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="block_id" + provider="${ $.parentName }" + sortOrder="130" + 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"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/block/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Block Title</item> + <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> + <label translate="true">Used in Blocks</label> + <dataScope>block_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> 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 new file mode 100644 index 0000000000000..e49ba7a98c8ce --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="page_id" + provider="${ $.parentName }" + sortOrder="120" + 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"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/page/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Page Title</item> + <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> + <label translate="true">Used in Pages</label> + <dataScope>page_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="block_id" + provider="${ $.parentName }" + sortOrder="130" + 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"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/block/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Block Title</item> + <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> + <label translate="true">Used in Blocks</label> + <dataScope>block_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryIntegration/LICENSE.txt b/app/code/Magento/MediaGalleryIntegration/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/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/MediaGalleryIntegration/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/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/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php b/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php new file mode 100644 index 0000000000000..ed8108f012af0 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Plugin; + +use Magento\MediaGalleryUiApi\Api\ConfigInterface; +use Magento\Ui\Component\Form\Element\DataType\Media\OpenDialogUrl; + +/** + * Plugin to get open media gallery dialog URL for WYSIWYG and widgets + */ +class NewMediaGalleryOpenDialogUrl +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @param ConfigInterface $config + */ + public function __construct(ConfigInterface $config) + { + $this->config = $config; + } + + /** + * Get Url based on media gallery configuration + * + * @param OpenDialogUrl $subject + * @param string $result + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @return string + */ + public function afterGet(OpenDialogUrl $subject, string $result) + { + return $this->config->isEnabled() ? 'media_gallery/index/index' : $result; + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php b/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php new file mode 100644 index 0000000000000..a999b9004d9e5 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Uploader; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; +use Psr\Log\LoggerInterface; + +/** + * Save image information by SaveAssetsInterface. + */ +class SaveImageInformation +{ + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + + /** + * @var string[] + */ + private $imageExtensions; + + /** + * @param Filesystem $filesystem + * @param LoggerInterface $log + * @param IsPathExcludedInterface $isPathExcluded + * @param SynchronizeFilesInterface $synchronizeFiles + * @param ConfigInterface $config + * @param array $imageExtensions + */ + public function __construct( + Filesystem $filesystem, + LoggerInterface $log, + IsPathExcludedInterface $isPathExcluded, + SynchronizeFilesInterface $synchronizeFiles, + ConfigInterface $config, + array $imageExtensions + ) { + $this->log = $log; + $this->isPathExcluded = $isPathExcluded; + $this->filesystem = $filesystem; + $this->synchronizeFiles = $synchronizeFiles; + $this->config = $config; + $this->imageExtensions = $imageExtensions; + } + + /** + * Saves asset to media gallery after save image. + * + * @param Uploader $subject + * @param array $result + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @return array + */ + public function afterSave(Uploader $subject, array $result): array + { + if (!$this->config->isEnabled()) { + return $result; + } + + $path = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) + ->getRelativePath(rtrim($result['path'], '/') . '/' . ltrim($result['file'], '/')); + if (!$this->isApplicable($path)) { + return $result; + } + $this->synchronizeFiles->execute([$path]); + + return $result; + } + + /** + * Can asset be saved with provided path + * + * @param string $path + * @return bool + */ + private function isApplicable(string $path): bool + { + try { + return $path + && !$this->isPathExcluded->execute($path) + && preg_match('#\.(' . implode("|", $this->imageExtensions) . ')$# i', $path); + } catch (\Exception $exception) { + $this->log->critical($exception); + return false; + } + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/README.md b/app/code/Magento/MediaGalleryIntegration/README.md new file mode 100644 index 0000000000000..365cde86777f2 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryIntegration + +The purpose of this module is to keep the integration of enhanced media gallery to Magento separated from implementation. diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php new file mode 100644 index 0000000000000..dfeaa3eff56bd --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Ui\Component\Form\Element\DataType\Media\Image; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update open dialog url functionality for media editor. + * @magentoAppArea adminhtml + */ +class ImageComponentOpenDialogUrlTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var Image + */ + private $image; + + /** + * @var string + */ + private $mediaGalleryOpenDialogUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->image = $this->objectManger->create(Image::class); + $this->image->setData('config', ['initialMediaGalleryOpenSubpath' => 'wysiwyg']); + + $url = $this->objectManger->create(UrlInterface::class); + $this->mediaGalleryOpenDialogUrl = $url->getUrl('media_gallery/index/index'); + } + + /** + * Test image open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + $this->image->prepare(); + $expectedOpenDialogUrl = $this->image->getConfiguration()['mediaGallery']['openDialogUrl']; + self::assertNotEquals($this->mediaGalleryOpenDialogUrl, $expectedOpenDialogUrl); + } + + /** + * Test image open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + $this->image->prepare(); + $expectedOpenDialogUrl = $this->image->getConfiguration()['mediaGallery']['openDialogUrl']; + self::assertEquals($this->mediaGalleryOpenDialogUrl, $expectedOpenDialogUrl); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlTest.php new file mode 100644 index 0000000000000..90f363d6d792b --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\ObjectManagerInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Ui\Component\Form\Element\DataType\Media\OpenDialogUrl; +use PHPUnit\Framework\TestCase; + +/** + * Provide tests cover getting correct url based on the config settings. + * @magentoAppArea adminhtml + */ +class OpenDialogUrlTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var OpenDialogUrl + */ + private $openDialogUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $config = $this->objectManger->create(ConfigInterface::class); + $this->openDialogUrl = $this->objectManger->create( + OpenDialogUrl::class, + ['config' => $config] + ); + } + + /** + * Test getting open dialog url with enhanced media gallery disabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + self::assertEquals('cms/wysiwyg_images/index', $this->openDialogUrl->get()); + } + + /** + * Test getting open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + self::assertEquals('media_gallery/index/index', $this->openDialogUrl->get()); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php new file mode 100644 index 0000000000000..81a4dc642cfa0 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Tinymce3\Model\Config\Gallery\Config; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update open dialog url functionality for media editor. + * @magentoAppArea adminhtml + */ +class TinyMceOpenDialogUrlTest extends TestCase +{ + private const FILES_BROWSER_WINDOW_URL = 'files_browser_window_url'; + + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var Config + */ + private $tinyMce3Config; + + /** + * @var DataObject + */ + private $configDataObject; + + /** + * @var string + */ + private $fileBrowserWindowUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->tinyMce3Config = $this->objectManger->create(Config::class); + $this->configDataObject = $this->objectManger->create(DataObject::class); + + $url = $this->objectManger->create(UrlInterface::class); + $this->fileBrowserWindowUrl = $url->getUrl('media_gallery/index/index'); + } + + /** + * Test image open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + $config = $this->tinyMce3Config->getConfig($this->configDataObject); + self::assertNotEquals($this->fileBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } + + /** + * Test image open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + $config = $this->tinyMce3Config->getConfig($this->configDataObject); + self::assertEquals($this->fileBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php new file mode 100644 index 0000000000000..aebf5927869d5 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Config; +use Magento\Cms\Model\Wysiwyg\Gallery\DefaultConfigProvider; +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update wysiwyg editor dialog url update when media gallery enabled. + * @magentoAppArea adminhtml + */ +class WysiwygDefaultConfigOpenDialogUrlTest extends TestCase +{ + private const FILES_BROWSER_WINDOW_URL = 'files_browser_window_url'; + + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var DataObject + */ + private $configDataObject; + + /** + * @var string + */ + private $filesBrowserWindowUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->configDataObject = $this->objectManger->create(DataObject::class); + + $url = $this->objectManger->create(UrlInterface::class); + $imageHelper = $this->objectManger->create(Images::class); + $this->filesBrowserWindowUrl = $url->getUrl( + 'media_gallery/index/index', + ['current_tree_path' => $imageHelper->idEncode(Config::IMAGE_DIRECTORY)] + ); + } + + /** + * Test update wysiwyg editor open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + /** @var DefaultConfigProvider $defaultConfigProvider */ + $defaultConfigProvider = $this->objectManger->create(DefaultConfigProvider::class); + $config = $defaultConfigProvider->getConfig($this->configDataObject); + self::assertNotEquals($this->filesBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } + + /** + * Test update wysiwyg editor open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + /** @var DefaultConfigProvider $defaultConfigProvider */ + $defaultConfigProvider = $this->objectManger->create(DefaultConfigProvider::class); + $config = $defaultConfigProvider->getConfig($this->configDataObject); + self::assertEquals($this->filesBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/composer.json b/app/code/Magento/MediaGalleryIntegration/composer.json new file mode 100644 index 0000000000000..a9709da81222e --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/composer.json @@ -0,0 +1,32 @@ +{ + "name": "magento/module-media-gallery-integration", + "description": "Magento module responsible for integration of enhanced media gallery", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-ui": "*" + }, + "require-dev": { + "magento/module-cms": "*" + }, + "suggest": { + "magento/module-catalog": "*", + "magento/module-cms": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryIntegration\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..08e83ce6cad88 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml @@ -0,0 +1,25 @@ +<?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\Ui\Component\Form\Element\DataType\Media\OpenDialogUrl"> + <plugin name="new_media_gallery_open_dialog_url" type="Magento\MediaGalleryIntegration\Plugin\NewMediaGalleryOpenDialogUrl" /> + </type> + <type name="Magento\Framework\File\Uploader"> + <plugin name="save_asset_image" type="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"/> + </type> + <type name="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"> + <arguments> + <argument name="imageExtensions" 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> +</config> diff --git a/app/code/Magento/MediaGalleryIntegration/etc/module.xml b/app/code/Magento/MediaGalleryIntegration/etc/module.xml new file mode 100644 index 0000000000000..88af90477cc8a --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryIntegration"> + <sequence> + <module name="Magento_Ui"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/MediaGalleryIntegration/registration.php b/app/code/Magento/MediaGalleryIntegration/registration.php new file mode 100644 index 0000000000000..028f8d5b4288a --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryIntegration', __DIR__); diff --git a/app/code/Magento/MediaGalleryMetadata/LICENSE.txt b/app/code/Magento/MediaGalleryMetadata/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/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/MediaGalleryMetadata/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/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/MediaGalleryMetadata/Model/AddIptcMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php new file mode 100644 index 0000000000000..9935904468388 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php @@ -0,0 +1,180 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Write iptc data to the file return updated FileInterface with iptc data + */ +class AddIptcMetadata +{ + private const IPTC_TITLE_SEGMENT = '2#005'; + private const IPTC_DESCRIPTION_SEGMENT = '2#120'; + private const IPTC_KEYWORDS_SEGMENT = '2#025'; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param DriverInterface $driver + * @param ReadFile $fileReader + */ + public function __construct( + FileInterfaceFactory $fileFactory, + DriverInterface $driver, + ReadFile $fileReader + ) { + $this->fileFactory = $fileFactory; + $this->driver = $driver; + $this->fileReader = $fileReader; + } + + /** + * Write metadata + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @param null|SegmentInterface $segment + */ + public function execute(FileInterface $file, MetadataInterface $metadata, ?SegmentInterface $segment): FileInterface + { + if (!is_callable('iptcembed') && !is_callable('iptcparse')) { + throw new LocalizedException(__('iptcembed() && iptcparse() must be enabled in php configuration')); + } + + $iptcData = $segment ? iptcparse($segment->getData()) : []; + + if ($metadata->getTitle() !== null) { + $iptcData[self::IPTC_TITLE_SEGMENT][0] = $metadata->getTitle(); + } + + if ($metadata->getDescription() !== null) { + $iptcData[self::IPTC_DESCRIPTION_SEGMENT][0] = $metadata->getDescription(); + } + + if ($metadata->getKeywords() !== null) { + $iptcData = $this->writeKeywords($metadata->getKeywords(), $iptcData); + } + + $newData = ''; + + foreach ($iptcData as $tag => $values) { + foreach ($values as $value) { + $newData .= $this->iptcMaketag(2, (int) substr($tag, 2), $value); + } + } + + $this->writeFile($file->getPath(), iptcembed($newData, $file->getPath())); + + $fileWithIptc = $this->fileReader->execute($file->getPath()); + + return $this->fileFactory->create([ + 'path' => $fileWithIptc->getPath(), + 'segments' => $this->getSegmentsWithIptc($fileWithIptc, $file) + ]); + } + + /** + * Return iptc segment from file. + * + * @param FileInterface $fileWithIptc + * @param FileInterface $originFile + */ + private function getSegmentsWithIptc(FileInterface $fileWithIptc, $originFile): array + { + $segments = $fileWithIptc->getSegments(); + $originFileSegments = $originFile->getSegments(); + + foreach ($segments as $key => $segment) { + if ($segment->getName() === 'APP13') { + foreach ($originFileSegments as $originKey => $segment) { + if ($segment->getName() === 'APP13') { + $originFileSegments[$originKey] = $segments[$key]; + } + } + return $originFileSegments; + } + } + return $originFileSegments; + } + + /** + * Write keywords field to the iptc segment. + * + * @param array $keywords + * @param array $iptcData + */ + private function writeKeywords(array $keywords, array $iptcData): array + { + foreach ($keywords as $key => $keyword) { + $iptcData[self::IPTC_KEYWORDS_SEGMENT][$key] = $keyword; + } + return $iptcData; + } + + /** + * Write iptc data to the image directly to the file. + * + * @param string $filePath + * @param string $content + */ + private function writeFile(string $filePath, string $content): void + { + $resource = $this->driver->fileOpen($filePath, 'wb'); + + $this->driver->fileWrite($resource, $content); + $this->driver->fileClose($resource); + } + + /** + * Create new iptc tag text + * + * @param int $rec + * @param int $tag + * @param string $value + */ + private function iptcMaketag(int $rec, int $tag, string $value) + { + //phpcs:disable Magento2.Functions.DiscouragedFunction + $length = strlen($value); + $retval = chr(0x1C) . chr($rec) . chr($tag); + + if ($length < 0x8000) { + $retval .= chr($length >> 8) . chr($length & 0xFF); + } else { + $retval .= chr(0x80) . + chr(0x04) . + chr(($length >> 24) & 0xFF) . + chr(($length >> 16) & 0xFF) . + chr(($length >> 8) & 0xFF) . + chr($length & 0xFF); + } + //phpcs:enable Magento2.Functions.DiscouragedFunction + return $retval . $value; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php new file mode 100644 index 0000000000000..269df146f2c81 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Add metadata to the XMP template + */ +class AddXmpMetadata +{ + private const XMP_XPATH_SELECTOR_TITLE = '//dc:title/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_DESCRIPTION = '//dc:description/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORDS = '//dc:subject/rdf:Bag'; + private const XMP_XPATH_SELECTOR_KEYWORDS_EACH = '//dc:subject/rdf:Bag/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORD_ITEM = 'rdf:li'; + + /** + * Parse metadata + * + * @param string $data + * @param MetadataInterface $metadata + * @return string + */ + public function execute(string $data, MetadataInterface $metadata): string + { + $xml = simplexml_load_string($data); + $namespaces = $xml->getNamespaces(true); + + foreach ($namespaces as $prefix => $url) { + $xml->registerXPathNamespace($prefix, $url); + } + + if ($metadata->getTitle() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_TITLE); + } else { + $this->setValueByXpath($xml, self::XMP_XPATH_SELECTOR_TITLE, $metadata->getTitle()); + } + if ($metadata->getDescription() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_DESCRIPTION); + } else { + $this->setValueByXpath($xml, self::XMP_XPATH_SELECTOR_DESCRIPTION, $metadata->getDescription()); + } + if ($metadata->getKeywords() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_KEYWORDS); + } else { + $this->updateKeywords($xml, $metadata->getKeywords()); + } + + $data = $xml->asXML(); + return str_replace("<?xml version=\"1.0\"?>\n", '', $data); + } + + /** + * Update keywords + * + * @param \SimpleXMLElement $xml + * @param array $keywords + */ + private function updateKeywords(\SimpleXMLElement $xml, array $keywords): void + { + foreach ($xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS_EACH) as $keywordElement) { + unset($keywordElement[0]); + } + + foreach ($xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS) as $element) { + foreach ($keywords as $keyword) { + $element->addChild(self::XMP_XPATH_SELECTOR_KEYWORD_ITEM, $keyword); + } + } + } + + /** + * Deletes xml node by xpath + * + * @param \SimpleXMLElement $xml + * @param string $xpath + */ + private function deleteValueByXpath(\SimpleXMLElement $xml, string $xpath): void + { + foreach ($xml->xpath($xpath) as $element) { + unset($element[0]); + } + } + + /** + * Set value to xml node by xpath + * + * @param \SimpleXMLElement $xml + * @param string $xpath + * @param string $value + */ + private function setValueByXpath(\SimpleXMLElement $xml, string $xpath, string $value): void + { + foreach ($xml->xpath($xpath) as $element) { + $element[0] = $value; + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File.php b/app/code/Magento/MediaGalleryMetadata/Model/File.php new file mode 100644 index 0000000000000..4b7605e8ec839 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; + +/** + * File internal data transfer object + */ +class File implements FileInterface +{ + /** + * @var string + */ + private $path; + + /** + * @var array + */ + private $segments; + + /** + * @var FileExtensionInterface|null + */ + private $extensionAttributes; + + /** + * @param string $path + * @param array $segments + * @param FileExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $path, + array $segments, + ?FileExtensionInterface $extensionAttributes = null + ) { + $this->path = $path; + $this->segments = $segments; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getSegments(): array + { + return $this->segments; + } + + /** + * @inheritdoc + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?FileExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?FileExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php new file mode 100644 index 0000000000000..d5918781135a8 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\File; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Add metadata to the asset by path. Should be used as a virtual type with a file type specific configuration + */ +class AddMetadata implements AddMetadataInterface +{ + /** + * @var array + */ + private $segmentWriters; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var ReadFileInterface + */ + private $fileReader; + + /** + * @var WriteFileInterface + */ + private $fileWriter; + + /** + * @param FileInterfaceFactory $fileFactory + * @param ReadFileInterface $fileReader + * @param WriteFileInterface $fileWriter + * @param array $segmentWriters + */ + public function __construct( + FileInterfaceFactory $fileFactory, + ReadFileInterface $fileReader, + WriteFileInterface $fileWriter, + array $segmentWriters + ) { + $this->fileFactory = $fileFactory; + $this->fileReader = $fileReader; + $this->fileWriter = $fileWriter; + $this->segmentWriters = $segmentWriters; + } + + /** + * @inheritdoc + */ + public function execute(string $path, MetadataInterface $metadata): void + { + try { + $file = $this->fileReader->execute($path); + } catch (ValidatorException $e) { + return; + } catch (\Exception $exception) { + throw new LocalizedException( + __('Could not parse the image file for metadata: %path', ['path' => $path]) + ); + } + + try { + $this->fileWriter->execute($this->writeMetadata($file, $metadata)); + } catch (\Exception $exception) { + throw new LocalizedException( + __('Could not update the image file metadata: %path', ['path' => $path]) + ); + } + } + + /** + * Write metadata by given metadata writer + * + * @param FileInterface $file + * @param MetadataInterface $metadata + */ + private function writeMetadata(FileInterface $file, MetadataInterface $metadata): FileInterface + { + foreach ($this->segmentWriters as $writer) { + if (!$writer instanceof WriteMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($writer) . ' must implement ' . WriteFileInterface::class) + ); + } + + $file = $writer->execute($file, $metadata); + } + return $file; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php new file mode 100644 index 0000000000000..f5efd25bca041 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\File; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; + +/** + * Extract Metadata from asset file by given extractors + */ +class ExtractMetadata implements ExtractMetadataInterface +{ + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var array + */ + private $segmentReaders; + + /** + * @var ReadFileInterface + */ + private $fileReader; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param MetadataInterfaceFactory $metadataFactory + * @param ReadFileInterface $fileReader + * @param array $segmentReaders + */ + public function __construct( + FileInterfaceFactory $fileFactory, + MetadataInterfaceFactory $metadataFactory, + ReadFileInterface $fileReader, + array $segmentReaders + ) { + $this->fileFactory = $fileFactory; + $this->metadataFactory = $metadataFactory; + $this->fileReader = $fileReader; + $this->segmentReaders = $segmentReaders; + } + + /** + * @inheritdoc + */ + public function execute(string $path): MetadataInterface + { + try { + return $this->readSegments($this->fileReader->execute($path)); + } catch (\Exception $exception) { + return $this->metadataFactory->create(); + } + } + + /** + * Read file segments by given segmentReader + * + * @param FileInterface $file + */ + private function readSegments(FileInterface $file): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + foreach ($this->segmentReaders as $segmentReader) { + if (!$segmentReader instanceof ReadMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($segmentReader) . ' must implement ' . ReadMetadataInterface::class) + ); + } + + try { + $data = $segmentReader->execute($file); + } catch (\Exception $exception) { + continue; + } + + $title = !empty($data->getTitle()) ? $data->getTitle() : $title; + $description = !empty($data->getDescription()) ? $data->getDescription() : $description; + + if (!empty($data->getKeywords())) { + foreach ($data->getKeywords() as $keyword) { + $keywords[] = $keyword; + } + } + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => empty($keywords) ? null : array_unique($keywords) + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php new file mode 100644 index 0000000000000..e100a7f852e42 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; + +/** + * Get metadata from IPTC block + */ +class GetIptcMetadata +{ + private const IPTC_TITLE = '2#005'; + private const IPTC_DESCRIPTION = '2#120'; + private const IPTC_KEYWORDS = '2#025'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * Parse metadata + * + * @param string $data + * @return MetadataInterface + */ + public function execute(string $data): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + if (is_callable('iptcparse')) { + $iptcData = iptcparse($data); + + if (!empty($iptcData[self::IPTC_TITLE])) { + $title = trim($iptcData[self::IPTC_TITLE][0]); + } + + if (!empty($iptcData[self::IPTC_DESCRIPTION][0])) { + $description = trim($iptcData[self::IPTC_DESCRIPTION][0]); + } + + if (!empty($iptcData[self::IPTC_KEYWORDS][0])) { + $keywords = array_values($iptcData[self::IPTC_KEYWORDS]); + } + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => !empty($keywords) ? $keywords : null + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php new file mode 100644 index 0000000000000..bda01645ddfec --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; + +/** + * Get metadata from XMP block + */ +class GetXmpMetadata +{ + private const XMP_XPATH_SELECTOR_TITLE = '//dc:title/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_DESCRIPTION = '//dc:description/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORDS = '//dc:subject/rdf:Bag/rdf:li'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct(MetadataInterfaceFactory $metadataFactory) + { + $this->metadataFactory = $metadataFactory; + } + + /** + * Parse metadata + * + * @param string $data + * @return MetadataInterface + */ + public function execute(string $data): MetadataInterface + { + $xml = simplexml_load_string($data); + $namespaces = $xml->getNamespaces(true); + + foreach ($namespaces as $prefix => $url) { + $xml->registerXPathNamespace($prefix, $url); + } + + $keywords = array_map( + function (\SimpleXMLElement $element): string { + return (string) $element; + }, + $xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS) + ); + + $description = implode(' ', $xml->xpath(self::XMP_XPATH_SELECTOR_DESCRIPTION)); + $title = implode(' ', $xml->xpath(self::XMP_XPATH_SELECTOR_TITLE)); + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php new file mode 100644 index 0000000000000..88810d3ccf28f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php @@ -0,0 +1,318 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\Framework\Exception\ValidatorException; + +/** + * File segments reader + */ +class ReadFile implements ReadFileInterface +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->segmentNames = $segmentNames; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + $resource = $this->driver->fileOpen($path, 'rb'); + + $header = $this->read($resource, 3); + + if ($header != "GIF") { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a GIF image')); + } + + $version = $this->read($resource, 3); + + if (!in_array($version, ['87a', '89a'])) { + $this->driver->fileClose($resource); + throw new LocalizedException(__('Unexpected GIF version')); + } + + $headerSegment = $this->segmentFactory->create([ + 'name' => 'header', + 'data' => $header . $version + ]); + + $width = $this->read($resource, 2); + $height = $this->read($resource, 2); + $bitPerPixelBinary = $this->read($resource, 1); + $bitPerPixel = $this->getBitPerPixel($bitPerPixelBinary); + $backgroundAndAspectRatio = $this->read($resource, 2); + $globalColorTable = $this->getGlobalColorTable($resource, $bitPerPixel); + + $generalSegment = $this->segmentFactory->create([ + 'name' => 'header2', + 'data' => $width . $height . $bitPerPixelBinary . $backgroundAndAspectRatio . $globalColorTable + ]); + + $segments = $this->getSegments($resource); + + array_unshift($segments, $headerSegment, $generalSegment); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read gif segments + * + * @param resource $resource + * @return SegmentInterface[] + * @throws FileSystemException + */ + private function getSegments($resource): array + { + $gifFrameSeparator = pack("C", ord(",")); + $gifExtensionSeparator = pack("C", ord("!")); + $gifTerminator = pack("C", ord(";")); + + $segments = []; + do { + $separator = $this->read($resource, 1); + + if ($separator == $gifTerminator) { + return $segments; + } + + if ($separator == $gifFrameSeparator) { + $segments[] = $this->segmentFactory->create([ + 'name' => 'frame', + 'data' => $gifFrameSeparator . $this->readFrame($resource) + ]); + continue; + } + + if ($separator != $gifExtensionSeparator) { + throw new LocalizedException(__('The file is corrupted')); + } + + $segments[] = $this->getExtensionSegment($resource); + } while (!$this->driver->endOfFile($resource)); + + return $segments; + } + + /** + * Read extension segment + * + * @param resource $resource + * @return SegmentInterface + * @throws FileSystemException + */ + private function getExtensionSegment($resource): SegmentInterface + { + $gifExtensionSeparator = pack("C", ord("!")); + $extensionCodeBinary = $this->read($resource, 1); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $extensionCode = unpack('C', $extensionCodeBinary)[1]; + + if ($extensionCode == 0xF9) { + return $this->segmentFactory->create([ + 'name' => 'Graphics Control Extension', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + if ($extensionCode == 0xFE) { + return $this->segmentFactory->create([ + 'name' => 'comment', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + if ($extensionCode != 0xFF) { + return $this->segmentFactory->create([ + 'name' => 'Programm extension', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + $blockLengthBinary = $this->read($resource, 1); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $blockLength = unpack('C', $blockLengthBinary)[1]; + $name = $this->read($resource, $blockLength); + + if ($blockLength != 11) { + throw new LocalizedException(__('The file is corrupted')); + } + + if ($name == 'XMP DataXMP') { + return $this->segmentFactory->create([ + 'name' => $name, + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $blockLengthBinary + . $name . $this->readBlockWithSubblocks($resource) + ]); + } + + return $this->segmentFactory->create([ + 'name' => $name, + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $blockLengthBinary + . $name . $this->readBlock($resource) + ]); + } + + /** + * Read gif frame + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readFrame($resource): string + { + $boundingBox = $this->read($resource, 8); + $bitPerPixelBinary = $this->read($resource, 1); + $bitPerPixel = $this->getBitPerPixel($bitPerPixelBinary); + $globalColorTable = $this->getGlobalColorTable($resource, $bitPerPixel); + return $boundingBox . $bitPerPixelBinary . $globalColorTable . $this->read($resource, 1) + . $this->readBlockWithSubblocks($resource); + } + + /** + * Retrieve bits per pixel value + * + * @param string $data + * @return int + */ + private function getBitPerPixel(string $data): int + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $bitPerPixel = unpack('C', $data)[1]; + $bpp = ($bitPerPixel & 7) + 1; + $bitPerPixel >>= 7; + $haveMap = $bitPerPixel & 1; + return $haveMap ? $bpp : 0; + } + + /** + * Read global color table + * + * @param resource $resource + * @param int $bitPerPixel + * @return string + * @throws FileSystemException + */ + private function getGlobalColorTable($resource, int $bitPerPixel): string + { + $globalColorTable = ''; + if ($bitPerPixel > 0) { + $max = pow(2, $bitPerPixel); + for ($i = 1; $i <= $max; ++$i) { + $globalColorTable .= $this->read($resource, 3); + } + } + return $globalColorTable; + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } + + /** + * Read the block stored in multiple sections + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readBlockWithSubblocks($resource): string + { + $data = ''; + $subLength = $this->read($resource, 1); + + while ($subLength !== "\0") { + $data .= $subLength . $this->read($resource, ord($subLength)); + $subLength = $this->read($resource, 1); + } + + return $data . $subLength; + } + + /** + * Read gif block + * + * @param resource $resource + * @return string + * @throws FileSystemException] + */ + private function readBlock($resource): string + { + $blockLengthBinary = $this->read($resource, 1); + $blockLength = ord($blockLengthBinary); + if ($blockLength == 0) { + return ''; + } + return $blockLengthBinary . $this->read($resource, $blockLength) . $this->read($resource, 1); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php new file mode 100644 index 0000000000000..1b83554ef4df3 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * XMP Reader for gif file format + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'XMP DataXMP'; + /** + * see XMP Specification Part 3, 1.1.2 GIF + */ + private const MAGIC_TRAILER_LENGTH = 258; + private const MAGIC_TRAILER_START = "\x01\xFF\xFE"; + private const MAGIC_TRAILER_END = "\x03\x02\x01\x00\x00"; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isXmp($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + $xmp = substr($segment->getData(), 14); + + if (substr($xmp, -self::MAGIC_TRAILER_LENGTH, 3) !== self::MAGIC_TRAILER_START + || substr($xmp, -5) !== self::MAGIC_TRAILER_END + ) { + throw new LocalizedException(__('XMP data is corrupted')); + } + + return substr($xmp, 0, -self::MAGIC_TRAILER_LENGTH); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php new file mode 100644 index 0000000000000..2b5167eba596b --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php @@ -0,0 +1,191 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * XMP Writer for GIF format + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'XMP DataXMP'; + private const XMP_DATA_START_POSITION = 14; + private const MAGIC_TRAILER_START = "\x01\xFF\xFE"; + private const MAGIC_TRAILER_END = "\x03\x02\x01\x00\x00"; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $gifSegments = $file->getSegments(); + $xmpGifSegments = []; + foreach ($gifSegments as $key => $segment) { + if ($this->isSegmentXmp($segment)) { + $xmpGifSegments[$key] = $segment; + } + } + + if (empty($xmpGifSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertXmpGifSegment($gifSegments, $this->createXmpSegment($metadata)) + ]); + } + + foreach ($xmpGifSegments as $key => $segment) { + $gifSegments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $gifSegments + ]); + } + + /** + * Insert XMP segment to gif image segments (at position 3) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertXmpGifSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 4), [$xmpSegment], array_slice($segments, 4)); + } + + /** + * Return XMP template from string + * + * @param string $string + * @param string $start + * @param string $end + */ + private function getXmpData(string $string, string $start, string $end): string + { + $string = ' ' . $string; + $ini = strpos($string, $start); + if ($ini == 0) { + return ''; + } + $ini += strlen($start); + $len = strpos($string, $end, $ini) - $ini; + + return substr($string, $ini, $len); + } + + /** + * Write new segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + public function createXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + + $xmpSegment = pack("C", ord("!")) . pack("C", 255) . pack("C", 11) . + self::XMP_SEGMENT_NAME . $this->addXmpMetadata->execute($xmpData, $metadata) . "\x01"; + + /** + * Write Magic trailer 258 bytes see XMP Specification Part 3, 1.1.2 GIF + */ + $i = 255; + while ($i > 0) { + $xmpSegment .= pack("C", $i); + $i--; + } + + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => $xmpSegment . "\0\0" + ]); + } + + /** + * Add metadata to the segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + public function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $data = $segment->getData(); + $start = substr($data, 0, self::XMP_DATA_START_POSITION); + $xmpData = $this->getXmpData($data, self::XMP_SEGMENT_NAME, "\x01"); + $end = substr($data, strpos($data, "\x01")); + + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $start . $this->addXmpMetadata->execute($xmpData, $metadata) . $end + ]); + } + + /** + * Check if segment contains XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php new file mode 100644 index 0000000000000..cbdc9fa286e85 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments writer + */ +class WriteFile implements WriteFileInterface +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write file object to the filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileClose($resource); + } + + /** + * Write gif segment + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + $this->driver->fileWrite( + $resource, + $segment->getData() + ); + } + $this->driver->fileWrite($resource, pack("C", ord(";"))); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php new file mode 100644 index 0000000000000..ed241d03506c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php @@ -0,0 +1,209 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; + +/** + * Jpeg file reader + */ +class ReadFile implements ReadFileInterface +{ + private const MARKER_IMAGE_FILE_START = "\xD8"; + private const MARKER_PREFIX = "\xFF"; + private const MARKER_IMAGE_END = "\xD9"; + private const MARKER_IMAGE_START = "\xDA"; + + private const TWO_BYTES = 2; + private const ONE_MEGABYTE = 1048576; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->segmentNames = $segmentNames; + } + + /** + * Is reader applicable + * + * @param string $path + * @return bool + * @throws FileSystemException + */ + private function isApplicable(string $path): bool + { + $resource = $this->driver->fileOpen($path, 'rb'); + try { + $marker = $this->readMarker($resource); + } catch (LocalizedException $exception) { + return false; + } + $this->driver->fileClose($resource); + + return $marker == self::MARKER_IMAGE_FILE_START; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + if (!$this->isApplicable($path)) { + throw new ValidatorException(__('Not a JPEG image')); + } + + $resource = $this->driver->fileOpen($path, 'rb'); + $marker = $this->readMarker($resource); + + if ($marker != self::MARKER_IMAGE_FILE_START) { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a JPEG image')); + } + + do { + $marker = $this->readMarker($resource); + $segments[] = $this->readSegment($resource, ord($marker)); + } while (($marker != self::MARKER_IMAGE_START) && (!$this->driver->endOfFile($resource))); + + if ($marker != self::MARKER_IMAGE_START) { + throw new LocalizedException(__('File is corrupted')); + } + + $segments[] = $this->segmentFactory->create([ + 'name' => 'CompressedImage', + 'data' => $this->readCompressedImage($resource) + ]); + + $this->driver->fileClose($resource); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read jpeg marker + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readMarker($resource): string + { + $data = $this->read($resource, self::TWO_BYTES); + + if ($data[0] != self::MARKER_PREFIX) { + $this->driver->fileClose($resource); + throw new LocalizedException(__('File is corrupted')); + } + + return $data[1]; + } + + /** + * Read compressed image + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readCompressedImage($resource): string + { + $compressedImage = ''; + do { + $compressedImage .= $this->read($resource, self::ONE_MEGABYTE); + } while (!$this->driver->endOfFile($resource)); + + $endOfImageMarkerPosition = strpos($compressedImage, self::MARKER_PREFIX . self::MARKER_IMAGE_END); + + if ($endOfImageMarkerPosition !== false) { + $compressedImage = substr($compressedImage, 0, $endOfImageMarkerPosition); + } + + return $compressedImage; + } + + /** + * Read jpeg segment + * + * @param resource $resource + * @param int $segmentType + * @return SegmentInterface + * @throws FileSystemException + */ + private function readSegment($resource, int $segmentType): SegmentInterface + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentSize = unpack('nsize', $this->read($resource, 2))['size'] - 2; + return $this->segmentFactory->create([ + 'name' => $this->segmentNames->getSegmentName($segmentType), + 'data' => $this->read($resource, $segmentSize) + ]); + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php new file mode 100644 index 0000000000000..b6c32296f3f7d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Jpeg EXIF Reader + */ +class ReadExif implements ReadMetadataInterface +{ + private const EXIF_SEGMENT_NAME = 'APP1'; + private const EXIF_SEGMENT_START = "Exif\x00"; + private const EXIF_DATA_START_POSITION = 0; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + if (!is_callable('exif_read_data')) { + throw new LocalizedException( + __('exif_read_data() must be enabled in php configuration') + ); + } + + foreach ($file->getSegments() as $segment) { + if ($this->isExifSegment($segment)) { + return $this->getExifData($file->getPath()); + } + } + + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Parese exif data from segment + * + * @param string $filePath + */ + private function getExifData(string $filePath): MetadataInterface + { + $title = null; + $description = null; + $keywords = null; + + $data = exif_read_data($filePath); + + if (!empty($data)) { + $title = isset($data['DocumentName']) ? $data['DocumentName'] : null; + $description = isset($data['ImageDescription']) ? $data['ImageDescription'] : null; + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + } + + /** + * Does segment contain Exif data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isExifSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::EXIF_SEGMENT_NAME + && strncmp( + substr($segment->getData(), self::EXIF_DATA_START_POSITION, 5), + self::EXIF_SEGMENT_START, + 5 + ) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php new file mode 100644 index 0000000000000..e56993528a041 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\GetIptcMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * IPTC Reader to read IPTC data for jpeg image + */ +class ReadIptc implements ReadMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'APP13'; + private const IPTC_SEGMENT_START = 'Photoshop 3.0'; + private const IPTC_DATA_START_POSITION = 0; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetIptcMetadata + */ + private $getIptcData; + + /** + * @param GetIptcMetadata $getIptcData + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + GetIptcMetadata $getIptcData, + MetadataInterfaceFactory $metadataFactory + ) { + $this->getIptcData = $getIptcData; + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isIptcSegment($segment)) { + return $this->getIptcData->execute($segment->getData()); + } + } + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp($segment->getData(), self::IPTC_SEGMENT_START, self::IPTC_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php new file mode 100644 index 0000000000000..e68c86d35eb97 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Jpeg XMP Reader + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'APP1'; + private const XMP_SEGMENT_START = "http://ns.adobe.com/xap/1.0/\x00"; + private const XMP_DATA_START_POSITION = 29; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isSegmentXmp($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strncmp($segment->getData(), self::XMP_SEGMENT_START, self::XMP_DATA_START_POSITION) == 0; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), self::XMP_DATA_START_POSITION); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php new file mode 100644 index 0000000000000..e9fcd500f1dca --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\AddIptcMetadata; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Jpeg IPTC Writer + */ +class WriteIptc implements WriteMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'APP13'; + private const IPTC_SEGMENT_START = 'Photoshop 3.0\0x00'; + private const IPTC_DATA_START_POSITION = 0; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddIPtcMetadata + */ + private $addIptcMetadata; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddIptcMetadata $addIptcMetadata + * @param ReadFile $fileReader + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddIptcMetadata $addIptcMetadata, + ReadFile $fileReader + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addIptcMetadata = $addIptcMetadata; + $this->fileReader = $fileReader; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $iptcSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isIptcSegment($segment)) { + $iptcSegments[$key] = $segment; + } + } + + foreach ($iptcSegments as $segment) { + return $this->addIptcMetadata->execute($file, $metadata, $segment); + } + return $this->addIptcMetadata->execute($file, $metadata, null); + } + + /** + * Check if segment contains IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp($segment->getData(), self::IPTC_SEGMENT_START, self::IPTC_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php new file mode 100644 index 0000000000000..e88cdd5b7b8f4 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Jpeg XMP Writer + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'APP1'; + private const XMP_SEGMENT_START = "http://ns.adobe.com/xap/1.0/\x00"; + private const XMP_DATA_START_POSITION = 29; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $xmpSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isSegmentXmp($segment)) { + $xmpSegments[$key] = $segment; + } + } + + if (empty($xmpSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertXmpSegment($segments, $this->createXmpSegment($metadata)) + ]); + } + + foreach ($xmpSegments as $key => $segment) { + $segments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Insert XMP segment to image segments (at position 1) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertXmpSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 2), [$xmpSegment], array_slice($segments, 2)); + } + + /** + * Write new segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function createXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Add metadata to the segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $data = $segment->getData(); + $start = substr($data, 0, self::XMP_DATA_START_POSITION); + $xmpData = substr($data, self::XMP_DATA_START_POSITION); + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $start . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Check if segment contains XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strncmp($segment->getData(), self::XMP_SEGMENT_START, self::XMP_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php new file mode 100644 index 0000000000000..403bc7f3d7449 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments reader + */ +class WriteFile implements WriteFileInterface +{ + private const MARKER_IMAGE_FILE_START = "\xD8"; + private const MARKER_IMAGE_PREFIX = "\xFF"; + private const MARKER_IMAGE_END = "\xD9"; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write file object to the filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + foreach ($file->getSegments() as $segment) { + if ($segment->getName() != 'CompressedImage' && strlen($segment->getData()) > 0xfffd) { + throw new LocalizedException(__('A Header is too large to fit in the segment!')); + } + } + + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->driver->fileWrite($resource, self::MARKER_IMAGE_PREFIX . self::MARKER_IMAGE_FILE_START); + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileWrite($resource, self::MARKER_IMAGE_PREFIX . self::MARKER_IMAGE_END); + $this->driver->fileClose($resource); + } + + /** + * Write jpeg segment + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + if ($segment->getName() !== 'CompressedImage') { + $this->driver->fileWrite( + $resource, + //phpcs:ignore Magento2.Functions.DiscouragedFunction + self::MARKER_IMAGE_PREFIX . chr($this->segmentNames->getSegmentType($segment->getName())) + ); + $this->driver->fileWrite($resource, pack("n", strlen($segment->getData()) + 2)); + } + $this->driver->fileWrite($resource, $segment->getData()); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php b/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php new file mode 100644 index 0000000000000..9e3ee5d29a495 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Media asset metadata data transfer object + */ +class Metadata implements MetadataInterface +{ + /** + * @var string + */ + private $title; + + /** + * @var string + */ + private $description; + + /** + * @var array + */ + private $keywords; + + /** + * @var MetadataExtensionInterface + */ + private $extensionAttributes; + + /** + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @param MetadataExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $title = null, + string $description = null, + array $keywords = null, + ?MetadataExtensionInterface $extensionAttributes = null + ) { + $this->title = $title; + $this->description = $description; + $this->keywords = $keywords; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getTitle(): ?string + { + return $this->title; + } + + /** + * @inheritdoc + */ + public function getKeywords(): ?array + { + return $this->keywords; + } + + /** + * @inheritdoc + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?MetadataExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?MetadataExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php new file mode 100644 index 0000000000000..673f8ff436ebe --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\Framework\Exception\ValidatorException; + +/** + * File segments reader + */ +class ReadFile implements ReadFileInterface +{ + private const PNG_FILE_START = "\x89PNG\x0d\x0a\x1a\x0a"; + private const PNG_MARKER_IMAGE_END = 'IEND'; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + $resource = $this->driver->fileOpen($path, 'rb'); + $header = $this->readHeader($resource); + + if ($header != self::PNG_FILE_START) { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a PNG image')); + } + + do { + $header = $this->readHeader($resource); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentHeader = unpack('Nsize/a4type', $header); + $data = $this->read($resource, $segmentHeader['size']); + $segments[] = $this->segmentFactory->create([ + 'name' => $segmentHeader['type'], + 'data' => $data + ]); + $cyclicRedundancyCheck = $this->read($resource, 4); + + if (pack('N', crc32($segmentHeader['type'] . $data)) != $cyclicRedundancyCheck) { + throw new LocalizedException(__('The image is corrupted')); + } + } while ($header + && $segmentHeader['type'] != self::PNG_MARKER_IMAGE_END + && !$this->driver->endOfFile($resource) + ); + + $this->driver->fileClose($resource); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read 8 bytes + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readHeader($resource): string + { + return $this->read($resource, 8); + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php new file mode 100644 index 0000000000000..09aeaf526443a --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Jpeg EXIF Reader + */ +class ReadExif implements ReadMetadataInterface +{ + private const EXIF_SEGMENT_NAME = 'eXIf'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + if (!is_callable('exif_read_data')) { + throw new LocalizedException( + __('exif_read_data() must be enabled in php configuration') + ); + } + + foreach ($file->getSegments() as $segment) { + if ($this->isExifSegment($segment)) { + return $this->getExifData($segment); + } + } + + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Parese exif data from segment + * + * @param SegmentInterface $segment + */ + private function getExifData(SegmentInterface $segment): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + $data = exif_read_data('data://image/jpeg;base64,' . base64_encode($segment->getData())); + + if ($data) { + $title = isset($data['DocumentName']) ? $data['DocumentName'] : null; + $description = isset($data['ImageDescription']) ? $data['ImageDescription'] : null; + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => !empty($keywords) ? $keywords : null + ]); + } + + /** + * Does segment contain Exif data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isExifSegment(SegmentInterface $segment): bool + { + return strcmp($segment->getName(), self::EXIF_SEGMENT_NAME) === 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php new file mode 100644 index 0000000000000..c856d95475a40 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * IPTC Reader to read IPTC data for png image + */ +class ReadIptc implements ReadMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'zTXt'; + private const IPTC_SEGMENT_START = 'iptc'; + private const IPTC_DATA_START_POSITION = 17; + private const IPTC_CHUNK_MARKER_LENGTH = 4; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isIptcSegment($segment)) { + if (!is_callable('gzcompress') && !is_callable('gzuncompress')) { + throw new LocalizedException( + __('zlib gzcompress() && zlib gzuncompress() must be enabled in php configuration') + ); + } + return $this->getIptcData($segment); + } + } + + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Read iptc data from zTXt segment + * + * @param SegmentInterface $segment + */ + private function getIptcData(SegmentInterface $segment): MetadataInterface + { + $description = null; + $title = null; + $keywords = null; + + $iptSegmentStartPosition = strpos($segment->getData(), pack("C", 0) . pack("C", 0) . 'x'); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $uncompressedData = gzuncompress(substr($segment->getData(), $iptSegmentStartPosition + 2)); + + $data = explode(PHP_EOL, trim($uncompressedData)); + //remove header and size from hex string + $iptcData = implode(array_slice($data, 2)); + $binData = hex2bin($iptcData); + + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $descriptionStartPosition = strpos($binData, $descriptionMarker); + if ($descriptionStartPosition) { + $description = substr( + $binData, + $descriptionStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $descriptionStartPosition + 3, 1)) + ); + } + + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $titleStartPosition = strpos($binData, $titleMarker); + if ($titleStartPosition) { + $title = substr( + $binData, + $titleStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $titleStartPosition + 3, 1)) + ); + } + + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywordsStartPosition = strpos($binData, $keywordsMarker); + if ($keywordsStartPosition) { + $keywords = substr( + $binData, + $keywordsStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $keywordsStartPosition + 3, 1)) + ); + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => !empty($keywords) ? explode(',', $keywords) : null + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp( + substr($segment->getData(), self::IPTC_DATA_START_POSITION, 4), + self::IPTC_SEGMENT_START, + self::IPTC_DATA_START_POSITION + ) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php new file mode 100644 index 0000000000000..518697d421474 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * PNG XMP Reader + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'iTXt'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * Read metadata from the file + * + * @param FileInterface $file + * @return MetadataInterface + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isXmpSegment($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmpSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strpos($segment->getData(), '<x:xmpmeta') !== -1; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), strpos($segment->getData(), '<x:xmpmeta')); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php new file mode 100644 index 0000000000000..9025ba9363fde --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php @@ -0,0 +1,230 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * IPTC Writer to write IPTC data for png image + */ +class WriteIptc implements WriteMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'zTXt'; + private const IPTC_SEGMENT_START = 'iptc'; + private const IPTC_DATA_START_POSITION = 17; + private const IPTC_SEGMENT_START_STRING = 'Raw profile type iptc'; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + } + + /** + * Write iptc metadata to zTXt segment + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $pngIptcSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isIptcSegment($segment)) { + $pngIptcSegments[$key] = $segment; + } + } + + if (!is_callable('gzcompress') && !is_callable('gzuncompress')) { + throw new LocalizedException( + __('zlib gzcompress() && zlib gzuncompress() must be enabled in php configuration') + ); + } + + if (empty($pngIptcSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertPngIptcSegment($segments, $this->createPngIptcSegment($metadata)) + ]); + } + + foreach ($pngIptcSegments as $key => $segment) { + $segments[$key] = $this->updateIptcSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Insert IPTC segment to image png segments before IEND chunk + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $iptcSegment + * @return SegmentInterface[] + */ + private function insertPngIptcSegment(array $segments, SegmentInterface $iptcSegment): array + { + $iendSegmentIndex = count($segments) - 1; + + return array_merge( + array_slice($segments, 0, $iendSegmentIndex), + [$iptcSegment], + array_slice($segments, $iendSegmentIndex) + ); + } + + /** + * Create new zTXt segment with metadata + * + * @param MetadataInterface $metadata + */ + private function createPngIptcSegment(MetadataInterface $metadata): SegmentInterface + { + $start = '8BIM' . str_repeat(pack('C', 4), 2) . str_repeat(pack("C", 0), 5) + . 'c' . pack('C', 28) . pack('C', 1); + $compression = 'Z' . pack('C', 0) . pack('C', 3) . pack('C', 27) . '%G' . pack('C', 28) . pack('C', 1); + $end = str_repeat(pack('C', 0), 2) . pack('C', 2) . pack('C', 0) . pack('C', 4) . pack('C', 28); + $binData = $start . $compression . $end; + + $description = $metadata->getDescription(); + if ($description !== null) { + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $binData .= $descriptionMarker . pack('C', strlen($description)) . $description . pack('C', 28); + } + + $title = $metadata->getTitle(); + if ($title !== null) { + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $binData .= $titleMarker . pack('C', strlen($title)) . $title . pack('C', 28); + } + + $keywords = $metadata->getKeywords(); + if ($keywords !== null) { + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywords = implode(',', $keywords); + $binData .= $keywordsMarker . pack('C', strlen($keywords)) . $keywords . pack('C', 28); + } + + $binData .= pack('C', 0); + $hexString = bin2hex($binData); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $compressedIptcData = gzcompress(PHP_EOL . 'iptc' . PHP_EOL . strlen($binData) . PHP_EOL . $hexString); + + return $this->segmentFactory->create([ + 'name' => self::IPTC_SEGMENT_NAME, + 'data' => self::IPTC_SEGMENT_START_STRING . str_repeat(pack('C', 0), 2) . $compressedIptcData + ]); + } + + /** + * Update iptc data to zTXt segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + */ + private function updateIptcSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $description = null; + $title = null; + $keywords = null; + + $iptSegmentStartPosition = strpos($segment->getData(), pack("C", 0) . pack("C", 0) . 'x'); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $uncompressedData = gzuncompress(substr($segment->getData(), $iptSegmentStartPosition + 2)); + + $data = explode(PHP_EOL, trim($uncompressedData)); + //remove header and size from hex string + $iptcData = implode(array_slice($data, 2)); + $binData = hex2bin($iptcData); + + if ($metadata->getDescription() !== null) { + $description = $metadata->getDescription(); + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $descriptionStartPosition = strpos($binData, $descriptionMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($description)) . $description, + $descriptionStartPosition + ) . substr($binData, $descriptionStartPosition + 1 + ord(substr($binData, $descriptionStartPosition))); + } + + if ($metadata->getTitle() !== null) { + $title = $metadata->getTitle(); + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $titleStartPosition = strpos($binData, $titleMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($title)) . $title, + $titleStartPosition + ) . substr($binData, $titleStartPosition + 1 + ord(substr($binData, $titleStartPosition))); + } + + if ($metadata->getKeywords() !== null) { + $keywords = implode(',', $metadata->getKeywords()); + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywordsStartPosition = strpos($binData, $keywordsMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($keywords)) . $keywords, + $keywordsStartPosition + ) . substr($binData, $keywordsStartPosition + 1 + ord(substr($binData, $keywordsStartPosition))); + } + $hexString = bin2hex($binData); + $iptcSegmentStart = substr($segment->getData(), 0, $iptSegmentStartPosition + 2); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentDataCompressed = gzcompress(PHP_EOL . $data[0] . PHP_EOL . strlen($binData) . PHP_EOL . $hexString); + + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $iptcSegmentStart . $segmentDataCompressed + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp( + substr($segment->getData(), self::IPTC_DATA_START_POSITION, 4), + self::IPTC_SEGMENT_START, + self::IPTC_DATA_START_POSITION + ) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php new file mode 100644 index 0000000000000..f03482ecf6054 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php @@ -0,0 +1,168 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * XMP Writer for png format + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'iTXt'; + private const XMP_SEGMENT_START = "XML:com.adobe.xmp\x00"; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add xmp metadata to the png file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $pngXmpSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isXmpSegment($segment)) { + $pngXmpSegments[$key] = $segment; + } + } + + if (empty($pngXmpSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertPngXmpSegment($segments, $this->createPngXmpSegment($metadata)) + ]); + } + + foreach ($pngXmpSegments as $key => $segment) { + $segments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Insert XMP segment to image png segments before IEND chunk + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertPngXmpSegment(array $segments, SegmentInterface $xmpSegment): array + { + $iendSegmentIndex = count($segments) - 1; + return array_merge( + array_slice($segments, 0, $iendSegmentIndex), + [$xmpSegment], + array_slice($segments, $iendSegmentIndex) + ); + } + + /** + * Write new png segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function createPngXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Add metadata to the png xmp segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($this->getXmpData($segment), $metadata) + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmpSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strpos($segment->getData(), '<x:xmpmeta') !== -1; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), strpos($segment->getData(), '<x:xmpmeta')); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php new file mode 100644 index 0000000000000..c5db6644b3545 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments reader + */ +class WriteFile implements WriteFileInterface +{ + private const PNG_FILE_START = "\x89PNG\x0d\x0a\x1a\x0a"; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write PNG file to filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->driver->fileWrite($resource, self::PNG_FILE_START); + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileClose($resource); + } + + /** + * Write PNG segments + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + $this->driver->fileWrite($resource, pack("N", strlen($segment->getData()))); + $this->driver->fileWrite($resource, pack("a4", $segment->getName())); + $this->driver->fileWrite($resource, $segment->getData()); + $this->driver->fileWrite($resource, pack("N", crc32($segment->getName() . $segment->getData()))); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Segment.php b/app/code/Magento/MediaGalleryMetadata/Model/Segment.php new file mode 100644 index 0000000000000..0e8a89767e40c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Segment.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Segment internal data transfer object + */ +class Segment implements SegmentInterface +{ + /** + * @var array + */ + private $name; + + /** + * @var string + */ + private $data; + + /** + * @var SegmentExtensionInterface + */ + private $extensionAttributes; + + /** + * @param string $name + * @param string $data + * @param SegmentExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $name, + string $data, + ?SegmentExtensionInterface $extensionAttributes = null + ) { + $this->name = $name; + $this->data = $data; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritdoc + */ + public function getData(): string + { + return $this->data; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?SegmentExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?SegmentExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php b/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php new file mode 100644 index 0000000000000..62eea09453ae5 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +/** + * Segment types to names mapper + */ +class SegmentNames +{ + private const SEGMENT_TYPE_TO_NAME = [ + 0xC0 => "SOF0", + 0xC1 => "SOF1", + 0xC2 => "SOF2", + 0xC3 => "SOF4", + 0xC5 => "SOF5", + 0xC6 => "SOF6", + 0xC7 => "SOF7", + 0xC8 => "JPG", + 0xC9 => "SOF9", + 0xCA => "SOF10", + 0xCB => "SOF11", + 0xCD => "SOF13", + 0xCE => "SOF14", + 0xCF => "SOF15", + 0xC4 => "DHT", + 0xCC => "DAC", + 0xD0 => "RST0", + 0xD1 => "RST1", + 0xD2 => "RST2", + 0xD3 => "RST3", + 0xD4 => "RST4", + 0xD5 => "RST5", + 0xD6 => "RST6", + 0xD7 => "RST7", + 0xD8 => "SOI", + 0xD9 => "EOI", + 0xDA => "SOS", + 0xDB => "DQT", + 0xDC => "DNL", + 0xDD => "DRI", + 0xDE => "DHP", + 0xDF => "EXP", + 0xE0 => "APP0", + 0xE1 => "APP1", + 0xE2 => "APP2", + 0xE3 => "APP3", + 0xE4 => "APP4", + 0xE5 => "APP5", + 0xE6 => "APP6", + 0xE7 => "APP7", + 0xE8 => "APP8", + 0xE9 => "APP9", + 0xEA => "APP10", + 0xEB => "APP11", + 0xEC => "APP12", + 0xED => "APP13", + 0xEE => "APP14", + 0xEF => "APP15", + 0xF0 => "JPG0", + 0xF1 => "JPG1", + 0xF2 => "JPG2", + 0xF3 => "JPG3", + 0xF4 => "JPG4", + 0xF5 => "JPG5", + 0xF6 => "JPG6", + 0xF7 => "JPG7", + 0xF8 => "JPG8", + 0xF9 => "JPG9", + 0xFA => "JPG10", + 0xFB => "JPG11", + 0xFC => "JPG12", + 0xFD => "JPG13", + 0xFE => "COM", + 0x01 => "TEM", + 0x02 => "RES", + ]; + + /** + * Get segment name by type + * + * @param int $type + * @return string + */ + public function getSegmentName(int $type): string + { + return self::SEGMENT_TYPE_TO_NAME[$type]; + } + + /** + * Get segment type by name + * + * @param string $name + * @return int + */ + public function getSegmentType(string $name): int + { + return array_search($name, self::SEGMENT_TYPE_TO_NAME); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php b/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php new file mode 100644 index 0000000000000..a7d07f66ba8aa --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Module\Dir; +use Magento\Framework\Module\Dir\Reader; + +/** + * XMP template provider + */ +class XmpTemplate +{ + private const XMP_TEMPLATE_FILENAME = 'default.xmp'; + + /** + * @var Reader + */ + private $moduleReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @param Reader $moduleReader + * @param DriverInterface $driver + */ + public function __construct(Reader $moduleReader, DriverInterface $driver) + { + $this->moduleReader = $moduleReader; + $this->driver = $driver; + } + + /** + * Get default XMP template + * + * @return string + * @throws FileSystemException + */ + public function get(): string + { + $etcDirectoryPath = $this->moduleReader->getModuleDir( + Dir::MODULE_ETC_DIR, + 'Magento_MediaGalleryMetadata' + ); + return $this->driver->fileGetContents( + $etcDirectoryPath . '/' . self::XMP_TEMPLATE_FILENAME + ); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/README.md b/app/code/Magento/MediaGalleryMetadata/README.md new file mode 100644 index 0000000000000..ec74e527ddebb --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryMetadata + +The purpose of this module is to provide an ability to extract the metadata from file and populating Media Asset entity fields when an image is uploaded to Magento and also provide an ability to update the metadata stored in an image file. diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php new file mode 100644 index 0000000000000..c284bf71e60af --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php @@ -0,0 +1,197 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * ExtractMetadata test + */ +class AddMetadataTest extends TestCase +{ + /** + * @var AddMetadataInterface + */ + private $addMetadata; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->addMetadata = Bootstrap::getObjectManager()->get(AddMetadataInterface::class); + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataInterfaceFactory::class); + $this->extractMetadata = Bootstrap::getObjectManager()->get(ExtractMetadataInterface::class); + } + + /** + * Test for ExtractMetadata::execute + * + * @dataProvider filesProvider + * @param null|string $fileName + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @throws LocalizedException + */ + public function testExecute( + ?string $fileName, + ?string $title, + ?string $description, + ?array $keywords + ): void { + $path = realpath(__DIR__ . '/../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $metadata = $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + + $this->addMetadata->execute($modifiableFilePath, $metadata); + + $updatedMetadata = $this->extractMetadata->execute($modifiableFilePath); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + + $this->driver->deleteFile($modifiableFilePath); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'iptc_only.png', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'macos-photos.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'macos-photos.jpeg', + 'Updated Title', + null, + null + ], + [ + 'iptc_only.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'empty_iptc.jpeg', + 'Updated Title', + null, + null + ], + [ + 'macos-preview.png', + 'Title of the magento image 2', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ], + [ + 'empty_xmp_image.jpeg', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ], + ], + [ + 'empty_xmp_image.png', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ], + ], + [ + 'exiftool.gif', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'empty_exiftool.gif', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php new file mode 100644 index 0000000000000..ebe96183eb1f2 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for ExtractMetadata + */ +class ExtractMetadataTest extends TestCase +{ + /** + * @var ExtractMetadataComposite + */ + private $extractMetadata; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->extractMetadata = Bootstrap::getObjectManager()->get(ExtractMetadataInterface::class); + } + + /** + * Test for ExtractMetadata::execute + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param null|array $keywords + * @throws LocalizedException + */ + public function testExecute( + string $fileName, + string $title, + string $description, + ?array $keywords + ): void { + $path = realpath(__DIR__ . '/../../_files/' . $fileName); + $metadata = $this->extractMetadata->execute($path); + + $this->assertEquals($title, $metadata->getTitle()); + $this->assertEquals($description, $metadata->getDescription()); + $this->assertEquals($keywords, $metadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'exif_image.png', + 'Exif title png imge', + 'Exif description png imge', + null + ], + [ + 'exif-image.jpeg', + 'Exif Magento title', + 'Exif description metadata', + null + ], + [ + 'macos-photos.jpeg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'macos-preview.png', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'iptc_only.jpeg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'exiftool.gif', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'iptc_only.png', + 'Title of the magento image', + 'PNG format is awesome', + [ + 'png', + 'awesome' + ] + ], + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php new file mode 100644 index 0000000000000..4bba73e3ca2a9 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Gif\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Gif\Segment\WriteXmp; +use Magento\MediaGalleryMetadata\Model\Gif\Segment\ReadXmp; +use Magento\MediaGalleryMetadata\Model\Gif\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for XMP reader and writer gif format + */ +class XmpTest extends TestCase +{ + /** + * @var WriteXmp + */ + private $xmpWriter; + + /** + * @var ReadXmp + */ + private $xmpReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->xmpWriter = Bootstrap::getObjectManager()->get(WriteXmp::class); + $this->xmpReader = Bootstrap::getObjectManager()->get(ReadXmp::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for XMP reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteReadGif( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $file = $this->fileReader->execute($path); + $originalGifMetadata = $this->xmpReader->execute($file); + + $this->assertEmpty($originalGifMetadata->getTitle()); + $this->assertEmpty($originalGifMetadata->getDescription()); + $this->assertEmpty($originalGifMetadata->getKeywords()); + $updatedGifFile = $this->xmpWriter->execute( + $file, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $updatedGifMetadata = $this->xmpReader->execute($updatedGifFile); + $this->assertEquals($title, $updatedGifMetadata->getTitle()); + $this->assertEquals($description, $updatedGifMetadata->getDescription()); + $this->assertEquals($keywords, $updatedGifMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_exiftool.gif', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php new file mode 100644 index 0000000000000..932b71df28430 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Jpeg\Segment; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteIptc; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadIptc; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for IPTC reader and writer + */ +class IptcTest extends TestCase +{ + /** + * @var WriteIptc + */ + private $iptcWriter; + + /** + * @var ReadIptc + */ + private $iptcReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->iptcWriter = Bootstrap::getObjectManager()->get(WriteIptc::class); + $this->iptcReader = Bootstrap::getObjectManager()->get(ReadIptc::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for IPTC reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $modifiableFilePath = $this->fileReader->execute($modifiableFilePath); + $originalMetadata = $this->iptcReader->execute($modifiableFilePath); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + + $updatedFile = $this->iptcWriter->execute( + $modifiableFilePath, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + + $updatedMetadata = $this->iptcReader->execute($updatedFile); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_iptc.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php new file mode 100644 index 0000000000000..043e26f67853f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Jpeg\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteXmp; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadXmp; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for XMP reader and writer + */ +class XmpTest extends TestCase +{ + /** + * @var WriteXmp + */ + private $xmpWriter; + + /** + * @var ReadXmp + */ + private $xmpReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->xmpWriter = Bootstrap::getObjectManager()->get(WriteXmp::class); + $this->xmpReader = Bootstrap::getObjectManager()->get(ReadXmp::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for XMP reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $file = $this->fileReader->execute($path); + $originalMetadata = $this->xmpReader->execute($file); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + $updatedFile = $this->xmpWriter->execute( + $file, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $updatedMetadata = $this->xmpReader->execute($updatedFile); + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_xmp_image.jpeg', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php new file mode 100644 index 0000000000000..d8bcfd7a94561 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Png\Segment; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Png\Segment\WriteIptc; +use Magento\MediaGalleryMetadata\Model\Png\Segment\ReadIptc; +use Magento\MediaGalleryMetadata\Model\Png\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for IPTC reader and writer + */ +class IptcTest extends TestCase +{ + /** + * @var WriteIptc + */ + private $iptcWriter; + + /** + * @var ReadIptc + */ + private $iptcReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->iptcWriter = Bootstrap::getObjectManager()->get(WriteIptc::class); + $this->iptcReader = Bootstrap::getObjectManager()->get(ReadIptc::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for IPTC reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $modifiableFilePath = $this->fileReader->execute($modifiableFilePath); + $originalMetadata = $this->iptcReader->execute($modifiableFilePath); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + + $updatedFile = $this->iptcWriter->execute( + $modifiableFilePath, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + + $updatedMetadata = $this->iptcReader->execute($updatedFile); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_iptc.png', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif new file mode 100644 index 0000000000000..14cc6026b5950 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg new file mode 100644 index 0000000000000..1a345c2d33fdd Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png new file mode 100644 index 0000000000000..129c49a1b7e64 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg new file mode 100644 index 0000000000000..cee7bff38a6c6 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png new file mode 100644 index 0000000000000..7e81891ebc0ee Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/exif-image.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/exif-image.jpeg new file mode 100644 index 0000000000000..cfe27433fd9fc Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/exif-image.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/exif_image.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/exif_image.png new file mode 100644 index 0000000000000..4a6bf30c2d516 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/exif_image.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif b/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif new file mode 100644 index 0000000000000..70574d70b609e Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg new file mode 100644 index 0000000000000..5d7dba35fede7 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png new file mode 100644 index 0000000000000..9b4821c1c4e5d Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg new file mode 100644 index 0000000000000..3a07b6abe788e Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png new file mode 100644 index 0000000000000..95eb45f69b3ea Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/composer.json b/app/code/Magento/MediaGalleryMetadata/composer.json new file mode 100644 index 0000000000000..c2ce66ce64c36 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-media-gallery-metadata", + "description": "Magento module responsible for images metadata processing", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-metadata-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryMetadata\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/etc/default.xmp b/app/code/Magento/MediaGalleryMetadata/etc/default.xmp new file mode 100644 index 0000000000000..772b6af671ec6 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/etc/default.xmp @@ -0,0 +1,24 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> +<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0"> + <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <rdf:Description rdf:about="" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"> + <dc:subject> + <rdf:Seq> + <rdf:li>magento</rdf:li> + </rdf:Seq> + </dc:subject> + <dc:description><rdf:Alt><rdf:li xml:lang="x-default">Magento</rdf:li></rdf:Alt></dc:description> + <dc:title><rdf:Alt><rdf:li xml:lang="x-default">Magento</rdf:li></rdf:Alt></dc:title> + <dc:subject> + <rdf:Bag> + <rdf:li>magento</rdf:li> + <rdf:li>mediagallerymetadata</rdf:li> + </rdf:Bag> + </dc:subject> + </rdf:Description> + </rdf:RDF> +</x:xmpmeta> +<?xpacket end="w"?> \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryMetadata/etc/di.xml b/app/code/Magento/MediaGalleryMetadata/etc/di.xml new file mode 100644 index 0000000000000..4cd9a34e43a93 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/etc/di.xml @@ -0,0 +1,129 @@ +<?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\MediaGalleryMetadataApi\Api\Data\MetadataInterface" type="Magento\MediaGalleryMetadata\Model\Metadata"/> + <preference for="Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface" type="Magento\MediaGalleryMetadataApi\Model\AddMetadataComposite"/> + <preference for="Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface" type="Magento\MediaGalleryMetadataApi\Model\ExtractMetadataComposite"/> + <preference for="Magento\MediaGalleryMetadataApi\Model\FileInterface" type="Magento\MediaGalleryMetadata\Model\File"/> + <preference for="Magento\MediaGalleryMetadataApi\Model\SegmentInterface" type="Magento\MediaGalleryMetadata\Model\Segment"/> + <type name="Magento\MediaGalleryMetadataApi\Model\ExtractMetadataComposite"> + <arguments> + <argument name="extractors" xsi:type="array"> + <item name="jpeg" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ExtractMetadata</item> + <item name="png" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ExtractMetadata</item> + <item name="gif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ExtractMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadataApi\Model\AddMetadataComposite"> + <arguments> + <argument name="writers" xsi:type="array"> + <item name="jpeg" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\AddMetadata</item> + <item name="png" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\AddMetadata</item> + <item name="gif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\AddMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Gif\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Png\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Jpeg\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Png\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Gif\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\XmpTemplate"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\AddIptcMetadata"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <virtualType name="Magento\MediaGalleryMetadata\Model\Jpeg\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Png\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\WriteXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\WriteIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Gif\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\Segment\WriteXmp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Gif\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\Segment\ReadXmp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Png\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\ReadXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\ReadIptc</item> + <item name="exif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\ReadExif</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Jpeg\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadIptc</item> + <item name="exif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadExif</item> + </argument> + </arguments> + </virtualType> +</config> diff --git a/app/code/Magento/MediaGalleryMetadata/etc/module.xml b/app/code/Magento/MediaGalleryMetadata/etc/module.xml new file mode 100644 index 0000000000000..776b05aecd284 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/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_MediaGalleryMetadata"/> +</config> diff --git a/app/code/Magento/MediaGalleryMetadata/registration.php b/app/code/Magento/MediaGalleryMetadata/registration.php new file mode 100644 index 0000000000000..fcf6789d9321f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/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_MediaGalleryMetadata', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php new file mode 100644 index 0000000000000..df645681e8971 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Add metadata to asset file + */ +interface AddMetadataInterface +{ + /** + * Add metadata to the asset file + * + * @param string $path + * @param MetadataInterface $metadata + * @throws LocalizedException + */ + public function execute(string $path, MetadataInterface $metadata): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php new file mode 100644 index 0000000000000..63e943150f4a7 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface; + +/** + * Media asset metadata data transfer object + */ +interface MetadataInterface extends ExtensibleDataInterface +{ + /** + * Get asset title + * + * @return null|string + */ + public function getTitle(): ?string; + + /** + * Get asset description + * + * @return null|string + */ + public function getDescription(): ?string; + + /** + * Get asset keywords + * + * @return null|array + */ + public function getKeywords(): ?array; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface|null + */ + public function getExtensionAttributes(): ?MetadataExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?MetadataExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php new file mode 100644 index 0000000000000..2327406db8bef --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Extract asset metadata + */ +interface ExtractMetadataInterface +{ + /** + * Extract metadata from the asset file + * + * @param string $path + * @return MetadataInterface + */ + public function execute(string $path): MetadataInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt b/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/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/MediaGalleryMetadataApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/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/MediaGalleryMetadataApi/Model/AddMetadataComposite.php b/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php new file mode 100644 index 0000000000000..fc3f53313199d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata writer pool + */ +class AddMetadataComposite implements AddMetadataInterface +{ + /** + * @var AddMetadataInterface[] + */ + private $writers; + + /** + * @param AddMetadataInterface[] $writers + */ + public function __construct(array $writers) + { + $this->writers = $writers; + } + + /** + * Write metadata to the path + * + * @param string $path + * @param MetadataInterface $data + * @throws LocalizedException + */ + public function execute(string $path, MetadataInterface $data): void + { + foreach ($this->writers as $writer) { + if (!$writer instanceof AddMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($writer) . ' must implement ' . AddMetadataInterface::class) + ); + } + + $writer->execute($path, $data); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php new file mode 100644 index 0000000000000..0d6e8aa345178 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; + +/** + * Metadata extractor composite + */ +class ExtractMetadataComposite implements ExtractMetadataInterface +{ + /** + * @var ExtractMetadataInterface[] + */ + private $extractors; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param ExtractMetadataInterface[] $extractors + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory, + array $extractors + ) { + $this->metadataFactory = $metadataFactory; + $this->extractors = $extractors; + } + + /** + * Extract metadata from file + * + * @param string $path + * @return MetadataInterface + * @throws LocalizedException + */ + public function execute(string $path): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + foreach ($this->extractors as $extractor) { + if (!$extractor instanceof ExtractMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($extractor) . ' must implement ' . ExtractMetadataInterface::class) + ); + } + + $data = $extractor->execute($path); + $title = !empty($data->getTitle()) ? $data->getTitle() : $title; + $description = !empty($data->getDescription()) ? $data->getDescription() : $description; + + if (!empty($data->getKeywords())) { + foreach ($data->getKeywords() as $keyword) { + $keywords[] = $keyword; + } + } + } + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => empty($keywords) ? null : array_unique($keywords) + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php new file mode 100644 index 0000000000000..0cd01bbf57c64 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface; + +/** + * File internal data transfer object + */ +interface FileInterface extends ExtensibleDataInterface +{ + /** + * Get file path + * + * @return string + */ + public function getPath(): string; + + /** + * Get metadata sections + * + * @return SegmentInterface[] + */ + public function getSegments(): array; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface|null + */ + public function getExtensionAttributes(): ?FileExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?FileExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php new file mode 100644 index 0000000000000..e45a934f7b5ad --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +/** + * File reader + */ +interface ReadFileInterface +{ + /** + * Create file object from the file + * + * @param string $path + * @return FileInterface + */ + public function execute(string $path): FileInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php new file mode 100644 index 0000000000000..b6d97118f848b --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata reader + */ +interface ReadMetadataInterface +{ + /** + * Read metadata from the file + * + * @param FileInterface $file + * @return MetadataInterface + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): MetadataInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php new file mode 100644 index 0000000000000..bf6cdc30306f8 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface; + +/** + * Segment internal data transfer object + */ +interface SegmentInterface extends ExtensibleDataInterface +{ + /** + * Get segment name + * + * @return string + */ + public function getName(): string; + + /** + * Get segment data + * + * @return string + */ + public function getData(): string; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface|null + */ + public function getExtensionAttributes(): ?SegmentExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?SegmentExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php new file mode 100644 index 0000000000000..fe7579989c40f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; + +/** + * File writer + */ +interface WriteFileInterface +{ + /** + * Write file to filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php new file mode 100644 index 0000000000000..943879ebaec86 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata writer + */ +interface WriteMetadataInterface +{ + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $data + */ + public function execute(FileInterface $file, MetadataInterface $data): FileInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/README.md b/app/code/Magento/MediaGalleryMetadataApi/README.md new file mode 100644 index 0000000000000..82f86d2f61c6d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryMetadataApi + +The Magento_MediaGalleryMetadataApi module is responsible for the media gallery metadata implementation API. diff --git a/app/code/Magento/MediaGalleryMetadataApi/composer.json b/app/code/Magento/MediaGalleryMetadataApi/composer.json new file mode 100644 index 0000000000000..f8673884b050c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-metadata-api", + "description": "Magento module responsible for media gallery metadata implementation API", + "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\\MediaGalleryMetadataApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/etc/module.xml b/app/code/Magento/MediaGalleryMetadataApi/etc/module.xml new file mode 100644 index 0000000000000..77adbc6efff88 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/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_MediaGalleryMetadataApi"/> +</config> diff --git a/app/code/Magento/MediaGalleryMetadataApi/registration.php b/app/code/Magento/MediaGalleryMetadataApi/registration.php new file mode 100644 index 0000000000000..90988681a5483 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/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_MediaGalleryMetadataApi', + __DIR__ +); 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/Mftf/ActionGroup/AdminRenditionsSetImageSizeActionGroup.xml b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/ActionGroup/AdminRenditionsSetImageSizeActionGroup.xml new file mode 100644 index 0000000000000..b841d064aab7e --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/ActionGroup/AdminRenditionsSetImageSizeActionGroup.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="AdminRenditionsSetImageSizeActionGroup"> + <arguments> + <argument name="width" defaultValue="1000" type="string"/> + <argument name="height" defaultValue="1000" type="string"/> + </arguments> + <magentoCLI command="config:set system/media_gallery_renditions/width {{width}}" stepKey="setWidth"/> + <magentoCLI command="config:set system/media_gallery_renditions/height {{height}}" stepKey="setHeight"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertLargeImageFileSizeTest.xml b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertLargeImageFileSizeTest.xml new file mode 100644 index 0000000000000..d859f4852aaaf --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertLargeImageFileSizeTest.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="AdminMediaGalleryInsertLargeImageFileSizeTest"> + <annotations> + <features value="AdminMediaGalleryInsertLargeImageFileSizeTest"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1806"/> + <title value="Admin user should see correct image file size after rendition"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1507933/scenarios/5200023"/> + <stories value="User inserts image rendition to the content"/> + <description value="Admin user should see correct image file size after rendition"/> + <severity value="AVERAGE"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete uploaded image --> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPageFoDelete"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItemForDelete"> + <argument name="categoryName" value="$category.name$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryForDelete"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clickDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + <!-- Delete category --> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + + <!-- Open category page --> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"> + <argument name="categoryName" value="$category.name$"/> + </actionGroup> + + <!-- Add image to category from gallery --> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="addCategoryImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImage"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="addSelected"/> + + + <!-- Assert added image size --> + <actionGroup ref="AdminAssertImageUploadFileSizeThanActionGroup" stepKey="assertSize"> + <argument name="fileSize" value="26 KB"/> + </actionGroup> + </test> +</tests> 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/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg new file mode 100644 index 0000000000000..6dc8cd69e41c1 Binary files /dev/null and b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg differ 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..b60a858da5f26 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml @@ -0,0 +1,28 @@ +<?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 Image Optimization</label> + <comment>Resize images to improve performance and decrease the file size. When you use an image from Media Gallery on the storefront, the smaller image is generated and placed instead of the original. + Changing these settings will update all generated images.</comment> + <field id="width" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Maximum Width</label> + <validate>validate-zero-or-greater validate-digits</validate> + <comment>Enter the maximum width of an image in pixels.</comment> + </field> + <field id="height" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Maximum Height</label> + <validate>validate-zero-or-greater validate-digits</validate> + <comment>Enter the maximum height of an image in pixels.</comment> + </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/Console/Command/Synchronize.php b/app/code/Magento/MediaGallerySynchronization/Console/Command/Synchronize.php new file mode 100644 index 0000000000000..339aca84ec68f --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Console/Command/Synchronize.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Console\Command; + +use Magento\Framework\Console\Cli; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Synchronize files in media storage and media assets database records + */ +class Synchronize extends Command +{ + /** + * @var SynchronizeInterface + */ + private $synchronizeAssets; + + /** + * @param SynchronizeInterface $synchronizeAssets + */ + public function __construct( + SynchronizeInterface $synchronizeAssets + ) { + $this->synchronizeAssets = $synchronizeAssets; + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $this->setName('media-gallery:sync'); + $this->setDescription( + 'Synchronize media storage and media assets in the database' + ); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Synchronizing assets information from media storage to database...'); + + $this->synchronizeAssets->execute(); + + $output->writeln('Completed assets synchronization.'); + + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/LICENSE.txt b/app/code/Magento/MediaGallerySynchronization/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/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/MediaGallerySynchronization/LICENSE_AFL.txt b/app/code/Magento/MediaGallerySynchronization/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/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/Consume.php b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php new file mode 100644 index 0000000000000..b796d4225d08c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; + +/** + * Media gallery image synchronization queue consumer. + */ +class Consume +{ + /** + * @var SynchronizeInterface + */ + private $synchronize; + + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + + /** + * @param SynchronizeInterface $synchronize + * @param SynchronizeFilesInterface $synchronizeFiles + */ + 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(array $paths) : void + { + 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 new file mode 100644 index 0000000000000..b4c360c3e0538 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGallerySynchronization\Model\Filesystem\GetFileInfo; +use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface; + +/** + * Create media asset object based on the file information + */ +class CreateAssetFromFile implements CreateAssetFromFileInterface +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var File + */ + private $driver; + + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var GetContentHash + */ + private $getContentHash; + + /** + * @var GetFileInfo + */ + private $getFileInfo; + + /** + * @param Filesystem $filesystem + * @param File $driver + * @param AssetInterfaceFactory $assetFactory + * @param GetContentHash $getContentHash + * @param GetFileInfo $getFileInfo + */ + public function __construct( + Filesystem $filesystem, + File $driver, + AssetInterfaceFactory $assetFactory, + GetContentHash $getContentHash, + GetFileInfo $getFileInfo + ) { + $this->filesystem = $filesystem; + $this->driver = $driver; + $this->assetFactory = $assetFactory; + $this->getContentHash = $getContentHash; + $this->getFileInfo = $getFileInfo; + } + + /** + * @inheritdoc + */ + public function execute(string $path): AssetInterface + { + $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); + $file = $this->getFileInfo->execute($absolutePath); + [$width, $height] = getimagesize($absolutePath); + + return $this->assetFactory->create( + [ + 'id' => null, + 'path' => $path, + 'title' => $file->getBasename(), + 'width' => $width, + 'height' => $height, + 'hash' => $this->getHash($path), + 'size' => $file->getSize(), + 'contentType' => 'image/' . $file->getExtension(), + 'source' => 'Local' + ] + ); + } + + /** + * Get hash image content. + * + * @param string $path + * @return string + * @throws FileSystemException + */ + private function getHash(string $path): string + { + return $this->getContentHash->execute($this->getMediaDirectory()->readFile($path)); + } + + /** + * Retrieve media directory instance with read access + * + * @return ReadInterface + */ + private function getMediaDirectory(): ReadInterface + { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/FetchBatches.php b/app/code/Magento/MediaGallerySynchronization/Model/FetchBatches.php new file mode 100644 index 0000000000000..efc79d3c32423 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/FetchBatches.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\FlagManager; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; +use Psr\Log\LoggerInterface; + +/** + * Select data from database by provided batch size + */ +class FetchBatches implements FetchBatchesInterface +{ + private const LAST_EXECUTION_TIME_CODE = 'media_content_last_execution'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var int + */ + private $pageSize; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var FlagManager + */ + private $flagManager; + + /** + * @param FlagManager $flagManager + * @param ResourceConnection $resourceConnection + * @param LoggerInterface $logger + * @param int $pageSize + */ + public function __construct( + FlagManager $flagManager, + ResourceConnection $resourceConnection, + LoggerInterface $logger, + int $pageSize + ) { + $this->flagManager = $flagManager; + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + $this->pageSize = $pageSize; + } + + /** + * Get data from table by batches, based on limit offset value. + * + * @param string $tableName + * @param array $columns + * @param string|null $dateColumnName + */ + public function execute(string $tableName, array $columns, ?string $dateColumnName = null): \Traversable + { + try { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName($tableName); + $totalPages = $this->getTotalPages($tableName); + + for ($page = 0; $page < $totalPages; $page++) { + $offset = $page * $this->pageSize; + $select = $connection->select() + ->from($this->resourceConnection->getTableName($tableName), $columns) + ->limit($this->pageSize, $offset); + if (!empty($dateColumnName)) { + $select = $this->addLastExecutionCondition($select, $dateColumnName); + } + yield $connection->fetchAll($select); + } + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new LocalizedException( + __( + 'Could not fetch data from %tableName', + [ + 'tableName' => $tableName + ] + ) + ); + } + } + + /** + * Get where condition if last execution time set + * + * @param Select $select + * @param string $dateColumnName + * @return Select + */ + private function addLastExecutionCondition(Select $select, string $dateColumnName): Select + { + $lastExecutionTime = $this->flagManager->getFlagData(self::LAST_EXECUTION_TIME_CODE); + if (!empty($lastExecutionTime)) { + return $select->where($dateColumnName . ' > ?', $lastExecutionTime); + } + return $select; + } + + /** + * Return number of total pages by page size + * + * @param string $tableName + * @return float + */ + private function getTotalPages(string $tableName): float + { + $connection = $this->resourceConnection->getConnection(); + $total = $connection->fetchOne($connection->select()->from($tableName, 'COUNT(*)')); + return ceil($total / $this->pageSize); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/FetchMediaStorageFileBatches.php b/app/code/Magento/MediaGallerySynchronization/Model/FetchMediaStorageFileBatches.php new file mode 100644 index 0000000000000..0643673ae30ab --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/FetchMediaStorageFileBatches.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Psr\Log\LoggerInterface; + +/** + * Fetch files from media storage in batches + */ +class FetchMediaStorageFileBatches +{ + /** + * @var GetAssetsIterator + */ + private $getAssetsIterator; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var File + */ + private $driver; + + /** + * @var string + */ + private $fileExtensions; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var int + */ + private $batchSize; + + /** + * @param LoggerInterface $log + * @param IsPathExcludedInterface $isPathExcluded + * @param Filesystem $filesystem + * @param GetAssetsIterator $assetsIterator + * @param File $driver + * @param int $batchSize + * @param array $fileExtensions + */ + public function __construct( + LoggerInterface $log, + IsPathExcludedInterface $isPathExcluded, + Filesystem $filesystem, + GetAssetsIterator $assetsIterator, + File $driver, + int $batchSize, + array $fileExtensions + ) { + $this->log = $log; + $this->isPathExcluded = $isPathExcluded; + $this->getAssetsIterator = $assetsIterator; + $this->filesystem = $filesystem; + $this->driver = $driver; + $this->batchSize = $batchSize; + $this->fileExtensions = $fileExtensions; + } + + /** + * Return files from files system by provided size of batch + */ + public function execute(): \Traversable + { + $i = 0; + $batch = []; + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + + /** @var \SplFileInfo $file */ + foreach ($this->getAssetsIterator->execute($mediaDirectory->getAbsolutePath()) as $file) { + $relativePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) + ->getRelativePath($file->getPathName()); + if (!$this->isApplicable($relativePath)) { + continue; + } + + $batch[] = $relativePath; + if (++$i == $this->batchSize) { + yield $batch; + $i = 0; + $batch = []; + } + } + if (count($batch) > 0) { + yield $batch; + } + } + + /** + * Can synchronization be applied to asset with provided path + * + * @param string $path + * @return bool + */ + private function isApplicable(string $path): bool + { + try { + return $path + && !$this->isPathExcluded->execute($path) + && preg_match('#\.(' . implode("|", $this->fileExtensions) . ')$# i', $path); + } catch (\Exception $exception) { + $this->log->critical($exception); + return false; + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php new file mode 100644 index 0000000000000..5e523fd0e905a --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php @@ -0,0 +1,148 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model\Filesystem; + +/** + * Class for getting image file information. + */ +class FileInfo +{ + /** + * @var string + */ + private $path; + + /** + * @var string + */ + private $filename; + + /** + * @var string + */ + private $extension; + + /** + * @var $basename + */ + private $basename; + + /** + * @var int + */ + private $size; + + /** + * @var int + */ + private $mTime; + + /** + * @var int + */ + private $cTime; + + /** + * FileInfo constructor. + * + * @param string $path + * @param string $filename + * @param string $extension + * @param string $basename + * @param int $size + * @param int $mTime + * @param int $cTime + */ + public function __construct( + string $path, + string $filename, + string $extension, + string $basename, + int $size, + int $mTime, + int $cTime + ) { + $this->path = $path; + $this->filename = $filename; + $this->extension = $extension; + $this->basename = $basename; + $this->size = $size; + $this->mTime = $mTime; + $this->cTime = $cTime; + } + + /** + * Get path without filename. + * + * @return string + */ + public function getPath(): string + { + return $this->path; + } + + /** + * Get filename. + * + * @return string + */ + public function getFilename(): string + { + return $this->filename; + } + + /** + * Get file extension. + * + * @return string + */ + public function getExtension(): string + { + return $this->extension; + } + + /** + * Get file basename. + * + * @return string + */ + public function getBasename(): string + { + return $this->basename; + } + + /** + * Get file size. + * + * @return int + */ + public function getSize(): int + { + return $this->size; + } + + /** + * Get last modified time. + * + * @return int + */ + public function getMTime(): int + { + return $this->mTime; + } + + /** + * Get inode change time. + * + * @return int + */ + public function getCTime(): int + { + return $this->cTime; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php new file mode 100644 index 0000000000000..8f9080767d6e3 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model\Filesystem; + +use Magento\MediaGallerySynchronization\Model\Filesystem\FileInfoFactory; + +/** + * Get file information + */ +class GetFileInfo +{ + /** + * @var FileInfoFactory + */ + private $fileInfoFactory; + + /** + * GetFileInfo constructor. + * @param FileInfoFactory $fileInfoFactory + */ + public function __construct( + FileInfoFactory $fileInfoFactory + ) { + $this->fileInfoFactory = $fileInfoFactory; + } + + /** + * Get file information based on path provided. + * + * @param string $path + * @return FileInfo + */ + public function execute(string $path): FileInfo + { + $splFileInfo = new \SplFileInfo($path); + + return $this->fileInfoFactory->create([ + 'path' => $splFileInfo->getPath(), + 'filename' => $splFileInfo->getFilename(), + 'extension' => $splFileInfo->getExtension(), + 'basename' => $splFileInfo->getBasename('.' . $splFileInfo->getExtension()), + 'size' => $splFileInfo->getSize(), + 'mTime' => $splFileInfo->getMTime(), + 'cTime' => $splFileInfo->getCTime() + ]); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/SplFileInfoFactory.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/SplFileInfoFactory.php new file mode 100644 index 0000000000000..1fbfae640a732 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/SplFileInfoFactory.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model\Filesystem; + +/** + * Creates a new file based on the file name parameter. + */ +class SplFileInfoFactory +{ + /** + * Creates SplFileInfo from filename + * + * @param string $fileName + * @return \SplFileInfo + */ + public function create(string $fileName) : \SplFileInfo + { + return new \SplFileInfo($fileName); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php new file mode 100644 index 0000000000000..be672666786dd --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +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 + */ +class GetAssetFromPath +{ + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var CreateAssetFromFileInterface + */ + private $createAssetFromFile; + + /** + * @param AssetInterfaceFactory $assetFactory + * @param GetAssetsByPathsInterface $getMediaGalleryAssetByPath + * @param CreateAssetFromFileInterface $createAssetFromFile + */ + public function __construct( + AssetInterfaceFactory $assetFactory, + GetAssetsByPathsInterface $getMediaGalleryAssetByPath, + CreateAssetFromFileInterface $createAssetFromFile + ) { + $this->assetFactory = $assetFactory; + $this->getAssetsByPaths = $getMediaGalleryAssetByPath; + $this->createAssetFromFile = $createAssetFromFile; + } + + /** + * Create media asset object based on the file information + * + * @param string $path + * @return AssetInterface + * @throws LocalizedException + * @throws ValidatorException + */ + public function execute(string $path): AssetInterface + { + $asset = $this->getAsset($path); + $assetFromFile = $this->createAssetFromFile->execute($path); + + if (!$asset) { + return $assetFromFile; + } + + return $this->assetFactory->create( + [ + 'id' => $asset->getId(), + 'path' => $path, + 'title' => $asset->getTitle(), + 'description' => $asset->getDescription() ?? $assetFromFile->getDescription(), + 'width' => $assetFromFile->getWidth(), + 'height' => $assetFromFile->getHeight(), + 'hash' => $assetFromFile->getHash(), + 'size' => $assetFromFile->getSize(), + 'contentType' => $asset->getContentType(), + 'source' => $asset->getSource() + ] + ); + } + + /** + * Returns asset if asset already exist by provided path + * + * @param string $path + * @return AssetInterface|null + * @throws ValidatorException + * @throws LocalizedException + */ + private function getAsset(string $path): ?AssetInterface + { + $asset = $this->getAssetsByPaths->execute([$path]); + return !empty($asset) ? $asset[0] : null; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetsIterator.php b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetsIterator.php new file mode 100644 index 0000000000000..6c0592c49f09c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetsIterator.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +/** + * Retrieve media storage assets iterator + */ +class GetAssetsIterator +{ + /** + * Get media storage assets 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/MediaGallerySynchronization/Model/GetContentHash.php b/app/code/Magento/MediaGallerySynchronization/Model/GetContentHash.php new file mode 100644 index 0000000000000..703fd56c4f0b8 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetContentHash.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +/** + * Get hashed value of image content. + */ +class GetContentHash +{ + /** + * Return the hash value of the given filepath. + * + * @param string $content + * @return string + */ + public function execute(string $content): string + { + return sha1($content); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/ImportMediaAsset.php b/app/code/Magento/MediaGallerySynchronization/Model/ImportMediaAsset.php new file mode 100644 index 0000000000000..3cac99f816d12 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/ImportMediaAsset.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; + +/** + * Import image file to the media gallery asset table + */ +class ImportMediaAsset implements ImportFilesInterface +{ + /** + * @var SaveAssetsInterface + */ + private $saveAssets; + + /** + * @var GetAssetFromPath + */ + private $getAssetFromPath; + + /** + * @param SaveAssetsInterface $saveAssets + * @param GetAssetFromPath $getAssetFromPath + */ + public function __construct( + SaveAssetsInterface $saveAssets, + GetAssetFromPath $getAssetFromPath + ) { + $this->saveAssets = $saveAssets; + $this->getAssetFromPath = $getAssetFromPath; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + $assets = []; + + foreach ($paths as $path) { + $assets[] = $this->getAssetFromPath->execute($path); + } + + $this->saveAssets->execute($assets); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php new file mode 100644 index 0000000000000..ec314416e36ee --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\MessageQueue\PublisherInterface; + +/** + * Publish media gallery synchronization queue. + */ +class Publish +{ + /** + * Media gallery synchronization queue topic name. + */ + private const TOPIC_MEDIA_GALLERY_SYNCHRONIZATION = 'media.gallery.synchronization'; + + /** + * @var PublisherInterface + */ + private $publisher; + + /** + * @param PublisherInterface $publisher + */ + public function __construct(PublisherInterface $publisher) + { + $this->publisher = $publisher; + } + + /** + * Publish media content synchronization message to the message queue + * + * @param array $paths + */ + public function execute(array $paths = []) : void + { + $this->publisher->publish( + self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION, + $paths + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/ResolveNonExistedAssets.php b/app/code/Magento/MediaGallerySynchronization/Model/ResolveNonExistedAssets.php new file mode 100644 index 0000000000000..d70547a7528e0 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/ResolveNonExistedAssets.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; +use Psr\Log\LoggerInterface; + +/** + * Delete assets which not exist physically + */ +class ResolveNonExistedAssets +{ + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + private const MEDIA_GALLERY_ASSET_PATH = 'path'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var DeleteAssetsByPathsInterface + */ + private $deleteAssetsByPaths; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var Read + */ + private $mediaDirectory; + + /** + * @var FetchBatchesInterface + */ + private $selectBatches; + + /** + * @param Filesystem $filesystem + * @param ResourceConnection $resourceConnection + * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths + * @param LoggerInterface $logger + * @param FetchBatchesInterface $selectBatches + */ + public function __construct( + Filesystem $filesystem, + ResourceConnection $resourceConnection, + DeleteAssetsByPathsInterface $deleteAssetsByPaths, + LoggerInterface $logger, + FetchBatchesInterface $selectBatches + ) { + $this->filesystem = $filesystem; + $this->resourceConnection = $resourceConnection; + $this->deleteAssetsByPaths = $deleteAssetsByPaths; + $this->logger = $logger; + $this->selectBatches = $selectBatches; + } + + /** + * Delete assets which not existed + * + * @return void + */ + public function execute(): void + { + $columns = [self::MEDIA_GALLERY_ASSET_PATH]; + try { + foreach ($this->selectBatches->execute(self::TABLE_MEDIA_GALLERY_ASSET, $columns, null) as $batch) { + foreach ($batch as $item) { + if (!$this->getMediaDirectory()->isExist($item[self::MEDIA_GALLERY_ASSET_PATH])) { + $this->deleteAssetsByPaths->execute([$item[self::MEDIA_GALLERY_ASSET_PATH]]); + } + } + } + } catch (\Exception $exception) { + $this->logger->critical($exception); + } + } + + /** + * Retrieve media directory instance with read permissions + * + * @return Read + */ + private function getMediaDirectory(): Read + { + if (!$this->mediaDirectory) { + $this->mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } + return $this->mediaDirectory; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Synchronize.php b/app/code/Magento/MediaGallerySynchronization/Model/Synchronize.php new file mode 100644 index 0000000000000..4396ea6a77736 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Synchronize.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\SynchronizerPool; +use Psr\Log\LoggerInterface; + +/** + * Synchronize media storage and media assets database records + */ +class Synchronize implements SynchronizeInterface +{ + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var SynchronizerPool + */ + private $synchronizerPool; + + /** + * @var FetchMediaStorageFileBatches + */ + private $batchGenerator; + + /** + * @var ResolveNonExistedAssets + */ + private $resolveNonExistedAssets; + + /** + * @param ResolveNonExistedAssets $resolveNonExistedAssets + * @param LoggerInterface $log + * @param SynchronizerPool $synchronizerPool + * @param FetchMediaStorageFileBatches $batchGenerator + */ + public function __construct( + ResolveNonExistedAssets $resolveNonExistedAssets, + LoggerInterface $log, + SynchronizerPool $synchronizerPool, + FetchMediaStorageFileBatches $batchGenerator + ) { + $this->resolveNonExistedAssets = $resolveNonExistedAssets; + $this->log = $log; + $this->synchronizerPool = $synchronizerPool; + $this->batchGenerator = $batchGenerator; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $failed = []; + + foreach ($this->synchronizerPool->get() as $name => $synchronizer) { + if (!$synchronizer instanceof SynchronizeFilesInterface) { + $failed[] = $name; + continue; + } + foreach ($this->batchGenerator->execute() as $batch) { + try { + $synchronizer->execute($batch); + } catch (\Exception $exception) { + $this->log->critical($exception); + $failed[] = $name; + } + } + } + + $this->resolveNonExistedAssets->execute(); + if (!empty($failed)) { + throw new LocalizedException( + __( + 'Failed to execute the following synchronizers: %synchronizers', + [ + 'synchronizers' => implode(', ', $failed) + ] + ) + ); + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php new file mode 100644 index 0000000000000..eebb172e48202 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGallerySynchronization\Model\Filesystem\GetFileInfo; +use Psr\Log\LoggerInterface; + +/** + * Synchronize files in media storage and media assets database records + */ +class SynchronizeFiles implements SynchronizeFilesInterface +{ + /** + * Date format + */ + private const DATE_FORMAT = 'Y-m-d H:i:s'; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @var File + */ + private $driver; + + /** + * @var GetFileInfo + */ + private $getFileInfo; + + /** + * @var ImportFilesInterface + */ + private $importFiles; + + /** + * @var DateTime + */ + private $date; + + /** + * @param File $driver + * @param Filesystem $filesystem + * @param DateTime $date + * @param LoggerInterface $log + * @param GetFileInfo $getFileInfo + * @param GetAssetsByPathsInterface $getAssetsByPaths + * @param ImportFilesInterface $importFiles + */ + public function __construct( + File $driver, + Filesystem $filesystem, + DateTime $date, + LoggerInterface $log, + GetFileInfo $getFileInfo, + GetAssetsByPathsInterface $getAssetsByPaths, + ImportFilesInterface $importFiles + ) { + $this->driver = $driver; + $this->filesystem = $filesystem; + $this->date = $date; + $this->log = $log; + $this->getFileInfo = $getFileInfo; + $this->getAssetsByPaths = $getAssetsByPaths; + $this->importFiles = $importFiles; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + try { + $this->importFiles->execute($this->getPathsToUpdate($paths)); + } catch (LocalizedException $localizedException) { + throw $localizedException; + } catch (\Exception $exception) { + $this->log->critical($exception); + throw new LocalizedException( + __( + 'Could not import media assets for files: %files', + [ + 'files' => implode(', ', $paths) + ] + ) + ); + } + } + + /** + * Return existing assets from files + * + * @param string[] $paths + * @return array + * @throws LocalizedException + */ + private function getPathsToUpdate(array $paths): array + { + $assetPaths = []; + + foreach ($paths as $path) { + $assetPath = $this->getAssetPath($path); + $assetPaths[$assetPath] = $assetPath; + } + + $assets = $this->getAssetsByPaths->execute($assetPaths); + + foreach ($assets as $asset) { + if ($asset->getUpdatedAt() === $this->getFileModificationTime($asset->getPath())) { + unset($assetPaths[$asset->getPath()]); + } + } + + return $assetPaths; + } + + /** + * Retrieve formatted file modification time + * + * @param string $path + * @return string + */ + private function getFileModificationTime(string $path): string + { + return $this->date->gmtDate( + self::DATE_FORMAT, + $this->getFileInfo->execute($this->getMediaDirectory()->getAbsolutePath($path))->getMTime() + ); + } + + /** + * Get correct path for media asset + * + * @param string $path + * @return string + */ + private function getAssetPath(string $path): string + { + return $this->driver->getParentDirectory($path) === '.' ? '/' . $path : $path; + } + + /** + * Retrieve media directory instance + * + * @return ReadInterface + */ + private function getMediaDirectory(): ReadInterface + { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Plugin/MediaGallerySyncTrigger.php b/app/code/Magento/MediaGallerySynchronization/Plugin/MediaGallerySyncTrigger.php new file mode 100644 index 0000000000000..9583c91184d1a --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Plugin/MediaGallerySyncTrigger.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Plugin; + +use Magento\Framework\App\Config\Value; +use Magento\MediaGallerySynchronization\Model\Publish; + +/** + * Plugin to synchronize media storage and media assets database records when media gallery enabled in configuration. + */ +class MediaGallerySyncTrigger +{ + private const MEDIA_GALLERY_CONFIG_VALUE = 'system/media_gallery/enabled'; + private const MEDIA_GALLERY_ENABLED_VALUE = 1; + + /** + * @var Publish + */ + private $publish; + + /** + * @param Publish $publish + */ + public function __construct(Publish $publish) + { + $this->publish = $publish; + } + + /** + * Update media gallery grid table when configuration is saved and media gallery enabled. + * + * @param Value $config + * @param Value $result + * @return Value + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(Value $config, Value $result): Value + { + if ($result->getPath() === self::MEDIA_GALLERY_CONFIG_VALUE + && $result->isValueChanged() + && (int) $result->getValue() === self::MEDIA_GALLERY_ENABLED_VALUE + ) { + $this->publish->execute(); + } + + return $result; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/README.md b/app/code/Magento/MediaGallerySynchronization/README.md new file mode 100644 index 0000000000000..4947c18986f3b --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/README.md @@ -0,0 +1,14 @@ +# Magento_MediaGallerySynchronization module + +The Magento_MediaGallerySynchronization module represents implementation of synchronization between data and objects contains +media asset information. + +## Extensibility + +Extension developers can interact with the Magento_MediaGallerySynchronization 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_MediaGallerySynchronization 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/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php new file mode 100644 index 0000000000000..6b1e8a676d02b --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Test\Integration\Model\Filesystem; + +use Magento\MediaGallerySynchronization\Model\Filesystem\GetFileInfo; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Integration test for GetFileInfo + */ +class GetFileInfoTest extends TestCase +{ + /** + * @var GetFileInfo + */ + private $getFileInfo; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->getFileInfo = Bootstrap::getObjectManager()->get(GetFileInfo::class); + } + + /** + * @dataProvider filesProvider + * @param string $file + */ + public function testExecute( + string $file + ): void { + + $path = $this->getImageFilePath($file); + + $fileInfo = $this->getFileInfo->execute($path); + $expectedResult = new \SplFileInfo($path); + $this->assertEquals($expectedResult->getPath(), $fileInfo->getPath()); + $this->assertEquals($expectedResult->getFilename(), $fileInfo->getFilename()); + $this->assertEquals($expectedResult->getExtension(), $fileInfo->getExtension()); + $this->assertEquals( + $expectedResult->getBasename('.' . $expectedResult->getExtension()), + $fileInfo->getBasename() + ); + $this->assertEquals($expectedResult->getSize(), $fileInfo->getSize()); + $this->assertEquals($expectedResult->getMTime(), $fileInfo->getMTime()); + $this->assertEquals($expectedResult->getCTime(), $fileInfo->getCTime()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'magento.jpg', + 'magento_2.jpg' + ] + ]; + } + + /** + * Return image file path + * + * @param string $filename + * @return string + */ + private function getImageFilePath(string $filename): string + { + return dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + $filename + ] + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/GetContentHashTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/GetContentHashTest.php new file mode 100644 index 0000000000000..a9c428fed7bca --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/GetContentHashTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Test\Integration\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGallerySynchronization\Model\GetContentHash; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for GetContentHash. + */ +class GetContentHashTest extends TestCase +{ + /** + * @var GetContentHash + */ + private $getContentHash; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->getContentHash = Bootstrap::getObjectManager()->get(GetContentHash::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + } + + /** + * Test for GetContentHash::execute + * + * @dataProvider filesProvider + * @param string $firstFile + * @param string $secondFile + * @param bool $isEqual + * @throws FileSystemException + */ + public function testExecute( + string $firstFile, + string $secondFile, + bool $isEqual + ): void { + $firstHash = $this->getContentHash->execute($this->getImageContent($firstFile)); + $secondHash = $this->getContentHash->execute($this->getImageContent($secondFile)); + $isEqual ? $this->assertEquals($firstHash, $secondHash) : $this->assertNotEquals($firstHash, $secondHash); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'magento.jpg', + 'magento_2.jpg', + true + ], + [ + 'magento.jpg', + 'magento_3.png', + false + ] + ]; + } + + /** + * Get image file content. + * + * @param string $filename + * @return string + * @throws FileSystemException + */ + private function getImageContent(string $filename): string + { + return $this->driver->fileGetContents($this->getImageFilePath($filename)); + } + + /** + * Return image file path + * + * @param string $filename + * @return string + */ + private function getImageFilePath(string $filename): string + { + return dirname(__DIR__, 1) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + $filename + ] + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php new file mode 100644 index 0000000000000..8a44307298065 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\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\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +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; + + /** + * @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->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + } + + /** + * Test for SynchronizeFiles::execute + * + * @dataProvider filesProvider + * @param string $file + * @param string $title + * @param string $source + * @throws FileSystemException + * @throws LocalizedException + */ + public function testExecute( + string $file, + string $title, + string $source + ): void { + $path = realpath(__DIR__ . '/../_files/' . $file); + $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($file); + $this->driver->copy( + $path, + $modifiableFilePath + ); + + $this->synchronizeFiles->execute([$file]); + + $loadedAsset = $this->getAssetsByPath->execute([$file])[0]; + + $this->assertEquals($title, $loadedAsset->getTitle()); + $this->assertEquals($source, $loadedAsset->getSource()); + + $this->driver->deleteFile($modifiableFilePath); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + '/magento.jpg', + 'magento', + 'Local' + ] + ]; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento.jpg b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento.jpg new file mode 100644 index 0000000000000..c377daf8fb0b3 Binary files /dev/null and b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento.jpg differ diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_2.jpg b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_2.jpg new file mode 100644 index 0000000000000..c377daf8fb0b3 Binary files /dev/null and b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_2.jpg differ diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_3.png b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_3.png new file mode 100644 index 0000000000000..366b1b8b9c3f7 Binary files /dev/null and b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_3.png differ diff --git a/app/code/Magento/MediaGallerySynchronization/composer.json b/app/code/Magento/MediaGallerySynchronization/composer.json new file mode 100644 index 0000000000000..f9d642dd02568 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-gallery-synchronization", + "description": "Magento module provides implementation of the media gallery data synchronization.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/framework-message-queue": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGallerySynchronization\\": "" + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/etc/communication.xml b/app/code/Magento/MediaGallerySynchronization/etc/communication.xml new file mode 100644 index 0000000000000..ba5ae5fc9f9bc --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/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.synchronization" is_synchronous="false" request="string[]"> + <handler name="media.gallery.synchronization.handler" + type="Magento\MediaGallerySynchronization\Model\Consume" method="execute"/> + </topic> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/di.xml b/app/code/Magento/MediaGallerySynchronization/etc/di.xml new file mode 100644 index 0000000000000..82bd1303eda74 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/di.xml @@ -0,0 +1,53 @@ +<?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\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="10" xsi:type="object">Magento\MediaGallerySynchronization\Model\ImportMediaAsset</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronizationApi\Model\SynchronizerPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_gallery_asset_synchronizer" xsi:type="object">Magento\MediaGallerySynchronization\Model\SynchronizeFiles</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronization\Model\FetchMediaStorageFileBatches"> + <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\MediaGallerySynchronization\Model\FetchBatches"> + <arguments> + <argument name="pageSize" xsi:type="number">100</argument> + </arguments> + </type> + <type name="Magento\Framework\Console\CommandListInterface"> + <arguments> + <argument name="commands" xsi:type="array"> + <item name="mediaGallerySynchronization" xsi:type="object">Magento\MediaGallerySynchronization\Console\Command\Synchronize</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\Config\Value"> + <plugin name="admin_system_config_adobe_stock_save_plugin" type="Magento\MediaGallerySynchronization\Plugin\MediaGallerySyncTrigger"/> + </type> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/module.xml b/app/code/Magento/MediaGallerySynchronization/etc/module.xml new file mode 100644 index 0000000000000..496f6aa0233a5 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/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_MediaGallerySynchronization" /> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/queue_consumer.xml b/app/code/Magento/MediaGallerySynchronization/etc/queue_consumer.xml new file mode 100644 index 0000000000000..4471d68fd8c47 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/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.synchronization" queue="media.gallery.synchronization" + connection="db" handler="Magento\MediaGallerySynchronization\Model\Consume::execute"/> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/queue_publisher.xml b/app/code/Magento/MediaGallerySynchronization/etc/queue_publisher.xml new file mode 100644 index 0000000000000..1a7cb04847c4a --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/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.synchronization"> + <connection name="db" exchange="magento-db" disabled="false" /> + </publisher> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/queue_topology.xml b/app/code/Magento/MediaGallerySynchronization/etc/queue_topology.xml new file mode 100644 index 0000000000000..81baefbfc53dc --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/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="MediaGallerySynchronization" topic="media.gallery.synchronization" + destinationType="queue" destination="media.gallery.synchronization"/> + </exchange> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/registration.php b/app/code/Magento/MediaGallerySynchronization/registration.php new file mode 100644 index 0000000000000..9e5f42b14c985 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/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_MediaGallerySynchronization', + __DIR__ +); diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeFilesInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeFilesInterface.php new file mode 100644 index 0000000000000..de5b00f99e059 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeFilesInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Api; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Synchronize assets from the provided files information to database + */ +interface SynchronizeFilesInterface +{ + /** + * Create media gallery assets based on files information and save them to database + * + * @param string[] $paths + * @throws LocalizedException + */ + public function execute(array $paths): void; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeInterface.php new file mode 100644 index 0000000000000..0b49780bd7590 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Api; + +/** + * Synchronize assets from the media storage to database + */ +interface SynchronizeInterface +{ + /** + * Synchronize assets from the media storage to database + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(): void; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/LICENSE.txt b/app/code/Magento/MediaGallerySynchronizationApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/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/MediaGallerySynchronizationApi/LICENSE_AFL.txt b/app/code/Magento/MediaGallerySynchronizationApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/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/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/Model/FetchBatchesInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/FetchBatchesInterface.php new file mode 100644 index 0000000000000..42cd8265d5087 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/FetchBatchesInterface.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; + +/** + * Fetch data from database in batches + */ +interface FetchBatchesInterface +{ + /** + * Fetch the columns from the database table in batches + * $modificationDateColumn contains the entities which were changed since last execution + * to avoid fetching items that have been previously synchronized + * + * @param string $tableName + * @param array $columns + * @param string|null $modificationDateColumn + * @return \Traversable + */ + public function execute(string $tableName, array $columns, ?string $modificationDateColumn): \Traversable; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesComposite.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesComposite.php new file mode 100644 index 0000000000000..8e5df842d8a55 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesComposite.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +/** + * File save pool + */ +class ImportFilesComposite implements ImportFilesInterface +{ + /** + * @var ImportFilesInterface[] + */ + private $importers; + + /** + * @param ImportFilesInterface[] $importers + */ + public function __construct(array $importers) + { + ksort($importers); + $this->importers = $importers; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + foreach ($this->importers as $importer) { + $importer->execute($paths); + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesInterface.php new file mode 100644 index 0000000000000..40e5947d3a11d --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesInterface.php @@ -0,0 +1,25 @@ +<?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\LocalizedException; + +/** + * Save media files data + */ +interface ImportFilesInterface +{ + /** + * Save media files data + * + * @param string[] $paths + * @throws LocalizedException + */ + public function execute(array $paths): void; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/SynchronizerPool.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/SynchronizerPool.php new file mode 100644 index 0000000000000..1294a4f7679f1 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/SynchronizerPool.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; + +/** + * A pool of Media storage to database synchronizers + * @see SynchronizeFilesInterface + */ +class SynchronizerPool +{ + /** + * Media storage to database synchronizers + * + * @var SynchronizeFilesInterface[] + */ + private $synchronizers; + + /** + * @param SynchronizeFilesInterface[] $synchronizers + */ + public function __construct(array $synchronizers = []) + { + foreach ($synchronizers as $name => $synchronizer) { + if (!$synchronizer instanceof SynchronizeFilesInterface) { + throw new \InvalidArgumentException(sprintf( + 'Synchronizer %s must implement %s.', + $name, + SynchronizeFilesInterface::class + )); + } + } + $this->synchronizers = $synchronizers; + } + + /** + * Get all synchronizers from the pool + * + * @return SynchronizeFilesInterface[] + */ + public function get(): array + { + return $this->synchronizers; + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/README.md b/app/code/Magento/MediaGallerySynchronizationApi/README.md new file mode 100644 index 0000000000000..1a12883413920 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGallerySynchronizationApi module + +The Magento_MediaGallerySynchronizationApi module is responsible for the media gallery data synchronization implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaGallerySynchronizationApi 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_MediaGallerySynchronizationApi 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/MediaGallerySynchronizationApi/composer.json b/app/code/Magento/MediaGallerySynchronizationApi/composer.json new file mode 100644 index 0000000000000..19bab75dd5f42 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-media-gallery-synchronization-api", + "description": "Magento module responsible for the media gallery synchronization implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGallerySynchronizationApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/etc/di.xml b/app/code/Magento/MediaGallerySynchronizationApi/etc/di.xml new file mode 100644 index 0000000000000..5cf3b424539bd --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/etc/di.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:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface" type="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"/> +</config> diff --git a/app/code/Magento/MediaGallerySynchronizationApi/etc/module.xml b/app/code/Magento/MediaGallerySynchronizationApi/etc/module.xml new file mode 100644 index 0000000000000..48736124400c9 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/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_MediaGallerySynchronizationApi" /> +</config> diff --git a/app/code/Magento/MediaGallerySynchronizationApi/registration.php b/app/code/Magento/MediaGallerySynchronizationApi/registration.php new file mode 100644 index 0000000000000..542a46d02dd33 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/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_MediaGallerySynchronizationApi', + __DIR__ +); 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/MediaGallerySynchronizationMetadata/Model/ImportKeywords.php b/app/code/Magento/MediaGallerySynchronizationMetadata/Model/ImportKeywords.php new file mode 100644 index 0000000000000..a9910157f27c7 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/Model/ImportKeywords.php @@ -0,0 +1,144 @@ +<?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\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; + +/** + * import image keywords from file metadata + */ +class ImportKeywords implements ImportFilesInterface +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var KeywordInterfaceFactory + */ + private $keywordFactory; + + /** + * @var AssetKeywordsInterfaceFactory + */ + private $assetKeywordsFactory; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @var SaveAssetsKeywordsInterface + */ + private $saveAssetKeywords; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @param Filesystem $filesystem + * @param KeywordInterfaceFactory $keywordFactory + * @param ExtractMetadataInterface $extractMetadata + * @param SaveAssetsKeywordsInterface $saveAssetKeywords + * @param AssetKeywordsInterfaceFactory $assetKeywordsFactory + * @param GetAssetsByPathsInterface $getAssetsByPaths + */ + public function __construct( + Filesystem $filesystem, + KeywordInterfaceFactory $keywordFactory, + ExtractMetadataInterface $extractMetadata, + SaveAssetsKeywordsInterface $saveAssetKeywords, + AssetKeywordsInterfaceFactory $assetKeywordsFactory, + GetAssetsByPathsInterface $getAssetsByPaths + ) { + $this->filesystem = $filesystem; + $this->keywordFactory = $keywordFactory; + $this->extractMetadata = $extractMetadata; + $this->saveAssetKeywords = $saveAssetKeywords; + $this->assetKeywordsFactory = $assetKeywordsFactory; + $this->getAssetsByPaths = $getAssetsByPaths; + } + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + $keywords = []; + + foreach ($paths as $path) { + $metadataKeywords = $this->getMetadataKeywords($path); + if ($metadataKeywords !== null) { + $keywords[$path] = $metadataKeywords; + } + } + + $assets = $this->getAssetsByPaths->execute(array_keys($keywords)); + + $assetKeywords = []; + + foreach ($assets as $asset) { + $assetKeywords[] = $this->assetKeywordsFactory->create([ + 'assetId' => $asset->getId(), + 'keywords' => $keywords[$asset->getPath()] + ]); + } + + $this->saveAssetKeywords->execute($assetKeywords); + } + + /** + * Get keywords from file metadata + * + * @param string $path + * @return KeywordInterface[]|null + */ + private function getMetadataKeywords(string $path): ?array + { + $metadataKeywords = $this->extractMetadata->execute($this->getMediaDirectory()->getAbsolutePath($path)) + ->getKeywords(); + + if ($metadataKeywords === null) { + return null; + } + + $keywords = []; + + foreach ($metadataKeywords as $keyword) { + $keywords[] = $this->keywordFactory->create( + [ + 'keyword' => $keyword + ] + ); + } + + return $keywords; + } + + /** + * Retrieve media directory instance with read access + * + * @return ReadInterface + */ + private function getMediaDirectory(): ReadInterface + { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } +} 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/Asset/Search.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php new file mode 100644 index 0000000000000..9b6c08edbc86d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Asset; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\SearchAssetsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller getting the asset options for multiselect filter + */ +class Search extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var SearchAssetsInterface + */ + private $searchAssets; + + /** + * @param SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var Images + */ + private $images; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var Storage + */ + private $storage; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @param FilterBuilder $filterBuilder + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param FilterGroupBuilder $filterGroupBuilder + * @param SearchAssetsInterface $searchAssets + * @param Context $context + * @param LoggerInterface $logger + * @param Images $images + * @param Storage $storage + */ + public function __construct( + FilterBuilder $filterBuilder, + SearchCriteriaBuilder $searchCriteriaBuilder, + FilterGroupBuilder $filterGroupBuilder, + SearchAssetsInterface $searchAssets, + Context $context, + LoggerInterface $logger, + Images $images, + Storage $storage + ) { + parent::__construct($context); + + $this->filterBuilder = $filterBuilder; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->logger = $logger; + $this->searchAssets = $searchAssets; + $this->images = $images; + $this->storage = $storage; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $searchKey = $this->getRequest()->getParam('searchKey'); + $limit = $this->getRequest()->getParam('limit'); + $pageNum = $this->getRequest()->getParam('page'); + $responseContent = []; + + if (!$searchKey) { + return $resultJson->setData([ + 'options' => [], + 'total' => 0 + ]); + } + + try { + $titleFilter = $this->filterBuilder->setField('title') + ->setConditionType('fulltext') + ->setValue($searchKey) + ->create(); + $searchCriteria = $this->searchCriteriaBuilder + ->setFilterGroups([$this->filterGroupBuilder->setFilters([$titleFilter])->create()]) + ->setPageSize($limit) + ->setCurrentPage($pageNum < 2 ? 0 : $pageNum) + ->create(); + + $assets = $this->searchAssets->execute($searchCriteria); + + if (!empty($assets)) { + foreach ($assets as $asset) { + $responseContent['options'][] = [ + 'value' => (string) $asset->getId(), + 'label' => $asset->getTitle(), + 'src' => $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()) + ]; + $responseContent['total'] = count($responseContent['options']); + } + } + + $responseCode = self::HTTP_OK; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to get image details.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php new file mode 100644 index 0000000000000..76c00927b33e0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller to create the folders + */ +class Create extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::create_folder'; + + /** + * @var CreateDirectoriesByPathsInterface + */ + private $createDirectoriesByPaths; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param CreateDirectoriesByPathsInterface $createDirectoriesByPaths + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + CreateDirectoriesByPathsInterface $createDirectoriesByPaths, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->createDirectoriesByPaths = $createDirectoriesByPaths; + $this->logger = $logger; + } + + /** + * Create folder by provided path. + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $paths = $this->getRequest()->getParam('paths'); + + if (!$paths) { + $responseContent = [ + 'success' => false, + 'message' => __('Folder paths parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->createDirectoriesByPaths->execute($paths); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully created the folder.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to create folder.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php new file mode 100644 index 0000000000000..3dc43e5276860 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller deleting the folders + */ +class Delete extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::delete_folder'; + + /** + * @var DeleteAssetsByPathsInterface + */ + private $deleteAssetsByPaths; + + /** + * @var DeleteDirectoriesByPathsInterface + */ + private $deleteDirectoriesByPaths; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths + * @param DeleteDirectoriesByPathsInterface $deleteDirectoriesByPaths + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + DeleteAssetsByPathsInterface $deleteAssetsByPaths, + DeleteDirectoriesByPathsInterface $deleteDirectoriesByPaths, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->deleteAssetsByPaths = $deleteAssetsByPaths; + $this->deleteDirectoriesByPaths = $deleteDirectoriesByPaths; + $this->logger = $logger; + } + + /** + * Delete folder by provided path. + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $path = $this->getRequest()->getParam('path'); + + if (!$path) { + $responseContent = [ + 'success' => false, + 'message' => __('Folder path parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->deleteDirectoriesByPaths->execute([$path]); + $this->deleteAssetsByPaths->execute([$path]); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully removed the folder.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to remove folder.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php new file mode 100644 index 0000000000000..d4885cae055dd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\MediaGalleryUi\Model\Directories\GetFolderTree; +use Psr\Log\LoggerInterface; + +/** + * Returns all available directories + */ +class GetTree extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var GetFolderTree + */ + private $getFolderTree; + + /** + * Constructor + * + * @param Action\Context $context + * @param LoggerInterface $logger + * @param GetFolderTree $getFolderTree + */ + public function __construct( + Action\Context $context, + LoggerInterface $logger, + GetFolderTree $getFolderTree + ) { + parent::__construct($context); + $this->logger = $logger; + $this->getFolderTree = $getFolderTree; + } + /** + * @inheritdoc + */ + public function execute() + { + try { + $responseContent[] = $this->getFolderTree->execute(); + $responseCode = self::HTTP_OK; + } catch (\Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('Retrieving directories list failed.'), + ]; + } + + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php new file mode 100644 index 0000000000000..2f7766c590033 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryUi\Model\DeleteImage; +use Psr\Log\LoggerInterface; + +/** + * Controller deleting the media gallery content + */ +class Delete extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::delete_assets'; + + /** + * @var DeleteImage + */ + private $deleteImage; + + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsByIds; + + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Delete constructor. + * + * @param Context $context + * @param DeleteImage $deleteImage + * @param GetAssetsByIdsInterface $getAssetsByIds + * @param Storage $imagesStorage + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + DeleteImage $deleteImage, + GetAssetsByIdsInterface $getAssetsByIds, + Storage $imagesStorage, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->deleteImage = $deleteImage; + $this->getAssetsByIds = $getAssetsByIds; + $this->imagesStorage = $imagesStorage; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $imageIds = $this->getRequest()->getParam('ids'); + + if (empty($imageIds) || !is_array($imageIds)) { + $responseContent = [ + 'success' => false, + 'message' => __('Image Ids are required and must be of type array.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $assets = $this->getAssetsByIds->execute($imageIds); + $this->deleteImage->execute($assets); + $responseCode = self::HTTP_OK; + if (count($imageIds) === 1) { + $message = __( + 'The asset "%title" has been successfully deleted.', + [ + 'title' => current($assets)->getTitle() + ] + ); + } else { + $message = __( + '%count assets have been successfully deleted.', + [ + 'count' => count($imageIds) + ] + ); + } + $responseContent = [ + 'success' => true, + 'message' => $message, + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to delete image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php new file mode 100644 index 0000000000000..d959a070148ed --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryUi\Model\GetDetailsByAssetId; +use Psr\Log\LoggerInterface; + +/** + * Controller getting the media gallery image details + */ +class Details extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var GetDetailsByAssetId + */ + private $getDetailsByAssetId; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Details constructor. + * + * @param Context $context + * @param GetDetailsByAssetId $getDetailsByAssetId + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + GetDetailsByAssetId $getDetailsByAssetId, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->logger = $logger; + $this->getDetailsByAssetId = $getDetailsByAssetId; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $ids = $this->getRequest()->getParam('ids'); + + if (empty($ids) || !is_array($ids)) { + $responseContent = [ + 'success' => false, + 'message' => __('Assets Ids is required, and must be of type array.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $details = $this->getDetailsByAssetId->execute($ids); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'imageDetails' => $details + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to get image details.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/OnInsert.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/OnInsert.php new file mode 100644 index 0000000000000..b92724f64148e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/OnInsert.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\MediaGalleryUi\Model\InsertImageData\GetInsertImageData; + +/** + * OnInsert action returns on insert image details + */ +class OnInsert extends Action implements HttpPostActionInterface +{ + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::insert_assets'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var GetInsertImageData + */ + private $getInsertImageData; + + /** + * @param Context $context + * @param JsonFactory $resultJsonFactory + * @param GetInsertImageData $getInsertImageData + */ + public function __construct( + Context $context, + JsonFactory $resultJsonFactory, + GetInsertImageData $getInsertImageData + ) { + parent::__construct($context); + $this->resultJsonFactory = $resultJsonFactory; + $this->getInsertImageData = $getInsertImageData; + } + + /** + * Return a content (just a link or an html block) for inserting image to the content + * + * @return ResultInterface + */ + public function execute() + { + $data = $this->getRequest()->getParams(); + $insertImageData = $this->getInsertImageData->execute( + $data['filename'], + (bool)$data['force_static_path'], + (bool)$data['as_is'], + isset($data['store']) ? (int)$data['store'] : null + ); + + return $this->resultJsonFactory->create()->setData([ + 'content' => $insertImageData->getContent(), + 'size' => $insertImageData->getSize(), + 'type' => $insertImageData->getType(), + ]); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php new file mode 100644 index 0000000000000..87a2e7345c407 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryUi\Model\UpdateAsset; +use Psr\Log\LoggerInterface; + +class SaveDetails extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::edit_assets'; + + /** + * @var UpdateAsset + */ + private $updateAsset; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param MetadataInterfaceFactory $metadataFactory + * @param UpdateAsset $updateAsset + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + MetadataInterfaceFactory $metadataFactory, + UpdateAsset $updateAsset, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->metadataFactory = $metadataFactory; + $this->updateAsset = $updateAsset; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $assetId = (int) $this->getRequest()->getParam('id'); + $title = $this->getRequest()->getParam('title'); + $description = $this->getRequest()->getParam('description'); + $keywords = (array) $this->getRequest()->getParam('keywords'); + + if ($assetId === 0) { + $responseContent = [ + 'success' => false, + 'message' => __('Image ID is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->updateAsset->execute( + $assetId, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully saved the image "%image"', ['image' => $title]), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to save image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php new file mode 100644 index 0000000000000..4492595bbe6ee --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryUi\Model\UploadImage; +use Psr\Log\LoggerInterface; + +/** + * Controller responsible to upload the media gallery content + */ +class Upload extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::upload_assets'; + + /** + * @var UploadImage + */ + private $uploadImage; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param UploadImage $upload + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + UploadImage $upload, + LoggerInterface $logger + ) { + parent::__construct($context); + $this->uploadImage = $upload; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $targetFolder = $this->getRequest()->getParam('target_folder'); + $type = $this->getRequest()->getParam('type'); + + if (!$targetFolder) { + $responseContent = [ + 'success' => false, + 'message' => __('The target_folder parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->uploadImage->execute($targetFolder, $type); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('The image was uploaded successfully.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => false, + 'message' => __('Could not upload image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php new file mode 100644 index 0000000000000..e97d93d86bb0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Index; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\View\Result\Layout; +use Magento\Framework\View\Result\LayoutFactory; + +/** + * Controller serving the media gallery content + */ +class Index extends Action implements HttpGetActionInterface +{ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var LayoutFactory + */ + private $layoutFactory; + + /** + * Index constructor. + * + * @param Context $context + * @param LayoutFactory $layoutFactory + */ + public function __construct( + Context $context, + LayoutFactory $layoutFactory + ) { + parent::__construct($context); + $this->layoutFactory = $layoutFactory; + } + + /** + * Get the media gallery layout + * + * @return Layout + */ + public function execute(): Layout + { + return $this->layoutFactory->create(); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php new file mode 100644 index 0000000000000..8c5b3d4d3a9ac --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Media; + +use Magento\Backend\App\Action; +use Magento\Backend\Model\View\Result\Page; +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 + */ +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 + * + * @return ResultInterface + */ + 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') + ->addBreadcrumb(__('Media'), __('Media Gallery')); + $resultPage->getConfig()->getTitle()->prepend(__('Manage Gallery')); + + return $resultPage; + } +} diff --git a/app/code/Magento/MediaGalleryUi/LICENSE.txt b/app/code/Magento/MediaGalleryUi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/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/MediaGalleryUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/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/MediaGalleryUi/Model/Config.php b/app/code/Magento/MediaGalleryUi/Model/Config.php new file mode 100644 index 0000000000000..a9391d76428ca --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Config.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; + +/** + * Class responsible to provide access to system configuration related to the Media Gallery + */ +class Config implements ConfigInterface +{ + /** + * Path to enable/disable media gallery in the system settings. + */ + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Config constructor. + * + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check if masonry grid UI is enabled for Magento media gallery + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Config/MediaGallery/Yesno.php b/app/code/Magento/MediaGalleryUi/Model/Config/MediaGallery/Yesno.php new file mode 100644 index 0000000000000..40cf7630d9911 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Config/MediaGallery/Yesno.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\Config\MediaGallery; + +class Yesno implements \Magento\Framework\Data\OptionSourceInterface +{ + /** + * Options getter + * + * @return array + */ + public function toOptionArray() :array + { + return [['value' => 0, 'label' => __('Yes')], ['value' => 1, 'label' => __('No')]]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php b/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php new file mode 100644 index 0000000000000..2f4793c28ad47 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; + +/** + * Delete image from a storage + */ +class DeleteImage +{ + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * DeleteImage constructor. + * + * @param Storage $imagesStorage + * @param Filesystem $filesystem + * @param IsPathExcludedInterface $isPathExcluded + */ + public function __construct( + Storage $imagesStorage, + Filesystem $filesystem, + IsPathExcludedInterface $isPathExcluded + ) { + $this->imagesStorage = $imagesStorage; + $this->filesystem = $filesystem; + $this->isPathExcluded = $isPathExcluded; + } + + /** + * Delete asset image physically from file storage and from data storage. + * + * @param AssetInterface[] $assets + * @throws LocalizedException + */ + public function execute(array $assets): void + { + $failedAssets = []; + foreach ($assets as $asset) { + if ($this->isPathExcluded->execute($asset->getPath())) { + $failedAssets[] = $asset->getPath(); + } + + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + $absolutePath = $mediaDirectory->getAbsolutePath($asset->getPath()); + $this->imagesStorage->deleteFile($absolutePath); + } + if (!empty($failedAssets)) { + throw new LocalizedException( + __( + 'Could not delete "%image": destination directory is restricted.', + ['image' => implode(",", $failedAssets)] + ) + ); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php new file mode 100644 index 0000000000000..c22165ba4e51f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +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 media gallery folder tree structure by path + */ +class GetFolderTree +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @param Filesystem $filesystem + * @param IsPathExcludedInterface $isPathExcluded + */ + public function __construct( + Filesystem $filesystem, + IsPathExcludedInterface $isPathExcluded + ) { + $this->filesystem = $filesystem; + $this->isPathExcluded = $isPathExcluded; + } + + /** + * Return directory folder structure in array + * + * @return array + * @throws ValidatorException + */ + public function execute(): array + { + $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']; + } + + /** + * Build directory tree array in format for jstree strandart + * + * @return array + * @throws ValidatorException + */ + private function getDirectories(): array + { + $directories = []; + + /** @var Read $directory */ + $directory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + + if (!$directory->isDirectory()) { + return $directories; + } + + foreach ($directory->readRecursively() as $path) { + if (!$directory->isDirectory($path) || $this->isPathExcluded->execute($path)) { + continue; + } + + $pathArray = explode('/', $path); + $directories[] = [ + 'data' => count($pathArray) > 0 ? end($pathArray) : $path, + 'attr' => ['id' => $path], + 'metadata' => [ + 'path' => $path + ], + 'path_array' => $pathArray + ]; + } + return $directories; + } + + /** + * Find parent directory + * + * @param array $node + * @param array $treeNode + * @param int $level + * @return array + */ + private function findParent(array &$node, array &$treeNode, int $level = 0): array + { + $nodePathLength = count($node['path_array']); + $treeNodeParentLevel = $nodePathLength - 1; + + $result = ['treeNode' => &$treeNode]; + + if ($nodePathLength <= 1 || $level > $treeNodeParentLevel) { + return $result; + } + + foreach ($treeNode['children'] as &$tnode) { + if ($node['path_array'][$level] === $tnode['path_array'][$level]) { + return $this->findParent($node, $tnode, $level + 1); + } + } + return $result; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php b/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php new file mode 100644 index 0000000000000..88bd5cf96e534 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Provides asset detail for view details section + */ +class GetAssetDetails +{ + /** + * @var TimezoneInterface + */ + private $dateTime; + + /** + * @var GetAssetUsageDetails + */ + private $getAssetUsageDetails; + + /** + * @param GetAssetUsageDetails $getAssetUsageDetails + * @param TimezoneInterface $dateTime + */ + public function __construct( + GetAssetUsageDetails $getAssetUsageDetails, + TimezoneInterface $dateTime + ) { + $this->dateTime = $dateTime; + $this->getAssetUsageDetails = $getAssetUsageDetails; + } + + /** + * Get a piece of asset details + * + * @param AssetInterface $asset + * @return array + */ + public function execute(AssetInterface $asset): array + { + $details = [ + [ + 'title' => __('Type'), + 'value' => __('Image'), + ], + [ + 'title' => __('Created'), + 'value' => $this->formatDate($asset->getCreatedAt()) + ], + [ + 'title' => __('Modified'), + 'value' => $this->formatDate($asset->getUpdatedAt()) + ], + [ + 'title' => __('Width'), + 'value' => sprintf('%spx', $asset->getWidth()) + ], + [ + 'title' => __('Height'), + 'value' => sprintf('%spx', $asset->getHeight()) + ], + [ + 'title' => __('Size'), + 'value' => $this->formatSize($asset->getSize()) + ], + [ + 'title' => __('Used In'), + 'value' => $this->getAssetUsageDetails->execute($asset->getId()) + ] + ]; + return $details; + } + + /** + * Format image size + * + * @param int $size + * @return string + */ + private function formatSize(int $size): string + { + return $size === 0 ? '' : sprintf('%.2f KB', $size / 1024); + } + + /** + * Format date to standard format + * + * @param string $date + * @return string + */ + private function formatDate(string $date): string + { + return $this->dateTime->formatDate($date, \IntlDateFormatter::SHORT, true); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php b/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php new file mode 100644 index 0000000000000..1dd8b736a9c90 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; + +/** + * Provide information on which content asset is used in + */ +class GetAssetUsageDetails +{ + /** + * @var GetContentByAssetIdsInterface + */ + private $getContent; + + /** + * @var UrlInterface + */ + private $url; + + /** + * @var array + */ + private $contentTypes; + + /** + * @param GetContentByAssetIdsInterface $getContent + * @param UrlInterface $url + * @param array $contentTypes + */ + public function __construct( + GetContentByAssetIdsInterface $getContent, + UrlInterface $url, + array $contentTypes = [] + ) { + $this->getContent = $getContent; + $this->url = $url; + $this->contentTypes = $contentTypes; + } + + /** + * Provide information on which content asset is used in + * + * @param int $id + * @return array + * @throws IntegrationException + */ + public function execute(int $id): array + { + $details = []; + + foreach ($this->getUsageByEntities($id) as $type => $entities) { + $details[] = [ + 'name' => $this->getName($type), + 'number' => count($entities), + 'link' => $this->getLinkUrl($type) + ]; + } + + return $details; + } + + /** + * Retrieve the type name from content types configuration + * + * @param string $type + * @return string + */ + private function getName(string $type): string + { + if (isset($this->contentTypes[$type]) && !empty($this->contentTypes[$type]['name'])) { + return $this->contentTypes[$type]['name']; + } + return $type; + } + + /** + * Retrieve the type link from content types configuration + * + * @param string $type + * @return string|null + */ + private function getLinkUrl(string $type): ?string + { + if (isset($this->contentTypes[$type]) && !empty($this->contentTypes[$type]['link'])) { + return $this->url->getUrl($this->contentTypes[$type]['link']); + } + return null; + } + + /** + * Get used in counts per type + * + * @param int $assetId + * @return int[] + * @throws IntegrationException + */ + private function getUsageByEntities(int $assetId): array + { + $usage = []; + + foreach ($this->getContent->execute([$assetId]) as $contentIdentity) { + $id = $contentIdentity->getEntityId(); + $type = $contentIdentity->getEntityType(); + $usage[$type][$id] = isset($usage[$type][$id]) ? $usage[$type][$id]++ : 0; + } + + return $usage; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php b/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php new file mode 100644 index 0000000000000..f6972637b3610 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Exception; +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Load Media Asset from database by id add all related data to it + */ +class GetDetailsByAssetId +{ + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsById; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var SourceIconProvider + */ + private $sourceIconProvider; + + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @var GetAssetDetails + */ + private $getAssetDetails; + + /** + * @param GetAssetDetails $getAssetDetails + * @param GetAssetsByIdsInterface $getAssetById + * @param StoreManagerInterface $storeManager + * @param SourceIconProvider $sourceIconProvider + * @param GetAssetsKeywordsInterface $getAssetKeywords + */ + public function __construct( + GetAssetDetails $getAssetDetails, + GetAssetsByIdsInterface $getAssetById, + StoreManagerInterface $storeManager, + SourceIconProvider $sourceIconProvider, + GetAssetsKeywordsInterface $getAssetKeywords + ) { + $this->getAssetDetails = $getAssetDetails; + $this->getAssetsById = $getAssetById; + $this->storeManager = $storeManager; + $this->sourceIconProvider = $sourceIconProvider; + $this->getAssetKeywords = $getAssetKeywords; + } + + /** + * Get image details by assets Ids + * + * @param array $assetIds + * @throws LocalizedException + * @throws Exception + * @return array + */ + public function execute(array $assetIds): array + { + $assets = $this->getAssetsById->execute($assetIds); + + $details = []; + foreach ($assets as $asset) { + $details[$asset->getId()] = [ + 'image_url' => $this->getUrl($asset->getPath()), + 'title' => $asset->getTitle(), + 'path' => $asset->getPath(), + 'description' => $asset->getDescription(), + 'id' => $asset->getId(), + 'details' => $this->getAssetDetails->execute($asset), + 'size' => $asset->getSize(), + 'tags' => $this->getKeywords($asset), + 'source' => $asset->getSource() ? + $this->sourceIconProvider->getSourceIconUrl($asset->getSource()) : + null, + 'content_type' => strtoupper(str_replace('image/', '', $asset->getContentType())), + ]; + } + return $details; + } + + /** + * 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 + ); + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * + * @return string + * + * @throws LocalizedException + */ + private function getUrl(string $path): string + { + /** @var Store $store */ + $store = $this->storeManager->getStore(); + + return $store->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $path; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/InsertImageData.php b/app/code/Magento/MediaGalleryUi/Model/InsertImageData.php new file mode 100644 index 0000000000000..f70ed8e308c99 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/InsertImageData.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\MediaGalleryUi\Model\InsertImageDataExtensionInterface; + +/** + * Class responsible to provide insert image details + */ +class InsertImageData implements InsertImageDataInterface +{ + /** + * @var InsertImageDataExtensionInterface + */ + private $extensionAttributes; + + /** + * @var string + */ + private $content; + + /** + * @var int + */ + private $size; + + /** + * @var string + */ + private $type; + + /** + * InsertImageData constructor. + * + * @param string $content + * @param int $size + * @param string $type + */ + public function __construct(string $content, int $size, string $type) + { + $this->content = $content; + $this->size = $size; + $this->type = $type; + } + + /** + * Returns a content (just a link or an html block) for inserting image to the content + * + * @return string + */ + public function getContent(): string + { + return $this->content; + } + + /** + * Returns size of requested file + * + * @return int + */ + public function getSize(): int + { + return $this->size; + } + + /** + * Returns MIME type of requested file + * + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryUi\Model\InsertImageDataExtensionInterface|null + */ + public function getExtensionAttributes(): ?InsertImageDataExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryUi\Model\InsertImageDataExtensionInterface $extensionAttributes + * @return void + */ + public function setExtensionAttributes(InsertImageDataExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/InsertImageData/GetInsertImageData.php b/app/code/Magento/MediaGalleryUi/Model/InsertImageData/GetInsertImageData.php new file mode 100644 index 0000000000000..6f1d399784139 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/InsertImageData/GetInsertImageData.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\InsertImageData; + +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\GetInsertImageContent; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Mime; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\MediaGalleryUi\Model\InsertImageDataFactory; +use Magento\MediaGalleryUi\Model\InsertImageDataInterface; + +class GetInsertImageData +{ + /** + * @var ReadInterface + */ + private $mediaDirectory; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var GetInsertImageContent + */ + private $getInsertImageContent; + + /** + * @var InsertImageDataFactory + */ + private $insertImageDataFactory; + + /** + * @var Mime + */ + private $mime; + + /** + * @var Images + */ + private $imagesHelper; + + /** + * GetInsertImageData constructor. + * + * @param GetInsertImageContent $getInsertImageContent + * @param Filesystem $fileSystem + * @param Mime $mime + * @param InsertImageDataFactory $insertImageDataFactory + * @param Images $imagesHelper + */ + public function __construct( + GetInsertImageContent $getInsertImageContent, + Filesystem $fileSystem, + Mime $mime, + InsertImageDataFactory $insertImageDataFactory, + Images $imagesHelper + ) { + $this->getInsertImageContent = $getInsertImageContent; + $this->filesystem = $fileSystem; + $this->mime = $mime; + $this->insertImageDataFactory = $insertImageDataFactory; + $this->imagesHelper = $imagesHelper; + } + + /** + * Returns image data object + * + * @param string $encodedFilename + * @param bool $forceStaticPath + * @param bool $renderAsTag + * @param int|null $storeId + * @return InsertImageDataInterface + */ + public function execute( + string $encodedFilename, + bool $forceStaticPath, + bool $renderAsTag, + ?int $storeId = null + ): InsertImageDataInterface { + $content = $this->getInsertImageContent->execute( + $encodedFilename, + $forceStaticPath, + $renderAsTag, + $storeId + ); + $relativePath = $this->getImageRelativePath($content); + $size = $forceStaticPath ? $this->getSize($relativePath) : 0; + $type = $forceStaticPath ? $this->getType($relativePath) : ''; + return $this->insertImageDataFactory->create([ + 'content' => $content, + 'size' => $size, + 'type' => $type + ]); + } + + /** + * Retrieve size of requested file + * + * @param string $path + * @return int + */ + private function getSize(string $path): int + { + $directory = $this->getMediaDirectory(); + + return $directory->isExist($path) ? $directory->stat($path)['size'] : 0; + } + + /** + * Retrieve MIME type of requested file + * + * @param string $path + * @return string + */ + public function getType(string $path): string + { + $fileExist = $this->getMediaDirectory()->isExist($path); + + return $fileExist ? $this->mime->getMimeType($this->getMediaDirectory()->getAbsolutePath($path)) : ''; + } + + /** + * Retrieve pub directory read interface instance + * + * @return ReadInterface + */ + private function getMediaDirectory(): ReadInterface + { + if ($this->mediaDirectory === null) { + $this->mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } + + return $this->mediaDirectory; + } + + /** + * Retrieves image relative path + * + * @param string $content + * @return string + */ + private function getImageRelativePath(string $content): string + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $mediaPath = parse_url($this->imagesHelper->getCurrentUrl(), PHP_URL_PATH); + return substr($content, strlen($mediaPath)); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/InsertImageDataInterface.php b/app/code/Magento/MediaGalleryUi/Model/InsertImageDataInterface.php new file mode 100644 index 0000000000000..063d76292d625 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/InsertImageDataInterface.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Class responsible to provide insert image details + */ +interface InsertImageDataInterface extends ExtensibleDataInterface +{ + /** + * Returns a content (just a link or an html block) for inserting image to the content + * + * @return null|string + */ + public function getContent(): ?string; + + /** + * Returns size of requested file + * + * @return int + */ + public function getSize(): int; + + /** + * Returns MIME type of requested file + * + * @return string + */ + public function getType(): string; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryUi\Model\InsertImageDataExtensionInterface|null + */ + public function getExtensionAttributes(): ?\Magento\MediaGalleryUi\Model\InsertImageDataExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryUi\Model\InsertImageDataExtensionInterface $extensionAttributes + * @return void + */ + public function setExtensionAttributes( + \Magento\MediaGalleryUi\Model\InsertImageDataExtensionInterface $extensionAttributes + ): void; +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php b/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php new file mode 100644 index 0000000000000..88401465d56b7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\Listing; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\ReportingInterface; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory; +use Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider as UiComponentDataProvider; +use Magento\MediaGalleryUi\Ui\Component\Listing\Provider; + +/** + * Media gallery UI data provider. Try catch added for displaying errors in grid + */ +class DataProvider extends UiComponentDataProvider +{ + /** + * @var CollectionProcessorInterface + */ + private $collectionProcessor; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param ReportingInterface $reporting + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param RequestInterface $request + * @param FilterBuilder $filterBuilder + * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionFactory $collectionFactory + * @param array $meta + * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + $name, + $primaryFieldName, + $requestFieldName, + ReportingInterface $reporting, + SearchCriteriaBuilder $searchCriteriaBuilder, + RequestInterface $request, + FilterBuilder $filterBuilder, + CollectionProcessorInterface $collectionProcessor, + CollectionFactory $collectionFactory, + array $meta = [], + array $data = [] + ) { + parent::__construct( + $name, + $primaryFieldName, + $requestFieldName, + $reporting, + $searchCriteriaBuilder, + $request, + $filterBuilder, + $meta, + $data + ); + $this->collectionFactory = $collectionFactory; + $this->collectionProcessor = $collectionProcessor; + } + + /** + * @inheritdoc + */ + public function getData(): array + { + try { + return $this->searchResultToOutput($this->getSearchResult()); + } catch (\Exception $exception) { + return [ + 'items' => [], + 'totalRecords' => 0, + 'errorMessage' => $exception->getMessage() + ]; + } + } + + /** + * @inheritDoc + */ + public function getSearchResult(): SearchResultInterface + { + /** @var Provider $collection */ + $collection = $this->collectionFactory->getReport($this->getSearchCriteria()->getRequestName()); + $this->collectionProcessor->process($this->getSearchCriteria(), $collection); + + return $collection; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php new file mode 100644 index 0000000000000..785c3078cdbe5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to filter a content field + */ +class ContentField implements CustomFilterInterface +{ + /** + * @var GetAssetIdsByContentFieldInterface + */ + private $getAssetIdsByContentStatus; + + /** + * ContentField constructor. + * + * @param GetAssetIdsByContentFieldInterface $getAssetIdsByContentStatus + */ + public function __construct( + GetAssetIdsByContentFieldInterface $getAssetIdsByContentStatus + ) { + $this->getAssetIdsByContentStatus = $getAssetIdsByContentStatus; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $collection->addFieldToFilter( + 'main_table.id', + ['in' => $this->getAssetIdsByContentStatus->execute($filter->getField(), $filter->getValue())] + ); + + return true; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php new file mode 100644 index 0000000000000..36e9375525f8d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\Data\Collection\AbstractDb; + +class Directory implements CustomFilterInterface +{ + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = str_replace('%', '', $filter->getValue()); + $collection->getSelect()->where('path REGEXP ? ', '^' . $value . '/[^\/]*$'); + + return true; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php new file mode 100644 index 0000000000000..d43b3ac2ca451 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\DB\Select; + +/** + * Custom filter to filter collection by duplicated hash values + */ +class Duplicated implements CustomFilterInterface +{ + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + if ($filter->getValue()) { + $collection->getSelect()->where('main_table.hash IN (?)', $this->getDuplicatedIds()); + } + return true; + } + /** + * Return sql part of duplicated values. + */ + private function getDuplicatedIds(): array + { + $connection = $this->connection->getConnection(); + return $connection->fetchAssoc( + $connection->select() + ->from($this->connection->getTableName('media_gallery_asset'), ['hash']) + ->group('hash') + ->having('COUNT(*) > 1') + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php new file mode 100644 index 0000000000000..6027a7daf7442 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; + +/** + * Custom filter to filter collection by entity type + */ +class Entity implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_MEDIA_CONTENT_ASSET = 'media_content_asset'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var string + */ + private $entityType; + + /** + * @param ResourceConnection $resource + * @param string $entityType + */ + public function __construct(ResourceConnection $resource, string $entityType) + { + $this->connection = $resource; + $this->entityType = $entityType; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $ids = $filter->getValue(); + if (is_array($ids)) { + $collection->addFieldToFilter( + [self::TABLE_ALIAS . '.id'], + [ + ['in' => $this->getSelectByEntityIds($ids)] + ] + ); + } + return true; + } + + /** + * Return asset ids by entity type + * + * @param array $ids + * @return array + */ + private function getSelectByEntityIds(array $ids): array + { + $connection = $this->connection->getConnection(); + + return $connection->fetchAssoc( + $connection->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + $this->entityType + )->where( + 'entity_id IN (?)', + $ids + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php new file mode 100644 index 0000000000000..1b5e2282ff3dc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; + +/** + * Custom filter to filter collection by entity type + */ +class EntityType implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_MEDIA_CONTENT_ASSET = 'media_content_asset'; + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + private const NOT_USED = 'not_used'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = $filter->getValue(); + if (is_array($value)) { + $conditions = []; + + if (in_array(self::NOT_USED, $value)) { + unset($value[array_search(self::NOT_USED, $value)]); + $conditions[] = ['in' => $this->getNotUsedEntityIds()]; + } + + if (!empty($value)) { + $conditions[] = ['in' => $this->getEntityTypesIds($value)]; + } + + $collection->addFieldToFilter( + self::TABLE_ALIAS . '.id', + $conditions + ); + } + return true; + } + + /** + * Return asset ids by entity type + * + * @param array $value + * @return array + */ + private function getEntityTypesIds(array $value): array + { + $connection = $this->connection->getConnection(); + return $connection->fetchAssoc( + $connection->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type IN (?)', + $value + ) + ); + } + + /** + * Return asset ids that not exists in asset_content_table + */ + private function getNotUsedEntityIds(): array + { + $connection = $this->connection->getConnection(); + + return $connection->fetchAssoc( + $connection->select()->from( + ['media_gallery_asset' => $this->connection->getTableName(self::TABLE_MEDIA_GALLERY_ASSET)], + ['id'] + )->where( + 'media_gallery_asset.id not in ?', + $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + ) + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php new file mode 100644 index 0000000000000..1c8baa58d90ea --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; + +class Keyword implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_KEYWORDS = 'media_gallery_asset_keyword'; + private const TABLE_ASSET_KEYWORD = 'media_gallery_keyword'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = $filter->getValue(); + + $collection->addFieldToFilter( + [self::TABLE_ALIAS . '.title', self::TABLE_ALIAS . '.id'], + [ + ['like' => sprintf('%%%s%%', $value)], + ['in' => $this->getAssetIdsByKeyword($value)] + ] + ); + + return true; + } + + /** + * Return asset ids by keyword + * + * @param string $value + * @return array + */ + private function getAssetIdsByKeyword(string $value): array + { + $connection = $this->connection->getConnection(); + + return $connection->fetchAssoc( + $connection->select()->from( + $connection->select() + ->from( + ['asset_keywords_table' => $this->connection->getTableName(self::TABLE_ASSET_KEYWORD)], + ['id'] + )->where( + 'keyword = ?', + $value + )->joinInner( + ['keywords_table' => $this->connection->getTableName(self::TABLE_KEYWORDS)], + 'keywords_table.keyword_id = asset_keywords_table.id', + ['asset_id'] + ), + ['asset_id'] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php new file mode 100644 index 0000000000000..85522c6b07e00 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryUi\Model\UpdateAsset\UpdateKeywords; +use Magento\MediaGalleryUi\Model\UpdateAsset\SaveMetadataToFile; + +class UpdateAsset +{ + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsByIds; + + /** + * @var SaveAssetsInterface + */ + private $saveAssets; + + /** + * @var SaveMetadataToFile + */ + private $processMetadata; + + /** + * @var UpdateKeywords + */ + private $processKeywords; + + /** + * @param AssetInterfaceFactory $assetFactory + * @param GetAssetsByIdsInterface $getAssetsByIds + * @param SaveAssetsInterface $saveAssets + * @param UpdateKeywords $processKeywords + * @param SaveMetadataToFile $processMetadata + */ + public function __construct( + AssetInterfaceFactory $assetFactory, + GetAssetsByIdsInterface $getAssetsByIds, + SaveAssetsInterface $saveAssets, + UpdateKeywords $processKeywords, + SaveMetadataToFile $processMetadata + ) { + $this->assetFactory = $assetFactory; + $this->getAssetsByIds = $getAssetsByIds; + $this->saveAssets = $saveAssets; + $this->processKeywords = $processKeywords; + $this->processMetadata = $processMetadata; + } + + /** + * Save asset details + * + * @param int $id + * @param MetadataInterface $data + */ + public function execute(int $id, MetadataInterface $data): void + { + $asset = $this->getAsset($id); + + $updatedAsset = $this->assetFactory->create( + [ + 'id' => $asset->getId(), + 'path' => $asset->getPath(), + 'title' => $data->getTitle() ?? $asset->getTitle(), + 'description' => $data->getDescription() ?? $asset->getDescription(), + 'width' => $asset->getWidth(), + 'height' => $asset->getHeight(), + 'size' => $asset->getSize(), + 'hash' => $asset->getHash(), + 'contentType' => $asset->getContentType(), + 'source' => $asset->getSource() + ] + ); + + $this->saveAssets->execute([$updatedAsset]); + $this->processMetadata->execute($asset->getPath(), $data); + + $keywords = $data->getKeywords(); + if (isset($keywords)) { + $this->processKeywords->execute($id, $keywords); + } + } + + /** + * Load asset by id + * + * @param int $id + * @return AssetInterface + * @throws LocalizedException + */ + private function getAsset(int $id): AssetInterface + { + $assets = $this->getAssetsByIds->execute([$id]); + if (empty($assets)) { + throw new LocalizedException(__('Could not retrieve the asset.')); + } + return current($assets); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php new file mode 100644 index 0000000000000..3ebe04374f81e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\UpdateAsset; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Psr\Log\LoggerInterface; + +class SaveMetadataToFile +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var AddMetadataInterface + */ + private $addMetadata; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Filesystem $filesystem + * @param AddMetadataInterface $addMetadata + * @param LoggerInterface $logger + */ + public function __construct( + Filesystem $filesystem, + AddMetadataInterface $addMetadata, + LoggerInterface $logger + ) { + $this->filesystem = $filesystem; + $this->addMetadata = $addMetadata; + $this->logger = $logger; + } + + /** + * Save updated metadata + * + * @param string $path + * @param MetadataInterface $data + */ + public function execute(string $path, MetadataInterface $data): void + { + $absolutePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($path); + + try { + $this->addMetadata->execute($absolutePath, $data); + } catch (LocalizedException $e) { + $this->logger->critical($e); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php new file mode 100644 index 0000000000000..2a359d5a14025 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\UpdateAsset; + +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; + +class UpdateKeywords +{ + /** + * @var AssetKeywordsInterfaceFactory + */ + private $assetKeywordsFactory; + + /** + * @var KeywordInterfaceFactory + */ + private $keywordFactory; + + /** + * @var SaveAssetsKeywordsInterface + */ + private $saveAssetKeywords; + + /** + * @param AssetKeywordsInterfaceFactory $assetKeywordsFactory + * @param KeywordInterfaceFactory $keywordFactory + * @param SaveAssetsKeywordsInterface $saveAssetKeywords + */ + public function __construct( + AssetKeywordsInterfaceFactory $assetKeywordsFactory, + KeywordInterfaceFactory $keywordFactory, + SaveAssetsKeywordsInterface $saveAssetKeywords + ) { + $this->assetKeywordsFactory = $assetKeywordsFactory; + $this->keywordFactory = $keywordFactory; + $this->saveAssetKeywords = $saveAssetKeywords; + } + + /** + * Save asset keywords + * + * @param int $assetId + * @param string[] $keywords + */ + public function execute(int $assetId, array $keywords): void + { + $this->saveAssetKeywords->execute([ + $this->assetKeywordsFactory->create([ + 'assetId' => $assetId, + 'keywords' => $this->createKeywords($keywords) + ]) + ]); + } + + /** + * Create keyword objects from strings + * + * @param string[] $keywords + * @return KeywordInterface[] + */ + private function createKeywords(array $keywords): array + { + $keywordObjects = []; + foreach ($keywords as $keyword) { + $keywordObjects[] = $this->keywordFactory->create( + [ + 'keyword' => $keyword + ] + ); + } + return $keywordObjects; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UploadImage.php b/app/code/Magento/MediaGalleryUi/Model/UploadImage.php new file mode 100644 index 0000000000000..c918548bea553 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UploadImage.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; + +/** + * Uploads an image to storage + */ +class UploadImage +{ + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Storage $imagesStorage + * @param Filesystem $filesystem + */ + public function __construct( + Storage $imagesStorage, + Filesystem $filesystem + ) { + $this->imagesStorage = $imagesStorage; + $this->filesystem = $filesystem; + } + + /** + * Uploads the image and returns file object + * + * @param string $targetFolder + * @param string $type + * @throws LocalizedException + */ + public function execute(string $targetFolder, string $type): void + { + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + if (!$mediaDirectory->isDirectory($targetFolder)) { + throw new LocalizedException(__('Directory %1 does not exist in media directory.', $targetFolder)); + } + + $this->imagesStorage->uploadFile($mediaDirectory->getAbsolutePath($targetFolder), $type); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php b/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php new file mode 100644 index 0000000000000..7988ac2d9e635 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Plugin; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite; + +/** + * Create resizes files that were synced + */ +class CreateThumbnails +{ + /** + * @var Storage + */ + private $storage; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Filesystem $filesystem + * @param Storage $storage + */ + public function __construct(Filesystem $filesystem, Storage $storage) + { + $this->storage = $storage; + $this->filesystem = $filesystem; + } + + /** + * Create thumbnails for synced files. + * + * @param ImportFilesComposite $subject + * @param string[] $paths + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute(ImportFilesComposite $subject, array $paths): array + { + foreach ($paths as $path) { + $this->storage->resizeFile( + $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($path) + ); + } + + return [$paths]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/README.md b/app/code/Magento/MediaGalleryUi/README.md new file mode 100644 index 0000000000000..6fbad656b23a8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryUi module + +The Magento_MediaGalleryUi module is responsible for the media gallery user interface (UI) implementation. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryUi 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_MediaGalleryUi 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/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/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..c056727aa8fe8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.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="AdminAssertImageInStandaloneMediaGalleryActionGroup"> + <annotations> + <description>Validates that the provided image is present and correct in the standalone media gallery.</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + + <seeElement selector="{{AdminEnhancedMediaGalleryActionsSection.imageSrc(imageName)}}" + stepKey="checkFirstImageAfterSearch"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageUploadFileSizeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageUploadFileSizeActionGroup.xml new file mode 100644 index 0000000000000..8d8ff1f34e293 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageUploadFileSizeActionGroup.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="AdminAssertImageUploadFileSizeThanActionGroup"> + <annotations> + <description>Validates that the provided image has correct file size in category content section.</description> + </annotations> + <arguments> + <argument name="fileSize" type="string"/> + </arguments> + + <grabTextFrom selector="{{AdminCategoryContentSection.imageFileMeta}}" stepKey="imageSize"/> + <assertStringContainsString stepKey="assertFileSize"> + <expectedResult type="string">{{fileSize}}</expectedResult> + <actualResult type="variable">imageSize</actualResult> + </assertStringContainsString> + + </actionGroup> +</actionGroups> 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/AdminAssertMediaGalleryFilterPlaceholderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceholderActionGroup.xml new file mode 100644 index 0000000000000..db400ff151ae3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceholderActionGroup.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="AdminAssertMediaGalleryFilterPlaceholderActionGroup"> + <annotations> + <description>Assert asset filter placeholder value</description> + </annotations> + <arguments> + <argument name="filterPlaceholder" type="string"/> + </arguments> + + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.activeFilterPlaceholder(filterPlaceholder)}}" stepKey="assertFilterPLaceHolder" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..d47eb491f9b5d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.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="AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup"> + <annotations> + <description>Adds image to target element from View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.addImage}}" stepKey="openContextMenu"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml new file mode 100644 index 0000000000000..9a550805a7dec --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.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="AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup"> + <annotations> + <description>Applies duplicated images filter to the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.duplicatedFilterCheckbox}}" stepKey="clickShowDuplicates"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml new file mode 100644 index 0000000000000..9d7d725cf49de --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.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="AdminEnhancedMediaGalleryApplyFiltersActionGroup"> + <annotations> + <description>Apply filters in media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.applyFilters}}" stepKey="applyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml new file mode 100644 index 0000000000000..aeee921f92e58 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.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="AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup"> + <annotations> + <description>Assert media gallery grid filters</description> + </annotations> + <arguments> + <argument name="resultValue" type="string"/> + </arguments> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.filtersButton}}" stepKey="expandFiltersToCheckAppliedFilter"/> + <see selector="{{AdminEnhancedMediaGalleryFiltersSection.activeFilter(resultValue)}}" userInput="{{resultValue}}" stepKey="verifyAppliedFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml new file mode 100644 index 0000000000000..ca503b7357300 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.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="AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup"> + <annotations> + <description>Asserts images has been deleted in mass action.</description> + </annotations> + <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/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml new file mode 100644 index 0000000000000..efcf40cd2b644 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.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="AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup"> + <annotations> + <description>Asserts that massaction mode can be enabled and disabled, verify massaction view after switch to massaction mode</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckbox(imageName)}}" stepKey="selectImageInGridToDelte"/> + <see selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" userInput="(1 Selected)" stepKey="verifySelectedCount"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml new file mode 100644 index 0000000000000..1ec2004b22f24 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.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="AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup"> + <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/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml new file mode 100644 index 0000000000000..783e71719c659 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.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="AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup"> + <annotations> + <description>Assert that grid have no active filter</description> + </annotations> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryFiltersSection.activeFilterPlaceholder}}" stepKey="assertThereIsNoActiveFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml new file mode 100644 index 0000000000000..b53e76e06cfb5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.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="AdminEnhancedMediaGalleryAssertWarningMessageActionGroup"> + <annotations> + <description>Assert image delete action popup contains warnin message</description> + </annotations> + <arguments> + <argument name="messageText" type="string"/> + </arguments> + + <see userInput="{{messageText}}" stepKey="assertWarningMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml new file mode 100644 index 0000000000000..478ca2b3b5be9 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.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="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup"> + <annotations> + <description>Apply filters in Category grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.categoryGridApplyFilters}}" stepKey="applyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml new file mode 100644 index 0000000000000..00608504fd7a6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.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="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup"> + <annotations> + <description>Expand media gallery category filters by clicking on button</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.categoryGridFiltersButton}}" stepKey="expandFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml new file mode 100644 index 0000000000000..600e1cd747943 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.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="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup"> + <annotations> + <description>Click delete images button.</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}" stepKey="clickDeleteImages"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickEntityUsedInActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickEntityUsedInActionGroup.xml new file mode 100644 index 0000000000000..ec54d83bd808a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickEntityUsedInActionGroup.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="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup"> + <annotations> + <description>Clicks one Used In section entity</description> + </annotations> + <arguments> + <argument name="entityName" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.usedInLink(entityName)}}" stepKey="openContextMenu"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </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/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml new file mode 100644 index 0000000000000..3754eb319da44 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.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="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup"> + <annotations> + <description>Closes View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.cancel}}" stepKey="clickCancel"/> + <wait time="1" stepKey="waitForElementRender"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml new file mode 100644 index 0000000000000..90546eca8dc0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.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="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup"> + <annotations> + <description>Click confirm on confirmation popup images delete action.</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeletingProcces"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml new file mode 100644 index 0000000000000..95f3080049db7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.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="AdminEnhancedMediaGalleryDeleteGridViewActionGroup"> + <annotations> + <description>Delete grid view bookmarks by name</description> + </annotations> + <arguments> + <argument name="viewToDelete" type="string"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.viewByName(viewToDelete)}}{{AdminEnhancedMediaGalleryActionsSection.editViewButtonPartial}}" stepKey="clickEditButton"/> + <seeElement selector="{{AdminEnhancedMediaGalleryActionsSection.deleteViewButton}}" stepKey="seeDeleteButton"/> + <click selector="{{AdminEnhancedMediaGalleryActionsSection.deleteViewButton}}" stepKey="clickDeleteButton"/> + <waitForPageLoad stepKey="waitForDeletion" time="10"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml new file mode 100644 index 0000000000000..f404ffbe7c4f0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.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="AdminEnhancedMediaGalleryDisableMassactionModeActionGroup"> + <annotations> + <description>Disable massaction mode by clicking on cancel button</description> + </annotations> + + + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.cancelMassActionMode}}" stepKey="cancelMassAction"/> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" stepKey="verifyTeminateMAssAction"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..84712e8e3f3ae --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.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="AdminEnhancedMediaGalleryEditImageDetailsActionGroup"> + <annotations> + <description>Opens Edit image details panel panel for the first image in the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.edit}}" stepKey="edit"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml new file mode 100644 index 0000000000000..5e5c89637c6a1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.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="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup"> + <annotations> + <description>Activate massaction mode by click on Delete Selected..</description> + </annotations> + + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteImages}}" stepKey="waitForMassActionButton"/> + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteImages}}" stepKey="clickOnMassActionButton"/> + </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/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml new file mode 100644 index 0000000000000..d2ac1c78b2582 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.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="AdminEnhancedMediaGalleryExpandFilterActionGroup"> + <annotations> + <description>Expand media gallery filter by clicking on button</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.filtersButton}}" stepKey="expandFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml new file mode 100644 index 0000000000000..b3733ceb4c4a0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.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="AdminEnhancedMediaGalleryImageDeleteActionGroup"> + <annotations> + <description>Delete image from the Media Gallery</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.delete}}" stepKey="deleteImage"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml new file mode 100644 index 0000000000000..001aa010dbdd4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.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="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup"> + <annotations> + <description>Delete image from the View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.delete}}" stepKey="deleteImage"/> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryViewDetailsSection.confirmDelete}}" stepKey="waitForConfirmation"/> + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml new file mode 100644 index 0000000000000..931da0ee06fef --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.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="AdminEnhancedMediaGalleryImageDetailsEditActionGroup"> + <annotations> + <description>Edit image from the View Details panel</description> + </annotations> + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.edit}}" stepKey="editImage"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml new file mode 100644 index 0000000000000..0da3de9501c13 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.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="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup"> + <annotations> + <description>Save image details from the View Details panel</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.title}}" userInput="{{image.title}}" stepKey="setTitle" /> + <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.description}}" userInput="{{image.description}}" stepKey="setDescription" /> + <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.save}}" stepKey="saveDetails"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml new file mode 100644 index 0000000000000..57096124c0370 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.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="AdminEnhancedMediaGallerySaveCustomViewActionGroup"> + <annotations> + <description>Save custom view media gallery</description> + </annotations> + <arguments> + <argument name="viewName" type="string" defaultValue="Test View"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.saveViewAs}}" stepKey="saveView"/> + <fillField selector="{{AdminGridDefaultViewControls.viewName}}" userInput="{{viewName}}" stepKey="inputViewName"/> + <pressKey selector="{{AdminGridDefaultViewControls.viewName}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml new file mode 100644 index 0000000000000..4244724599fed --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup"> + <annotations> + <description>Apply custom bookmarks view to the media gallery grid</description> + </annotations> + <arguments> + <argument name="selectView" type="string"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.viewByName(selectView)}}" stepKey="clickOnViewButton"/> + <waitForPageLoad stepKey="waitForGridLoad" time="10"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml new file mode 100644 index 0000000000000..6532fb869d2cc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.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="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup"> + <annotations> + <description>Select images in grid by clicking on mass action checkbox</description> + </annotations> + <arguments> + <argument name="imageName" type="string" defaultValue="magento"/> + </arguments> + + <checkOption selector="{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckbox(imageName)}}" stepKey="selectImageInGridToDelte"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml new file mode 100644 index 0000000000000..9be288b064742 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.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="AdminEnhancedMediaGallerySelectSourceFilterActionGroup"> + <annotations> + <description>Select source filter by provided option</description> + </annotations> + <arguments> + <argument type="string" name="filterValue"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.sourceFilterValue(filterValue)}}" stepKey="openContextMenu"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml new file mode 100644 index 0000000000000..72d01e1871513 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.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="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup"> + <annotations> + <description>Set search options filter</description> + </annotations> + <arguments> + <argument type="string" name="filterName"/> + <argument type="string" name="optionName"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilter(filterName)}}" stepKey="openFilter"/> + <fillField selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterInput(filterName)}}" userInput="{{optionName}}" stepKey="enterOptionName" /> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterOption(filterName, optionName)}}" stepKey="selectOption"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterDone(filterName)}}" stepKey="clickDone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml new file mode 100644 index 0000000000000..053a1185b3fda --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.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="AdminEnhancedMediaGalleryUploadImageActionGroup"> + <annotations> + <description>Uploads the provided Image to Media Gallery. + If you use this action group, you MUST add steps to delete the image in the "after" steps.</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <attachFile selector="{{AdminEnhancedMediaGalleryActionsSection.upload}}" userInput="{{image.value}}" stepKey="uploadImage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml new file mode 100644 index 0000000000000..eb2fc79567d08 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.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="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup"> + <annotations> + <description>Verifies image description on the View Details panel</description> + </annotations> + <arguments> + <argument name="description"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.description}}" stepKey="grabDescription"/> + <assertStringContainsString stepKey="verifyDescription"> + <actualResult type="variable">grabDescription</actualResult> + <expectedResult type="string">{{description}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..1ebaa0581e33e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup"> + <annotations> + <description>Verifies image information on the View Details panel</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{image.fileName}}</expectedResult> + </assertStringContainsString> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.contentType}}" stepKey="grabContentType"/> + <assertStringContainsStringIgnoringCase stepKey="verifyContentType"> + <actualResult type="variable">grabContentType</actualResult> + <expectedResult type="string">{{image.extension}}</expectedResult> + </assertStringContainsStringIgnoringCase> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.type}}" stepKey="grabType"/> + <assertStringContainsString stepKey="verifyType"> + <actualResult type="variable">grabType</actualResult> + <expectedResult type="string">Image</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml new file mode 100644 index 0000000000000..4c38b7dbc8c3e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.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="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup"> + <annotations> + <description>Verifies image filename on the View Details panel</description> + </annotations> + <arguments> + <argument name="filename" type="string"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.filename}}" stepKey="grabFilename"/> + <assertStringContainsString stepKey="verifyFilename"> + <actualResult type="variable">grabFilename</actualResult> + <expectedResult type="string">{{filename}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml new file mode 100644 index 0000000000000..2fc4f7ea25fd0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.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="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup"> + <annotations> + <description>Verifies image keywords on the View Details panel</description> + </annotations> + <arguments> + <argument name="keywords"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.keywords}}" stepKey="grabKeywords"/> + <assertStringContainsString stepKey="verifyKeywords"> + <actualResult type="variable">grabKeywords</actualResult> + <expectedResult type="string">{{keywords}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml new file mode 100644 index 0000000000000..08dac976332ee --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.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="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup"> + <annotations> + <description>Verifies image title on the View Details panel</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{title}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..b5c0bbac69bec --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.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="AdminEnhancedMediaGalleryViewImageDetails"> + <annotations> + <description>Opens View Details panel for the first image in the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.viewDetails}}" stepKey="viewDetails"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml new file mode 100644 index 0000000000000..6ddb6311c1a7e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryApplySelectFilterActionGroup"> + <annotations> + <description>Applies select filter to the media gallery grid</description> + </annotations> + <arguments> + <argument name="filterLabel" type="string"/> + <argument name="optionLabel" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.selectFilter(filterLabel)}}" stepKey="openSelectFilter"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.selectFilterOption(filterLabel, optionLabel)}}" stepKey="selectFilterOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml new file mode 100644 index 0000000000000..a930f65b71040 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryApplyUsedInFilterActionGroup"> + <annotations> + <description>Applies Show Images Used In filter to the media gallery grid</description> + </annotations> + <arguments> + <argument name="entityType" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.usedInSelectDropdown}}" stepKey="openUsedInfilter"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.usedInEntityType(entityType)}}" stepKey="selectEntityType"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterDone('Show Images Used In')}}" stepKey="clickDone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml new file mode 100644 index 0000000000000..d0d9817da6d34 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.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="AdminMediaGalleryAssertFolderDoesNotExistActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <wait time="5" stepKey="waitForFolderTreeReloads"/> + <dontSeeElement selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="folderDoesNotExist"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml new file mode 100644 index 0000000000000..7d71c764bc8de --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.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="AdminMediaGalleryAssertFolderNameActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <waitForElementVisible selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="waitForFolder"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml new file mode 100644 index 0000000000000..08c93a805dc70 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.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="AdminMediaGalleryAssertImageInGridActionGroup"> + <annotations> + <description>Asserts that image exists in media gallery grid</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(title)}}" stepKey="waitForImageToBeVisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml new file mode 100644 index 0000000000000..cc4de51357de0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.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="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup"> + <annotations> + <description>Asserts that image does not exists in media gallery grid</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(title)}}" stepKey="waitForImageToBeVisible"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml new file mode 100644 index 0000000000000..45ab4dc4538e0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.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="AdminMediaGalleryClickAddSelectedActionGroup"> + <waitForElementVisible selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="waitForAddSelectedButton"/> + <click selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="ClickAddSelected"/> + <wait time="5" stepKey="waitForImageToBeAdded"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml new file mode 100644 index 0000000000000..ee2ff887488a4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.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="AdminMediaGalleryClickImageInGridActionGroup"> + <annotations> + <description>Select image on enhanced media gallery</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(imageName)}}" stepKey="waitForImageToBeVisible"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(imageName)}}" stepKey="clickOnImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml new file mode 100644 index 0000000000000..3e555c25e0a98 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.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="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup"> + <annotations> + <description>Click ok button on upload image tinyMce4 popup.</description> + </annotations> + + <waitForElementVisible selector="{{MediaGallerySection.OkBtn}}" stepKey="waitForOkBtn"/> + <click selector="{{MediaGallerySection.OkBtn}}" stepKey="clickOkBtn"/> + <waitForPageLoad stepKey="wait"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml new file mode 100644 index 0000000000000..f3ccc8ef7be04 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.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="AdminMediaGalleryCreateNewFolderActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <fillField selector="{{AdminMediaGalleryFolderSection.folderNameField}}" userInput="{{name}}" stepKey="setFolderName" /> + <click selector="{{AdminMediaGalleryFolderSection.folderConfirmCreateButton}}" stepKey="clickCreateButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml new file mode 100644 index 0000000000000..964b33dd38d55 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.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="AdminMediaGalleryEditAssetAddKeywordActionGroup"> + <annotations> + <description>Set Keywords on the Edit Details panel</description> + </annotations> + <arguments> + <argument name="keyword"/> + </arguments> + + <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.newKeyword}}" userInput="{{keyword}}" stepKey="enterKeyword"/> + <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.addNewKeyword}}" stepKey="addKeyword"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.xml new file mode 100644 index 0000000000000..b2ce726b3bd6c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.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="AdminMediaGalleryEditAssetRemoveKeywordActionGroup"> + <annotations> + <description>Remove Keywords on the Edit Details panel</description> + </annotations> + <arguments> + <argument name="keyword" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.removeSelectedKeyword(keyword)}}" stepKey="removeKeyword"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml new file mode 100644 index 0000000000000..d842535940253 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.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="AdminMediaGalleryEnhancedEnableActionGroup"> + <arguments> + <argument name="enabled" type="string" defaultValue="{{MediaGalleryConfigDataDisabled.value}}"/> + </arguments> + <amOnPage url="{{AdminMediaGalleryConfigSystemPage.url}}" stepKey="navigateToSystemConfigurationPage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + <scrollTo selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" stepKey="scrollToEnhancedMediaGalleryFieldset"/> + <conditionalClick stepKey="expandEnhancedMediaGalleryTab" selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" dependentSelector="{{AdminConfigSystemSection.enhancedMediaGalleryEnabledField}}" visible="false" /> + <waitForElementVisible selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" stepKey="waitForFieldset" /> + <selectOption userInput="{{enabled}}" selector="{{AdminConfigSystemSection.enhancedMediaGalleryEnabledField}}" stepKey="enableOrDisableMediaGallery"/> + <click selector="{{AdminConfigSystemSection.saveConfig}}" stepKey="saveConfiguration"/> + <waitForPageLoad stepKey="waitForConfigurationToSave"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryExpandFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryExpandFolderActionGroup.xml new file mode 100644 index 0000000000000..f10aed54c8447 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryExpandFolderActionGroup.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="AdminMediaGalleryExpandFolderActionGroup"> + <arguments> + <argument name="fieldId" type="string"/> + </arguments> + <conditionalClick selector="{{AdminMediaGalleryFolderSection.folderArrow(fieldId)}}" + dependentSelector="{{AdminMediaGalleryFolderSection.checkIfFolderArrowExpand(fieldId)}}" stepKey="clickArrowIfClosed" visible="true"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml new file mode 100644 index 0000000000000..f7e8f551e681f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.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="AdminMediaGalleryFolderDeleteActionGroup"> + <wait time="2" stepKey="waitBeforeDeleteButtonWillBeActive"/> + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderDeleteModalHeader}}" stepKey="waitBeforeModalAppears"/> + <click selector="{{AdminMediaGalleryFolderSection.folderConfirmDeleteButton}}" stepKey="clickConfirmDeleteButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml new file mode 100644 index 0000000000000..b8ed1d4f1cd25 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.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="AdminMediaGalleryFolderSelectActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <wait time="2" stepKey="waitBeforeClickOnFolder"/> + <click selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="selectFolder"/> + <waitForLoadingMaskToDisappear stepKey="waitForFolderContents"/> + </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/AdminMediaGalleryImageDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml new file mode 100644 index 0000000000000..e6cbbfbc1f48d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.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="AdminMediaGalleryImageDeleteActionGroup"> + <annotations> + <description>Delete image from the Enhanced Media Gallery using header delete button</description> + </annotations> + <waitForElementVisible selector="{{AdminMediaGalleryHeaderButtonsSection.deleteSelected}}" stepKey="waitForDeleteSelectedButton"/> + <click selector="{{AdminMediaGalleryHeaderButtonsSection.deleteSelected}}" stepKey="ClickDeleteSelectedButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml new file mode 100644 index 0000000000000..165522892f271 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.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="AdminMediaGalleryOpenNewFolderFormActionGroup"> + <click selector="{{AdminMediaGalleryFolderSection.folderNewCreateButton}}" stepKey="clickCreateNewFolderButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderNewModalHeader}}" stepKey="waitForModalOpen"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml new file mode 100644 index 0000000000000..89664ef152dba --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.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="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup"> + <annotations> + <description>Opens Enhanced MediaGallery from image uploader on category page</description> + </annotations> + + <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"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml new file mode 100644 index 0000000000000..0b2540de5288e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.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="AdminOpenMediaGalleryFromPageNoEditorActionGroup"> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <waitForElementVisible selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="waitForInsertImageButton" /> + <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImage" /> + <!-- wait for initial media gallery load, where the gallery chrome loads (and triggers loading modal) --> + <waitForPageLoad stepKey="waitForMediaGalleryInitialLoad"/> + <!-- wait for second media gallery load, where the gallery images load (and triggers loading modal once more) --> + <waitForPageLoad stepKey="waitForMediaGallerySecondaryLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml new file mode 100644 index 0000000000000..3143b4ff24fb4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.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="AdminOpenMediaGalleryTinyMce4ActionGroup"> + <annotations> + <description>Opens Enhanced MediaGallery from category page by tyniMce4 image icon</description> + </annotations> + + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="clickExpandContent"/> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> + <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <click selector="{{MediaGallerySection.Browse}}" stepKey="clickBrowse"/> + <waitForPageLoad stepKey="waitForPopup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..1ef908f34918e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.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="AdminOpenStandaloneMediaGalleryActionGroup"> + <amOnPage url="{{AdminStandaloneMediaGalleryPage.url}}" stepKey="amOnStandaloneMediaGalleryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml new file mode 100644 index 0000000000000..9460d0b339ca4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.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="AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup"> + <annotations> + <description>Assert that created_at updated_at time NOT equals</description> + </annotations> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.createdAtDate}}" stepKey="grabCreatedTime"/> + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.updatedAtDate}}" stepKey="grabModifietTime"/> + <assertNotEquals stepKey="verifyContentType"> + <actualResult type="variable">grabCreatedTime</actualResult> + <expectedResult type="variable">grabModifietTime</expectedResult> + </assertNotEquals> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml new file mode 100644 index 0000000000000..e9558ac87df3b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.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="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup"> + <annotations> + <description>Assert that an image was deleted from Enhanced Media Gallery.</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <see userInput='The asset "{{title}}" has been successfully deleted' stepKey="verifyDeleteImage"/> + </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/AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup.xml new file mode 100644 index 0000000000000..076885ddaf8b6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup.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="AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup"> + <annotations> + <description>Assert that created_at updated_at time are the same for newly uploaded image </description> + </annotations> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.createdAtDate}}" stepKey="grabCreatedTime"/> + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.updatedAtDate}}" stepKey="grabModifietTime"/> + <assertEquals stepKey="verifyContentType"> + <actualResult type="variable">grabCreatedTime</actualResult> + <expectedResult type="variable">grabModifietTime</expectedResult> + </assertEquals> + + </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/AssertAdminMediaGalleryContextMenuOpenedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminMediaGalleryContextMenuOpenedActionGroup.xml new file mode 100644 index 0000000000000..ede7052712b4b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminMediaGalleryContextMenuOpenedActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminMediaGalleryContextMenuOpenedActionGroup"> + <annotations> + <description>Verify that context menu is closed in Media Gallery.</description> + </annotations> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryImageActionsSection.contextMenuItem}}" stepKey="verifyContextMenuIsClosed" /> + </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/ActionGroup/AssertImageAddedToPageContentActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml new file mode 100644 index 0000000000000..ff11f1a5c7058 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.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="AssertImageAddedToPageContentActionGroup"> + <annotations> + <description>Validates that the an image was added to the content.</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <grabValueFrom selector="{{CmsNewPagePageContentSection.content}}" stepKey="grabTextFromContent"/> + <assertStringContainsString stepKey="assertContentContainsAddedImage"> + <expectedResult type="string">{{imageName}}</expectedResult> + <actualResult type="variable">grabTextFromContent</actualResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..e17be216335fb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertImageAttributesOnEnhancedMediaGalleryActionGroup"> + <annotations> + <description>Assets image information on the Media Gallery grid</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{image.fileName}}</expectedResult> + </assertStringContainsString> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.contentType}}" stepKey="grabContentType"/> + <assertStringContainsStringIgnoringCase stepKey="verifyContentType"> + <actualResult type="variable">grabContentType</actualResult> + <expectedResult type="string">{{image.extension}}</expectedResult> + </assertStringContainsStringIgnoringCase> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.dimensions}}" stepKey="grabDimensions"/> + <assertNotEmpty stepKey="verifyDimensions"> + <actualResult type="variable">grabDimensions</actualResult> + </assertNotEmpty> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup.xml new file mode 100644 index 0000000000000..be9c7e939103d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup.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="AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup"> + <annotations> + <description>Verifies that the passed comma-separated list of keywords are not present on the View Details panel</description> + </annotations> + <arguments> + <argument name="keywords" type="string"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.keywords}}" stepKey="grabKeywords"/> + <assertStringNotContainsString stepKey="verifyKeywords"> + <actualResult type="variable">grabKeywords</actualResult> + <expectedResult type="string">{{keywords}}</expectedResult> + </assertStringNotContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml new file mode 100644 index 0000000000000..1d568fb6a1da4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.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="SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup" extends="SearchAdminDataGridByKeywordActionGroup"> + <annotations> + <description>EXTENDS: SearchAdminDataGridByKeywordActionGroup. Fills 'Search by keyword' on an Standalone Media Gallery Admin Grid page. Clicks on Submit Search.</description> + </annotations> + <arguments> + <argument name="keyword" type="string" defaultValue=""/> + </arguments> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml new file mode 100644 index 0000000000000..1ec5e7d802a61 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.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="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup"> + <arguments> + <argument name="categoryEntity" defaultValue="SimpleSubCategory"/> + <argument name="imageName" type="string"/> + </arguments> + <annotations> + <description>Navigates to the category page on the storefront and asserts that the image is present in description.</description> + </annotations> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="openHomePage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryEntity.name)}}" stepKey="toCategory"/> + <waitForPageLoad stepKey="waitForCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.imageSource(imageName)}}" stepKey="seeImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml new file mode 100644 index 0000000000000..4adf92b1c4c09 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="UpdatedImageDetails" type="image"> + <data key="title">renamed title</data> + <data key="description">test description</data> + <data key="file">magento.jpg</data> + <data key="fileName">renamed title</data> + <data key="extension">jpg</data> + <data key="keyword">newkeyword</data> + </entity> + <entity name="ImageUploadPng" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="file_type">Upload File</data> + <data key="value">png.png</data> + <data key="file">png.png</data> + <data key="fileName">png</data> + <data key="extension">png</data> + </entity> + <entity name="ImageUploadGif" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="file_type">Upload File</data> + <data key="value">gif.gif</data> + <data key="file">gif.gif</data> + <data key="fileName">gif</data> + <data key="extension">gif</data> + </entity> + <entity name="ImageMetadata" type="image"> + <data key="title">Title of the magento image</data> + <data key="description">Description of the magento image</data> + <data key="file">magento3.jpg</data> + <data key="fileName">Title of the magento image</data> + <data key="extension">jpg</data> + <data key="keywords">magento, mediagallerymetadata</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml new file mode 100644 index 0000000000000..e4149acdf58d1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMediaGalleryFolderData"> + <data key="name" unique="suffix">folder</data> + </entity> + <entity name="AdminMediaGalleryFolderInvalidData"> + <data key="name">,.?/:;'[{]}|~`!@#$%^*()_=+</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml new file mode 100644 index 0000000000000..e8f394a006104 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="MediaGalleryConfigDataEnabled"> + <data key="path">system/media_gallery/enabled</data> + <data key="value">1</data> + </entity> + <entity name="MediaGalleryConfigDataDisabled"> + <data key="path">system/media_gallery/enabled</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminMediaGalleryConfigSystemPage.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminMediaGalleryConfigSystemPage.xml new file mode 100644 index 0000000000000..429e5da4129d3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminMediaGalleryConfigSystemPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminMediaGalleryConfigSystemPage" url="admin/system_config/edit/section/system" area="admin" module="Magento_Config"> + <section name="AdminConfigSystemSection"/> + </page> +</pages> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml new file mode 100644 index 0000000000000..f7ed27171db40 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminStandaloneMediaGalleryPage" url="/media_gallery/media" area="admin" module="Magento_MediaGalleryUi"/> +</pages> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml new file mode 100644 index 0000000000000..b7900f6664c62 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.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="AdminConfigSystemSection"> + <element name="enhancedMediaGalleryFieldset" type="block" selector="#system_media_gallery-head"/> + <element name="enhancedMediaGalleryEnabledField" type="select" selector="[data-ui-id='select-groups-media-gallery-fields-enabled-value']"/> + <element name="saveConfig" type="button" selector="#save"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml new file mode 100644 index 0000000000000..907f2c3116800 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.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="AdminEnhancedMediaGalleryActionsSection"> + <element name="editViewButtonPartial" type="button" selector="/following-sibling::div/button[@class='action-edit']"/> + <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"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml new file mode 100644 index 0000000000000..b4071295bacf3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.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="AdminEnhancedMediaGalleryDeleteModalSection"> + <element name="confirmDelete" type="button" selector=".media-gallery-delete-image-action .action-accept"/> + <element name="cancelDelete" type="button" selector=".media-gallery-delete-image-action .action-dismiss"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml new file mode 100644 index 0000000000000..b0bed4563003e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryEditDetailsSection"> + <element name="title" type="input" selector="#title"/> + <element name="fileName" type="text" selector="#path"/> + <element name="description" type="textarea" selector="#description"/> + <element name="newKeyword" type="input" selector="[data-ui-id='keyword']"/> + <element name="addNewKeyword" type="input" selector="[data-ui-id='add-keyword']"/> + <element name="removeSelectedKeyword" type="button" selector="//span[contains(text(), '{{keyword}}')]/following-sibling::button[@data-action='remove-selected-item']" parameterized="true"/> + <element name="cancel" type="button" selector="#image-details-action-cancel"/> + <element name="save" type="button" selector="#image-details-action-save"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml new file mode 100644 index 0000000000000..da9f773d0f75e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryFiltersSection"> + <element name="filtersButton" type="button" selector="//div[@class='media-gallery-container']//button[@data-action='grid-filter-expand']"/> + <element name="categoryGridFiltersButton" type="button" selector="//div[@class='media-gallery-category-container']//button[@data-action='grid-filter-expand']"/> + <element name="sourceFilterValue" type="select" parameterized="true" selector="//div[@class='media-gallery-container']//select[@name='source']//option[@value='{{option}}']"/> + <element name="applyFilters" type="button" selector="//div[@class='media-gallery-container']//button[@data-action='grid-filter-apply']"/> + <element name="categoryGridApplyFilters" type="button" selector="//div[@class='media-gallery-category-container']//button[@data-action='grid-filter-apply']"/> + <element name="activeFilter" type="text" selector="//div[@class='media-gallery-container']//div[@class='admin__current-filters-list-wrap']//span[contains( ., '{{filter}}')]" parameterized="true"/> + <element name="activeFilterPlaceholder" type="text" selector="//div[@class='media-gallery-container']//div[@class='admin__current-filters-list-wrap']"/> + <element name="usedInSelectDropdown" type="text" selector="//label[@class='admin__form-field-label']/span[text()='Show Images Used In']/parent::*/parent::div/div//div[@class='admin__action-multiselect-text' and text()='Select...']"/> + <element name="usedInEntityType" type="text" selector="//label[@class='admin__action-multiselect-label']/span[text()='{{entityType}}']" parameterized="true"/> + <element name="usedInDoneButton" type="button" selector="//div[@class='admin__action-multiselect-actions-wrap']/button/span[text()='Done']"/> + <element name="selectFilter" type="button" selector="//label[@class='admin__form-field-label']/span[text()='{{filterLabel}}']/parent::*/parent::div/div[@class='admin__form-field-control']/select" parameterized="true"/> + <element name="selectFilterOption" type="button" selector="//label[@class='admin__form-field-label']/span[text()='{{filterLabel}}']/parent::*/parent::div/div[@class='admin__form-field-control']/select/option[@data-title='{{optionLabel}}']" parameterized="true"/> + <element name="searchOptionsFilter" type="select" selector="//div[label/span[contains(text(), '{{filterName}}')]]//div[@class='action-select admin__action-multiselect']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterInput" type="input" selector="//div[label/span[contains(text(), '{{filterName}}')]]//input[@data-role='advanced-select-text']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterOption" type="text" selector="//div[label/span[contains(text(), '{{filterName}}')]]//label[@class='admin__action-multiselect-label']/span[text()='{{optionName}}']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterDone" type="button" selector="//div[label/span[contains(text(), '{{filterName}}')]]//button[@data-action='close-advanced-select']" parameterized="true"/> + <element name="duplicatedFilterCheckbox" type="button" selector="//input[@name='duplicated']"/> + <element name="activeFilterValue" type="text" selector="//div[@class='media-gallery-container']//div[@class='admin__current-filters-list-wrap']//li//span[contains(text(), '{{filterPlaceholder}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml new file mode 100644 index 0000000000000..f36fca88dc760 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryImageActionsSection"> + <element name="openContextMenu" type="button" selector=".three-dots"/> + <element name="contextMenuItem" type="block" selector="//div[@class='media-gallery-image']//ul[@class='action-menu _active']//li//a[@class='action-menu-item']"/> + <element name="viewDetails" type="button" selector="[data-ui-id='action-image-details']"/> + <element name="delete" type="button" selector="[data-ui-id='action-delete']"/> + <element name="edit" type="button" selector="[data-ui-id='action-edit']"/> + <element name="imageInGrid" type="button" selector="//li[@data-ui-id='title'and text()='{{imageTitle}}']/parent::*/parent::*/parent::div//img[@class='media-gallery-image-column']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml new file mode 100644 index 0000000000000..32cd99bfe6b11 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.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="AdminEnhancedMediaGalleryImageDescriptionSection"> + <element name="title" type="text" selector=".masonry-image-description .name"/> + <element name="contentType" type="text" selector=".masonry-image-description .type"/> + <element name="dimensions" type="text" selector=".masonry-image-description .dimensions" /> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml new file mode 100644 index 0000000000000..07f2dc23530e1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryMassActionSection"> + <element name="massActionCheckbox" type="button" selector="//input[@type='checkbox'][@data-ui-id ='{{imageName}}']" parameterized="true"/> + <element name="totalSelected" type="text" selector=".mediagallery-massaction-items-count > .selected_count_text"/> + <element name="cancelMassActionMode" type="button" selector="#cancel_massaction"/> + <element name="deleteImages" type="button" selector="#delete_massaction"/> + <element name="deleteSelected" type="button" selector="#delete_selected_massaction"/> + </section> +</sections> 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 new file mode 100644 index 0000000000000..d6abe464048c7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml @@ -0,0 +1,28 @@ +<?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="AdminEnhancedMediaGalleryViewDetailsSection"> + <element name="title" type="text" selector=".image-title"/> + <element name="contentType" type="text" selector="[data-ui-id='content-type']"/> + <element name="type" type="text" selector="//div[@class='attribute']/span[contains(text(), 'Type')]/following-sibling::div"/> + <element name="height" type="text" selector="//div[@class='attribute']/span[contains(text(), 'Height')]/following-sibling::div"/> + <element name="description" type="text" selector=".image-details-section.description p"/> + <element name="keywords" type="text" selector="//div[@class='tags-list']"/> + <element name="filename" type="text" selector=".image-details-section.filename p"/> + <element name="edit" type="button" selector="//div[@class='media-gallery-image-details-modal']//button[contains(@class, 'edit')]"/> + <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"/> + <element name="usedInLink" type="button" parameterized="true" selector="//div[@class='attribute']/span[contains(text(), 'Used In')]/following-sibling::div/a[contains(text(), '{{entityName}}')]"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml new file mode 100644 index 0000000000000..2e6919f692042 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMediaGalleryFolderSection"> + <element name="folderNewModalHeader" type="block" selector="//h1[contains(text(), 'New Folder Name')]"/> + <element name="folderDeleteModalHeader" type="block" selector="//h1[contains(text(), 'Are you sure you want to delete this folder?')]"/> + <element name="folderNewCreateButton" type="button" selector="#create_folder"/> + <element name="folderDeleteButton" type="button" selector="#delete_folder"/> + <element name="folderConfirmDeleteButton" type="button" selector="//footer//button/span[contains(text(), 'OK')]"/> + <element name="folderCancelDeleteButton" type="button" selector="//footer//button/span[contains(text(), 'Cancel')]"/> + <element name="folderNameField" type="button" selector="[name=folder_name]"/> + <element name="folderConfirmCreateButton" type="button" selector="//button/span[contains(text(),'Confirm')]"/> + <element name="folderNameValidationMessage" type="block" selector="label.mage-error"/> + <element name="folderArrow" type="button" selector="#{{id}} > .jstree-icon" parameterized="true"/> + <element name="checkIfFolderArrowExpand" type="button" selector="//li[@id='{{id}}' and contains(@class,'jstree-closed')]" parameterized="true"/> + </section> +</sections> 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/Section/AdminMediaGalleryHeaderButtonsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.xml new file mode 100644 index 0000000000000..9271c0ff61618 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.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="AdminMediaGalleryHeaderButtonsSection"> + <element name="addSelected" type="button" selector=".media-gallery-add-selected"/> + <element name="deleteSelected" type="button" selector="#delete_selected"/> + </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/Suite/MediaGalleryUiSuite.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml new file mode 100644 index 0000000000000..4749fc4a885b0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml @@ -0,0 +1,29 @@ +<?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="MediaGalleryUiSuite"> + <before> + <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYG" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="enableEnhancedMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="disableEnhancedMediaGallery"/> + <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYG" /> + </after> + <include> + <group name="media_gallery_ui"/> + </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 new file mode 100644 index 0000000000000..fe2b5b1639fbe --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryDeleteImagesInBulkTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1488"/> + <title value="User deletes images with less clicks"/> + <stories value="[Story #42] User deletes images in bulk"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="User deletes images with less clicks"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <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> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToVerifyMode"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup" stepKey="assertMassActionModeAvailable"> + <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}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + <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/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml new file mode 100644 index 0000000000000..52f3a8079e962 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryDuplicatedImagesTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1500"/> + <title value="User can filter duplicated images"/> + <stories value="[Story 59] User finds image duplicates"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="User can filter duplicated images"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + </after> + + <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="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup" stepKey="SelectDuplicatedFilter"/> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertFirstImageInGrid"> + <argument name="title" value="ImageUpload.filename"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertSecondImageInGrid"> + <argument name="title" value="ImageUpload_1.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml new file mode 100644 index 0000000000000..f026b87f7ec88 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryUploadImageWithMetadataTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="Magento extracts image meta data from file"/> + <stories value="Story 53 - Magento extracts image meta data from file"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4653671"/> + <description value="Magento extracts image meta data from file"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteJpegImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadPngImage"> + <argument name="image" value="ImageUploadPng"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewPngImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyPngImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyPngImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyPngImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deletePngImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadGifImage"> + <argument name="image" value="ImageUploadGif"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewGifImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyGifImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyGifImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyGifImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteGifImage"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml new file mode 100644 index 0000000000000..9a08f7cd0bb9c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.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="AdminEnhancedMediaGalleryVerifyAssetFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1292"/> + <title value="User sees entities where asset is used in"/> + <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/4951024"/> + <description value="User sees entities where asset is used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstIMage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <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="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="AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup" stepKey="assertCategoryGridPageProductsInMenuEnabledColumns"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="secondResetAdminDataGridToDefaultView"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml new file mode 100644 index 0000000000000..4719b98c78dbe --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1489"/> + <title value="User filters images that are not used in the content"/> + <stories value="Story 52: User filters images that are not used in the content"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4930844"/> + <description value="User filters images that are not used in the content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </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"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToFilterImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplyUsedInFilterActionGroup" stepKey="applyUsedInCategoryFilter"> + <argument name="entityType" value="Not used anywhere"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup" stepKey="assertImageNotExistsInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml new file mode 100644 index 0000000000000..f47d6d9202c05 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.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="AdminEnhancedMediaGalleryVerifyUpdatedTagsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1703"/> + <title value="User checks if the deleted tags are removed from Edit page, Tags field"/> + <stories value="User checks if the deleted tags are removed from Edit page, Tags field"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/5064888"/> + <description value="User checks if changes made on the tags are updated from Edit page, Tags field"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminMediaGalleryEditAssetAddKeywordActionGroup" stepKey="setKeywords"> + <argument name="keyword" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyAddedKeywords"> + <argument name="keywords" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="updateImageDetails"/> + <actionGroup ref="AdminMediaGalleryEditAssetRemoveKeywordActionGroup" stepKey="removeKeywords"> + <argument name="keyword" value="{{UpdatedImageDetails.keyword}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveUpdatedImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup" stepKey="verifyRemovedKeywords"> + <argument name="keywords" value="{{UpdatedImageDetails.keyword}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml new file mode 100644 index 0000000000000..d54399bdeb2b2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryVerifyUsedInFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1567"/> + <title value="User filters images by the area they used in"/> + <stories value="User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4930844"/> + <description value="User filters images by the area they used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstIMage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </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="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToFilterIMage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplyUsedInFilterActionGroup" stepKey="applyUsedInCategoryFilter"> + <argument name="entityType" value="Categories"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup" stepKey="assertImageNotExistsInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml new file mode 100644 index 0000000000000..cb7adf3307865 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.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="AdminMediaGalleryAddCategoryImageFromTwoComponentsTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="User add category image via wysiwyg and image uploader button"/> + <stories value="Story [54]: User inserts image rendition to the content with text area + Insert image button" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <description value="User add category image via wysiwyg and image uploader button"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <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"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadContentImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadCategoryImage"> + <argument name="image" value="ImageUpload_1"/> + </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"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedCategoryImage"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="reSaveCategory"/> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertContentImageIsVisible"> + <argument name="imageName" value="{{ImageUpload3.fileName}}"/> + </actionGroup> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertCategoryImageIsVisible"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml new file mode 100644 index 0000000000000..30f1412a5b08d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryAddCategoryImageTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1073"/> + <title value="User add category image via wysiwyg"/> + <stories value="User add category image via wysiwyg"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4484351"/> + <description value="User add category image via wysiwyg"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertImageInCategoryDescriptionField"> + <argument name="imageName" value="{{ImageUpload3.fileName}}" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml new file mode 100644 index 0000000000000..94307fa510a50 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.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="AdminMediaGalleryAddFromImageDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1229"/> + <stories value="[Story #38] User views basic image attributes in Media Gallery"/> + <title value="Adding image from the Image Details"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4569982"/> + <description value="Adding image from the Image Details"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup" stepKey="addImageFromViewDetails"/> + <actionGroup ref="AssertImageAddedToPageContentActionGroup" stepKey="assertImageAddedToContent"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml new file mode 100644 index 0000000000000..6e6f5240e84be --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCreateDeleteFolderTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1046; https://github.com/magento/adobe-stock-integration/issues/1047"/> + <stories value="Creating, deleting new folder functionality in Media Gallery"/> + <title value="Creating, deleting new folder functionality in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4456547; https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4457075"/> + <description value="Creating, deleting new folder functionality in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolderWithNotValidName"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.name}}"/> + </actionGroup> + + <grabTextFrom selector="{{AdminMediaGalleryFolderSection.folderNameValidationMessage}}" stepKey="grabValidationMessage"/> + <assertStringContainsString stepKey="assertFirst"> + <actualResult type="variable">grabValidationMessage</actualResult> + <expectedResult type="string">Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.</expectedResult> + </assertStringContainsString> + + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="deleteFolderButtonIsNotDisabled"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="unselectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}, :disabled" stepKey="deleteFolderButtonIsDisabledAgain"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"/> + + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="waitBeforeModalLoads"/> + <click selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="cancelDeleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFolderWasNotDeleted"/> + + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + </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/AdminMediaGalleryDefaultViewWithoutFiltersTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDefaultViewWithoutFiltersTest.xml new file mode 100644 index 0000000000000..7c2087247c574 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDefaultViewWithoutFiltersTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryDefaultViewWithoutFiltersTest"> + <annotations> + <features value="AdminMediaGalleryDefaultViewWithoutFiltersTest"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1813"/> + <title value="User shouldn't see applied filters if media gallery switched to Default View"/> + <stories value="Media gallery default directory"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/5199870"/> + <description value="No filters should be applied if Default View selected"/> + <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> + + <!-- Open category page --> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"> + <argument name="categoryName" value="$category.name$"/> + </actionGroup> + + <!-- Open media gallery folder --> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup" stepKey="selectDefaultView"> + <argument name="selectView" value="Default View"/> + </actionGroup> + + <!-- Asset folder is empty --> + <actionGroup ref="AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup" stepKey="assertEmptyFolder"/> + </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/AdminMediaGalleryDeleteImageContextMenuTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml new file mode 100644 index 0000000000000..980d6b7c85c20 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryDeleteImageContextMenuTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/710"/> + <title value="Uploading and deleting an image using context menu"/> + <stories value="[Story #52] User accesses Media Gallery from the main navigation"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="Uploading and deleting an image using context menu"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="assertImageDeleted"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml new file mode 100644 index 0000000000000..ad364e7709a33 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.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="AdminMediaGalleryDeleteImageFileTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1094"/> + <title value="Deleting new image file functionality in Enhanced Media Gallery"/> + <stories value="Deleting new image file functionality in Enhanced Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4756652"/> + <description value="Deleting new image file functionality in Enhanced Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="verifyImageIsDeleted"> + <argument name="title" value="ImageUpload.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml new file mode 100644 index 0000000000000..6ae8ed7047434 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.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="AdminMediaGalleryDeleteImageWithWarningPopupTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1511"/> + <title value="User sees warning when deleting image if it's used on storefront"/> + <stories value="User sees warning when deleting image if it's used on storefront"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4843896"/> + <description value="User sees warning when deleting image if it's used on storefront"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadCategoryImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToAssertMessage"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertWarningMessageActionGroup" stepKey="assertMessageImageUsedIn"> + <argument name="messageText" value="The selected assets are used in the content of the following entities: Categories(1)"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml new file mode 100644 index 0000000000000..5926b115afccf --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="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"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970565"/> + <description value="User filter asset by disabled content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <actionGroup ref="AdminEnableCategoryActionGroup" stepKey="disableCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Content Status"/> + <argument name="optionLabel" value="Disabled"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clickDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml new file mode 100644 index 0000000000000..960443998d010 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryEditImageDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <title value="User edits image meta data in media gallery"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml new file mode 100644 index 0000000000000..c2b167912dda7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryEnabledContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by enabled content"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970565"/> + <description value="User filter asset by enabled content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Content Status"/> + <argument name="optionLabel" value="Enabled"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml new file mode 100644 index 0000000000000..4369a61708a83 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryFilterImagesBySourceTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1393"/> + <title value="User filters images by source filter"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4760144"/> + <description value="User filters images by source filter"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewContentImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteContentImage"/> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadContentImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectSourceFilterActionGroup" stepKey="applyLocalFilter"> + <argument name="filterValue" value="Local"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml new file mode 100644 index 0000000000000..b8ce1f76ad4c8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGallerySaveFiltersStateTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1397"/> + <title value="User is able to use bookmarks controls for filter views in Standalone Media Gallery"/> + <stories value="User is able to use bookmarks controls in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4763040"/> + <description value="User is able to use bookmarks controls for filter views in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectSourceFilterActionGroup" stepKey="applyLocalFilter"> + <argument name="filterValue" value="Local"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySaveCustomViewActionGroup" stepKey="saveCustomView"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup" stepKey="assertFilterApplied"> + <argument name="resultValue" value="Uploaded Locally"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup" stepKey="selectDefaultView"> + <argument name="selectView" value="Default View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup" stepKey="assertNoActiveFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeleteGridViewActionGroup" stepKey="deleteView"> + <argument name="viewToDelete" value="Test View"/> + </actionGroup> + </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..365252095d49e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByDirectoryAscendingTest.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="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="selectParentFolderForDelete"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteParentFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertParentFolderWasDeleted"> + <argument name="name" value="parentFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openParentFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createParentFolder"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertParentFolderCreated"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoadAfterParentFolderCreated"/> + <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="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <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..85c468996d515 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByDirectoryDescendingTest.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="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="selectParentFolderForDelete"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteParentFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertParentFolderWasDeleted"> + <argument name="name" value="parentFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openParentFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createParentFolder"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertParentFolderCreated"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoadAfterParentFolderCreated"/> + <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="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <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..9dca51065124f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNameAToZTest.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="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="selectParentFolderForDelete"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteParentFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertParentFolderWasDeleted"> + <argument name="name" value="parentFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openParentFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createParentFolder"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertParentFolderCreated"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoadAfterParentFolderCreated"/> + <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="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <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..71d2020d08658 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNameZToATest.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="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="selectParentFolderForDelete"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteParentFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertParentFolderWasDeleted"> + <argument name="name" value="parentFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openParentFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createParentFolder"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertParentFolderCreated"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoadAfterParentFolderCreated"/> + <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="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <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..3da0546db090a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNewestFirstTest.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="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="selectParentFolderForDelete"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteParentFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertParentFolderWasDeleted"> + <argument name="name" value="parentFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openParentFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createParentFolder"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertParentFolderCreated"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoadAfterParentFolderCreated"/> + <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="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <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..e6191d2d02287 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByOldestFirstTest.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="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="selectParentFolderForDelete"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteParentFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertParentFolderWasDeleted"> + <argument name="name" value="parentFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openParentFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createParentFolder"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertParentFolderCreated"> + <argument name="name" value="parentFolder"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoadAfterParentFolderCreated"/> + <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="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <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/AdminMediaGalleryStoreViewCategoryFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml new file mode 100644 index 0000000000000..eceda879e5597 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryStoreViewCategoryFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by category store view"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970870"/> + <description value="User filter asset by category store view"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <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="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Store View"/> + <argument name="optionLabel" value="Main Website/Main Website Store/Default Store View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <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/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml new file mode 100644 index 0000000000000..86cae11267eaa --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryStoreViewContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by content store view"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970870"/> + <description value="User filter asset by content store view"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="createCMSPage" stepKey="deleteCmsPage"/> + </after> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSavePage"/> + <waitForPageLoad stepKey="waitForPageSave"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Store View"/> + <argument name="optionLabel" value="Main Website/Main Website Store/Default Store View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <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/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 new file mode 100644 index 0000000000000..fa43e4e17d406 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.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="AdminMediaGalleryUploadCategoryImageTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1435"/> + <stories value="User uploads image outside of the Media Gallery"/> + <title value="User uploads image outside of the Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4836631"/> + <description value="User uploads image outside of the Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewContentImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteCategoryImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> + <actionGroup ref="AdminMediaGalleryExpandFolderActionGroup" stepKey="expandCatalogFolder"> + <argument name="fieldId" 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"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml new file mode 100644 index 0000000000000..01a26cce1b6fb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.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="AdminMediaGalleryVerifyImageGridAttributesTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/708"/> + <title value="Verify image grid attributes"/> + <stories value="[Story #41] User views limited image information from the image grid in Media Gallery" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/3839218"/> + <description value="User views basic image attributes in Media Gallery grid"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="assertImageAttributes"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml new file mode 100644 index 0000000000000..00fc07eb6c1af --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.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="AdminMediaGalleryViewDetailsDeleteImageTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/461"/> + <title value="Deleting an image from view details panel"/> + <stories value="[Story #42] User deletes images"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4516773"/> + <description value="Deleting an image from view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageMetadata"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="assertImageDeleted"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml new file mode 100644 index 0000000000000..92909bcf06795 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryViewDetailsEditTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1581"/> + <title value="Editing an image from view details panel"/> + <stories value="[Story #44] User edits image meta data in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="Editing an image from view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml new file mode 100644 index 0000000000000..c9447d5cc8a52 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryViewDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="View image details"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4653671"/> + <description value="User views basic image attributes in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup" stepKey="verifyFilename"> + <argument name="filename" value="{{ImageUpload.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml new file mode 100644 index 0000000000000..164ab523d508a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.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="AdminStandaloneMediaGalleryCreateDeleteFolderTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1119; https://github.com/magento/adobe-stock-integration/issues/1120"/> + <stories value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <title value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503041; https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503101"/> + <description value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolderWithNotValidName"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.name}}"/> + </actionGroup> + + <grabTextFrom selector="{{AdminMediaGalleryFolderSection.folderNameValidationMessage}}" stepKey="grabValidationMessage"/> + <assertStringContainsString stepKey="assertFirst"> + <actualResult type="variable">grabValidationMessage</actualResult> + <expectedResult type="string">Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.</expectedResult> + </assertStringContainsString> + + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="deleteFolderButtonIsNotDisabled"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="unselectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}, :disabled" stepKey="deleteFolderButtonIsDisabledAgain"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"/> + + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="waitBeforeModalLoads"/> + <click selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="cancelDeleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFolderWasNotDeleted"/> + + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + </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/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml new file mode 100644 index 0000000000000..58c6f32b8d72f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.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="AdminStandaloneMediaGalleryEditImageDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <title value="User edits image meta data in standalone media gallery"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="clickViewDetails"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup" stepKey="verifyCreatedAndUpdatedAtDate" /> + <wait time="100" stepKey="waitForUpdateTimeToBeGreater"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup" stepKey="assertUpdatedAtTimeChanged" /> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml new file mode 100644 index 0000000000000..2cf6bf5dfe623 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStandaloneMediaGalleryViewDetailsEditTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1581"/> + <title value="Editing an image from standalone view details panel"/> + <stories value="[Story #44] User edits image meta data in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="Editing an image from standalone view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminMediaGalleryEditAssetAddKeywordActionGroup" stepKey="setKeywords"> + <argument name="keyword" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyAddedKeywords"> + <argument name="keywords" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyMetadataKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml new file mode 100644 index 0000000000000..bb7071497ce24 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStandaloneMediaGalleryViewDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="View image details in standalone media gallery"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503223"/> + <description value="User views basic image attributes in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup" stepKey="verifyFilename"> + <argument name="filename" value="{{ImageUpload.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php new file mode 100644 index 0000000000000..3d4e523d0d6b1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Unit\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGalleryUi\Model\Config; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Config data test. + */ +class ConfigTest extends TestCase +{ + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var Config + */ + private $config; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * Prepare test objects. + */ + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->config = $this->objectManager->getObject( + Config::class, + [ + 'scopeConfig' => $this->scopeConfigMock + ] + ); + } + + /** + * Get Magento media gallery enabled test. + */ + public function testIsEnabled(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('isSetFlag') + ->with(self::XML_PATH_ENABLED) + ->willReturn(true); + $this->assertEquals(true, $this->config->isEnabled()); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php new file mode 100644 index 0000000000000..fc8a0756a7b55 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Unit\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGalleryUi\Model\UploadImage; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Provides test for upload image functionality + */ +class UploadImageTest extends TestCase +{ + /** + * @var Storage|MockObject + */ + private $imagesStorageMock; + + /** + * @var Filesystem|MockObject + */ + private $fileSystemMock; + + /** + * @var Read|MockObject + */ + private $mediaDirectoryMock; + + /** + * @var UploadImage + */ + private $uploadImage; + + /** + * Prepare test objects. + */ + protected function setUp(): void + { + $this->imagesStorageMock = $this->createMock(Storage::class); + $this->fileSystemMock = $this->createMock(Filesystem::class); + $this->mediaDirectoryMock = $this->createMock(Read::class); + + $this->uploadImage = (new ObjectManager($this))->getObject( + UploadImage::class, + [ + 'imagesStorage' => $this->imagesStorageMock, + 'filesystem' => $this->fileSystemMock, + ] + ); + } + + /** + * Test successful image file upload. + * + * @param string $targetFolder + * @param string|null $type + * @param string $absolutePath + * + * @dataProvider executeDataProvider + */ + public function testExecute(string $targetFolder, string $type = null, string $absolutePath): void + { + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::MEDIA) + ->willReturn($this->mediaDirectoryMock); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('isDirectory') + ->with($targetFolder) + ->willReturn(true); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->with($targetFolder) + ->willReturn($absolutePath); + + $uploadResult = ['path' => 'media/catalog', 'file' => 'test-image.jpeg']; + $this->imagesStorageMock->expects($this->once()) + ->method('uploadFile') + ->with($absolutePath, $type) + ->willReturn($uploadResult); + + $this->uploadImage->execute($targetFolder, $type); + } + + /** + * Test upload image method with logical exception when the folder is not a folder. + */ + public function testExecuteWithException(): void + { + $targetFolder = 'not-a-folder'; + $type = 'image'; + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::MEDIA) + ->willReturn($this->mediaDirectoryMock); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('isDirectory') + ->with($targetFolder) + ->willReturn(false); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('Directory not-a-folder does not exist in media directory.'); + + $this->uploadImage->execute($targetFolder, $type); + } + + /** + * Provides test case data. + * + * @return array + */ + public function executeDataProvider(): array + { + return [ + [ + 'targetFolder' => 'media/catalog', + 'type' => 'image', + 'absolutePath' => 'root/pub/media/catalog/test-image.jpeg' + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg new file mode 100644 index 0000000000000..5244f8dc420e1 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg differ diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg new file mode 100644 index 0000000000000..5244f8dc420e1 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg differ 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/DirectoryTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php new file mode 100644 index 0000000000000..0ad5ad43f6157 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Component\Container; +use Magento\Framework\AuthorizationInterface; + +/** + * Directories tree component + */ +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; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + '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/ImageUploader.php b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php new file mode 100644 index 0000000000000..ad5e27381dee2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +use Magento\Framework\File\Size; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Component\Container; + +/** + * Image Uploader component + */ +class ImageUploader extends Container +{ + private const ACCEPT_FILE_TYPES = '/(\.|\/)(gif|jpe?g|png)$/i'; + private const ALLOWED_EXTENSIONS = 'jpg jpeg png gif'; + + /** + * @var UrlInterface + */ + private $url; + + /** + * @var Size + */ + private $size; + + /** + * @param Size $size + * @param ContextInterface $context + * @param UrlInterface $url + * @param array $components + * @param array $data + */ + public function __construct( + Size $size, + ContextInterface $context, + UrlInterface $url, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $components, $data); + $this->size = $size; + $this->url = $url; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'imageUploadUrl' => $this->url->getUrl('media_gallery/image/upload', ['type' => 'image']), + 'acceptFileTypes' => self::ACCEPT_FILE_TYPES, + 'allowedExtensions' => self::ALLOWED_EXTENSIONS, + 'maxFileSize' => $this->size->getMaxFileSize() + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php new file mode 100644 index 0000000000000..1fc5a80960a69 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +/** + * Image Uploader component + */ +class ImageUploaderStandAlone extends ImageUploader +{ + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'actionsPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing' . + '.media_gallery_columns.thumbnail_url', + 'directoriesPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing' . + '.media_gallery_directories', + 'messagesPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing.messages' + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Source/Options.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Source/Options.php new file mode 100644 index 0000000000000..b56848aa3515c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Source/Options.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Source; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Image source filter options + */ +class Options implements OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => 'Local', + 'label' => __('Uploaded Locally'), + ], + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php new file mode 100644 index 0000000000000..e425c9488b5c2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Columns; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Asset\Repository as AssetRepository; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\Store; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Source icon url provider + */ +class SourceIconProvider extends Column +{ + /** + * @var array + */ + private $sourceIcons; + + /** + * @var AssetRepository + */ + private $assetRepository; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param AssetRepository $assetRepository + * @param ScopeConfigInterface $scopeConfig + * @param array $components + * @param array $data + * @param array $sourceIcons + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + AssetRepository $assetRepository, + ScopeConfigInterface $scopeConfig, + array $components = [], + array $data = [], + array $sourceIcons = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->assetRepository = $assetRepository; + $this->scopeConfig = $scopeConfig; + $this->sourceIcons = $sourceIcons; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource): array + { + if (isset($dataSource['data']['items']) && is_iterable($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as &$item) { + $item[$this->getData('name')] = $item[$this->getData('name')] + ? $this->getSourceIconUrl($item[$this->getData('name')]) + : null; + } + } + + return $dataSource; + } + + /** + * Construct source icon url based on the source code matching + * + * @param string $sourceName + * + * @return string|null + */ + private function getSourceIconUrl(string $sourceName): ?string + { + return isset($this->sourceIcons[$sourceName]) + ? $this->assetRepository->getUrlWithParams( + $this->sourceIcons[$sourceName], + ['_secure' => $this->isSecure()] + ) + : null; + } + + /** + * Check if store use secure connection + * + * @return bool + */ + private function isSecure(): bool + { + return $this->scopeConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php new file mode 100644 index 0000000000000..a55c6397a31fa --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Columns; + +use Magento\Backend\Model\UrlInterface; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +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 + */ + private $storeManager; + + /** + * UrlInterface $urlInterface + */ + private $urlInterface; + + /** + * @var Images + */ + private $images; + + /** + * @var Storage + */ + private $storage; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param StoreManagerInterface $storeManager + * @param UrlInterface $urlInterface + * @param Images $images + * @param Storage $storage + * @param AuthorizationInterface $authorization + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + StoreManagerInterface $storeManager, + UrlInterface $urlInterface, + Images $images, + Storage $storage, + AuthorizationInterface $authorization, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->storeManager = $storeManager; + $this->urlInterface = $urlInterface; + $this->images = $images; + $this->storage = $storage; + $this->authorization = $authorization; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + * @throws NoSuchEntityException + */ + public function prepareDataSource(array $dataSource): array + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as & $item) { + $item['encoded_id'] = $this->images->idEncode($item['path']); + $item[$this->getData('name')] = $this->getUrl($item[$this->getData('name')]); + } + } + + return $dataSource; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array)$this->getData('config'), + [ + 'allowedActions' => $this->getAllowedActions(), + 'onInsertUrl' => $this->urlInterface->getUrl('media_gallery/image/oninsert'), + 'storeId' => $this->storeManager->getStore()->getId(), + ] + ) + ); + } + + /** + * 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 + * + * @param string $path + * @return string + * @throws NoSuchEntityException + */ + private function getUrl(string $path): string + { + return $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $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 new file mode 100644 index 0000000000000..f61e34512bfe3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\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\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Ui\Component\Filters\Type\Select; + +/** + * Asset filter + */ +class Asset extends Select +{ + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * Constructor + * + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param FilterBuilder $filterBuilder + * @param FilterModifier $filterModifier + * @param OptionSourceInterface $optionsProvider + * @param GetContentByAssetIdsInterface $getContentIdentities + * @param array $components + * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + FilterBuilder $filterBuilder, + FilterModifier $filterModifier, + OptionSourceInterface $optionsProvider = null, + GetContentByAssetIdsInterface $getContentIdentities, + array $components = [], + array $data = [] + ) { + $this->uiComponentFactory = $uiComponentFactory; + $this->filterBuilder = $filterBuilder; + parent::__construct( + $context, + $uiComponentFactory, + $filterBuilder, + $filterModifier, + $optionsProvider, + $components, + $data + ); + $this->getContentIdentities = $getContentIdentities; + } + + /** + * Apply filter + * + * @return void + */ + public function applyFilter() + { + if (!isset($this->filterData[$this->getName()])) { + return; + } + + $assetIds = $this->filterData[$this->getName()]; + if (!is_array($assetIds)) { + $assetIds = explode(',', str_replace(['[', ']'], '', $assetIds)); + } + + $filter = $this->filterBuilder->setConditionType('in') + ->setField($this->_data['config']['identityColumn']) + ->setValue($this->getEntityIdsByAsset($assetIds)) + ->create(); + $this->getContext()->getDataProvider()->addFilter($filter); + } + + /** + * Return entity ids by assets ids. + * + * @param array $ids + */ + private function getEntityIdsByAsset(array $ids): string + { + if (!empty($ids)) { + $categoryIds = []; + $data = $this->getContentIdentities->execute($ids); + foreach ($data as $identity) { + if ($identity->getEntityType() === $this->_data['config']['entityType']) { + $categoryIds[] = $identity->getEntityId(); + } + } + return implode(',', $categoryIds); + } + return ''; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php new file mode 100644 index 0000000000000..31c658a6c4208 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Status filter options + */ +class Status implements OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + ['value' => '1', 'label' => __('Enabled')], + ['value' => '0', 'label' => __('Disabled')] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php new file mode 100644 index 0000000000000..f60124a6cf933 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Store\Ui\Component\Listing\Column\Store\Options as StoreOptions; + +/** + * Store Options for content field + */ +class Store extends StoreOptions +{ + /** + * All Store Views value + */ + private const ALL_STORE_VIEWS = '0'; + + /** + * Get options + * + * @return array + */ + public function toOptionArray() + { + if ($this->options !== null) { + return $this->options; + } + + $this->currentOptions['All Store Views']['label'] = __('All Store Views'); + $this->currentOptions['All Store Views']['value'] = self::ALL_STORE_VIEWS; + + $this->generateCurrentOptions(); + + $this->options = array_values($this->currentOptions); + + return $this->options; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php new file mode 100644 index 0000000000000..e638fb7e86625 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Used in filter options + */ +class UsedIn implements OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + 'cms_page' => [ + 'value' => 'cms_page', + 'label' => 'Pages' + ], + 'catalog_category' => [ + 'value' => 'catalog_category', + 'label' => 'Categories' + ], + 'cms_block' => [ + 'value' => 'cms_block', + 'label' => 'Blocks' + ], + 'catalog_product' => [ + 'value' => 'catalog_product', + 'label' => 'Products' + ], + 'not_used' => [ + 'value' => 'not_used', + 'label' => 'Not used anywhere' + ] + ]; + } +} 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/Ui/Component/Listing/Provider.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php new file mode 100644 index 0000000000000..160097967165d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\MediaGalleryUi\Ui\Component\Listing; + +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy; +use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory; +use Magento\Framework\Event\ManagerInterface as EventManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Psr\Log\LoggerInterface as Logger; + +class Provider extends SearchResult +{ + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @param EntityFactory $entityFactory + * @param Logger $logger + * @param FetchStrategy $fetchStrategy + * @param EventManager $eventManager + * @param GetAssetsKeywordsInterface $getAssetKeywords + * @param string $mainTable + * @param null|string $resourceModel + * @param null|string $identifierName + * @param null|string $connectionName + * @throws LocalizedException + */ + public function __construct( + EntityFactory $entityFactory, + Logger $logger, + FetchStrategy $fetchStrategy, + EventManager $eventManager, + GetAssetsKeywordsInterface $getAssetKeywords, + $mainTable = 'media_gallery_asset', + $resourceModel = null, + $identifierName = null, + $connectionName = null + ) { + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $mainTable, + $resourceModel, + $identifierName, + $connectionName + ); + $this->getAssetKeywords = $getAssetKeywords; + } + + /** + * @inheritdoc + */ + public function getData() + { + $data = parent::getData(); + $keywords = []; + foreach ($this->_items as $asset) { + $keywords[$asset->getId()] = array_map(function (AssetKeywordsInterface $assetKeywords) { + return array_map(function (KeywordInterface $keyword) { + return $keyword->getKeyword(); + }, $assetKeywords->getKeywords()); + }, $this->getAssetKeywords->execute([$asset->getId()])); + } + + /** @var AssetInterface $asset */ + foreach ($data as $key => $asset) { + $data[$key]['thumbnail_url'] = $asset['path']; + $data[$key]['content_type'] = strtoupper(str_replace('image/', '', $asset['content_type'])); + $data[$key]['preview_url'] = $asset['path']; + $data[$key]['keywords'] = isset($keywords[$asset['id']]) ? implode(",", $keywords[$asset['id']]) : ''; + $data[$key]['source'] = empty($asset['source']) ? __('Local') : $asset['source']; + } + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json new file mode 100644 index 0000000000000..204e0b37c3bf8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -0,0 +1,32 @@ +{ + "name": "magento/module-media-gallery-ui", + "description": "Magento module responsible for the media gallery UI implementation", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-ui": "*", + "magento/module-store": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-metadata-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*", + "magento/module-cms": "*", + "magento/module-directory": "*", + "magento/module-authorization": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..552c5364f3500 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml @@ -0,0 +1,44 @@ +<?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\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField"> + <arguments> + <argument name="getAssetIdsByContentStatus" xsi:type="object">Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface</argument> + </arguments> + </type> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="path" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Directory</item> + <item name="fulltext" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Keyword</item> + <item name="entity_type" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\EntityType</item> + <item name="duplicated" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Duplicated</item> + <item name="content_status" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField</item> + <item name="store_id" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\SortingProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\SortingProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\PaginationProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\PaginationProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\JoinProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\JoinProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="filters" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor</item> + <item name="sorting" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\SortingProcessor</item> + <item name="pagination" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\PaginationProcessor</item> + <item name="joins" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\JoinProcessor</item> + </argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\Listing\DataProvider"> + <arguments> + <argument name="collectionProcessor" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor</argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml new file mode 100644 index 0000000000000..92839aa75ac8b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> + <menu> + <add id="Magento_MediaGalleryUi::media" title="Media" translate="title" module="Magento_MediaGalleryUi" sortOrder="15" parent="Magento_Backend::content" resource="Magento_Cms::media_gallery" dependsOnConfig="system/media_gallery/enabled"/> + <add id="Magento_MediaGalleryUi::media_gallery" title="Media Gallery" translate="title" module="Magento_MediaGalleryUi" sortOrder="0" parent="Magento_MediaGalleryUi::media" action="media_gallery/media/index" resource="Magento_Cms::media_gallery"/> + </menu> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..11a555e16e957 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery" frontName="media_gallery"> + <module name="Magento_MediaGalleryUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..17aa08b5363ca --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_gallery" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Media Gallery</label> + <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable Old Media Gallery</label> + <source_model>Magento\MediaGalleryUi\Model\Config\MediaGallery\Yesno</source_model> + <config_path>system/media_gallery/enabled</config_path> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/config.xml b/app/code/Magento/MediaGalleryUi/etc/config.xml new file mode 100644 index 0000000000000..fe8e73c406e59 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/config.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <media_gallery> + <enabled>0</enabled> + </media_gallery> + </system> + </default> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/di.xml b/app/code/Magento/MediaGalleryUi/etc/di.xml new file mode 100644 index 0000000000000..964ac92399738 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/di.xml @@ -0,0 +1,35 @@ +<?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\MediaGalleryUiApi\Api\ConfigInterface" type="Magento\MediaGalleryUi\Model\Config"/> + <preference for="Magento\MediaGalleryUiApi\Api\Data\InsertImageDataInterface" type="\Magento\MediaGalleryUi\Model\InsertImageData"/> + <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> + <arguments> + <argument name="collections" xsi:type="array"> + <item name="media_gallery_listing_data_source" xsi:type="string">Magento\MediaGalleryUi\Ui\Component\Listing\Provider</item> + </argument> + </arguments> + </type> + <virtualType name="mediaGallerySearchResult" type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult"> + <arguments> + <argument name="mainTable" xsi:type="string">media_gallery_asset_grid</argument> + <argument name="resourceModel" xsi:type="string">Magento\MediaGalleryUi\Model\ResourceModel\Grid\Asset</argument> + </arguments> + </virtualType> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <arguments> + <argument name="resizeParameters" xsi:type="array"> + <item name="height" xsi:type="number">200</item> + <item name="width" xsi:type="number">200</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> + <plugin name="createMediaGalleryThumbnails" type="Magento\MediaGalleryUi\Plugin\CreateThumbnails"/> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/module.xml b/app/code/Magento/MediaGalleryUi/etc/module.xml new file mode 100644 index 0000000000000..0deede3e6aad0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryUi"> + <sequence> + <module name="Magento_Cms" /> + </sequence> + </module> +</config> diff --git a/app/code/Magento/MediaGalleryUi/i18n/en_US.csv b/app/code/Magento/MediaGalleryUi/i18n/en_US.csv new file mode 100644 index 0000000000000..1882665ce8033 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/i18n/en_US.csv @@ -0,0 +1,8 @@ +"Enhanced Media Gallery","Enhanced Media Gallery" +Enabled,Enabled +All,All +Directory,Directory +"Uploaded Date","Uploaded Date" +"Modification Date","Modification Date" +Overlay,Overlay +"Thumbnail Image","Thumbnail Image" diff --git a/app/code/Magento/MediaGalleryUi/registration.php b/app/code/Magento/MediaGalleryUi/registration.php new file mode 100644 index 0000000000000..e1d321c5a8ff3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryUi', __DIR__); 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 new file mode 100644 index 0000000000000..a5eb247bd344f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <container name="root"> + <block name="media.gallery.container" + class="Magento\Backend\Block\Template" + template="Magento_MediaGalleryUi::container.phtml" + aclResource="Magento_Cms::media_gallery"> + <container name="gallery.actions" htmlTag="div" htmlClass="page-main-actions"> + <block name="page.actions.toolbar" template="Magento_Backend::pageactions.phtml"/> + </container> + <uiComponent name="media_gallery_listing"/> + <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> + </block> + <block name="image.edit.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_edit_details.phtml"> + <arguments> + <argument name="imageEditDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + <argument name="saveDetailsUrl" xsi:type="url" path="media_gallery/image/saveDetails"/> + </arguments> + </block> + </block> + </container> +</layout> 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 new file mode 100644 index 0000000000000..b4f377627c850 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer htmlTag="div" htmlClass="media-gallery-container" name="content"> + <uiComponent name="standalone_media_gallery_listing"/> + <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> + </block> + <block name="image.edit.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_edit_details_standalone.phtml"> + <arguments> + <argument name="imageEditDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + <argument name="saveDetailsUrl" xsi:type="url" path="media_gallery/image/saveDetails"/> + </arguments> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml new file mode 100644 index 0000000000000..5b905ea97d64a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength + +?> + +<div class="media-gallery-container"> + <?= $block->getChildHtml(); ?> +</div> + +<script type="text/x-magento-init"> + { + ".media-gallery-container": { + "Magento_Ui/js/core/app": { + "components": { + "media_gallery_container": { + "component": "Magento_MediaGalleryUi/js/container", + "containerSelector": ".media-gallery-container", + "masonryComponentPath": "media_gallery_listing.media_gallery_listing.media_gallery_columns" + } + } + } + } + } +</script> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml new file mode 100644 index 0000000000000..783ff5a9c05bd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails; +use Magento\Framework\Escaper; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var ImageDetails $block */ +/** @var Escaper $escaper */ + +?> + +<div class="media-gallery-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Image Details')); ?>' + } + }"> + <div class="page-main-actions" data-bind="scope: 'mediaGalleryImageActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-details-messages" data-bind="scope: 'mediaGalleryImageDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-details" data-bind="scope: 'mediaGalleryImageDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-details", + "imageDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageDetailsUrl')); ?>", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGridMessages": "media_gallery_listing.media_gallery_listing.messages" + }, + "mediaGalleryImageDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + }, + "mediaGalleryImageActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", + "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", + "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 new file mode 100644 index 0000000000000..a4a096939eea4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var \Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Image Details')); ?>' + } + }"> + <div class="page-main-actions" data-bind="scope: 'mediaGalleryImageActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-details-messages" data-bind="scope: 'mediaGalleryImageDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-details" data-bind="scope: 'mediaGalleryImageDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-details", + "imageDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageDetailsUrl')); ?>", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + }, + "mediaGalleryImageActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", + "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": <?= /* @noEscape */ $block->getActionsJson() ?> + }, + "mediaGalleryImageDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + } + } +</script> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml new file mode 100644 index 0000000000000..bda0dccb9ae4b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-edit-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-edit-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Edit Image')); ?>' + } + }"> + <div class="page-main-actions" data-bind="scope: 'mediaGalleryImageEditActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-edit-details-messages" data-bind="scope: 'mediaGalleryEditDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <form data-bind="mageInit:{'validation':{}}" id="image-edit-details-form" method="post" enctype="multipart/form-data"> + <div id="media-gallery-image-edit-details" data-bind="scope: 'mediaGalleryEditDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </form> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-edit-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-edit", + "imageEditDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageEditDetailsUrl')); ?>", + "saveDetailsUrl": "<?= $escaper->escapeJs($block->getData('saveDetailsUrl')); ?>", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + }, + "mediaGalleryEditDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + }, + "mediaGalleryImageEditActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-edit-image-details-modal", + "modalWindowSelector": ".media-gallery-edit-image-details", + "mediaGalleryEditDetailsName": "mediaGalleryEditDetails", + "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Save')); ?>", + "handler": "saveImageDetailsAction", + "name": "save", + "classes": "action-default scalable save action-quaternary" + } + ] + } + } + }, + "Magento_MediaGalleryUi/js/validation/validate-image-title": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-description": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-keyword": {} + } + } +</script> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml new file mode 100644 index 0000000000000..9a8f01b1c2939 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-edit-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-edit-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Edit Image')); ?>' + } + }"> + <div class="page-main-actions" data-bind="scope: 'mediaGalleryImageEditActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-edit-details-messages" data-bind="scope: 'mediaGalleryEditDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <form data-bind="mageInit:{'validation':{}}" id="image-edit-details-form" method="post" enctype="multipart/form-data"> + <div id="media-gallery-image-edit-details" data-bind="scope: 'mediaGalleryEditDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </form> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-edit-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-edit", + "imageEditDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageEditDetailsUrl')); ?>", + "saveDetailsUrl": "<?= $escaper->escapeJs($block->getData('saveDetailsUrl')); ?>", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + }, + "mediaGalleryEditDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + }, + "mediaGalleryImageEditActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-edit-image-details-modal", + "modalWindowSelector": ".media-gallery-edit-image-details", + "mediaGalleryEditDetailsName": "mediaGalleryEditDetails", + "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Save')); ?>", + "handler": "saveImageDetailsAction", + "name": "save", + "classes": "action-default scalable save action-quaternary" + } + ] + } + } + }, + "Magento_MediaGalleryUi/js/validation/validate-image-title": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-description": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-keyword": {} + } + } +</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 new file mode 100644 index 0000000000000..974171617a19b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + 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"> + <item name="entityType" xsi:type="string">cms_block</item> + <item name="identityColumn" xsi:type="string">block_id</item> + <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="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string" translate="true">notifyWhenChangesStop</item> + <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> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> 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 new file mode 100644 index 0000000000000..4e59b485df503 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + 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"> + <item name="entityType" xsi:type="string">cms_page</item> + <item name="identityColumn" xsi:type="string">page_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <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="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string" translate="true">notifyWhenChangesStop</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> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> 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 new file mode 100644 index 0000000000000..b7307f9a74fae --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,376 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + media_gallery_listing.media_gallery_listing_data_source + </item> + </item> + </argument> + <settings> + <buttons> + <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" 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> + <dep>media_gallery_listing.media_gallery_listing_data_source</dep> + </deps> + </settings> + <dataSource name="media_gallery_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="fulltext" /> + <filters name="listing_filters"> + <filterInput name="path" provider="${ $.parentName }" sortOrder="2000"> + <settings> + <visible>false</visible> + <dataScope>path</dataScope> + <label translate="true">Directory</label> + </settings> + </filterInput> + <filterRange name="created_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="10"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Uploaded Date</label> + <dataScope>created_at</dataScope> + </settings> + </filterRange> + <filterRange name="updated_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="20"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Modification Date</label> + <dataScope>updated_at</dataScope> + </settings> + </filterRange> + <filterSelect name="entity_type" provider="${ $.parentName }" sortOrder="210" component="Magento_Ui/js/form/element/ui-select" template="ui/grid/filters/elements/ui-select"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\UsedIn"/> + <label translate="true">Show Images Used In</label> + <dataScope>entity_type</dataScope> + </settings> + </filterSelect> + <filterSelect name="source" provider="${ $.parentName }" sortOrder="60"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Source\Options"/> + <label translate="true">Source</label> + <dataScope>source</dataScope> + </settings> + </filterSelect> + <filterSelect name="content_status" provider="${ $.parentName }" sortOrder="220"> + <settings> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Status"/> + <label translate="true">Content Status</label> + <caption>All</caption> + <dataScope>content_status</dataScope> + </settings> + </filterSelect> + <filterSelect name="store_id" provider="${ $.parentName }" sortOrder="200"> + <settings> + <captionValue>0</captionValue> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Store"/> + <label translate="true">Store View</label> + <dataScope>store_id</dataScope> + <imports> + <link name="visible">componentType = column, index = ${ $.index }:visible</link> + </imports> + </settings> + </filterSelect> + <filterInput + name="duplicated" + provider="${ $.parentName }" + sortOrder="300" + template="Magento_MediaGalleryUi/grid/filter/checkbox" + component="Magento_Ui/js/form/element/single-checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="description" xsi:type="string" translate="true">Show duplicates</item> + <item name="valueMap" xsi:type="array"> + <item name="true" xsi:type="string">Yes</item> + </item> + </item> + </argument> + <settings> + <dataScope>duplicated</dataScope> + <label translate="true">Show duplicates</label> + </settings> + </filterInput> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + <container + name="sorting" + provider="media_gallery_listing.media_gallery_listing_data_source" + displayArea="sorting" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/sortBy"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="deps" xsi:type="array"> + <item name="0" xsi:type="string"> + media_gallery_listing.media_gallery_listing.media_gallery_columns + </item> + </item> + </item> + </argument> + </container> + <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"> + <item name="config" xsi:type="array"> + <item name="checkboxComponentName" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.massaction_checkbox</item> + <item name="imageModelName" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryProvider" xsi:type="string">media_gallery_listing.media_gallery_listing_data_source</item> + </item> + </argument> + </container> + </listingToolbar> + <container name="media_gallery_directories" + 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"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="containerId" xsi:type="string">media-gallery-masonry-grid</item> + </item> + </argument> + <column name="source" component="Magento_Ui/js/grid/columns/overlay" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider"> + <settings> + <label translate="true">Source</label> + <visible>false</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="thumbnail_url" component="Magento_MediaGalleryUi/js/grid/columns/image" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Url"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="fields" xsi:type="array"> + <item name="url" xsi:type="string">thumbnail_url</item> + </item> + <item name="deleteImageUrl" xsi:type="url" path="media_gallery/image/delete"/> + <item name="massactionComponentName" xsi:type="string">media_gallery_listing.media_gallery_listing.listing_top.media_gallery_massactions</item> + <item name="messagesName" xsi:type="string">media_gallery_listing.media_gallery_listing.messages</item> + <item name="imageModelname" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryDirectoryComponent" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_directories</item> + </item> + </argument> + <settings> + <label translate="true">Thumbnail Image</label> + <visible>true</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="newest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Newest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="oldest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Oldest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="created_at"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Uploaded Date</label> + <dataType>date</dataType> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="path"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_desc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Descending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_asc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Ascending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="title"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_az"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: A to Z</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_za"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: Z to A</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + </columns> + <container name="media_gallery_image_uploader" + class="Magento\MediaGalleryUi\Ui\Component\ImageUploader" + template="Magento_MediaGalleryUi/image-uploader" + component="Magento_MediaGalleryUi/js/image-uploader"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sortByName" xsi:type="string"> + media_gallery_listing.media_gallery_listing.listing_top.sorting + </item> + <item name="listingPagingName" xsi:type="string"> + media_gallery_listing.media_gallery_listing.listing_top.listing_paging + </item> + </item> + </argument> + </container> +</listing> 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 new file mode 100644 index 0000000000000..0710479ec0f61 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + 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"> + <item name="entityType" xsi:type="string">catalog_product</item> + <item name="identityColumn" xsi:type="string">entity_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <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="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string" translate="true">notifyWhenChangesStop</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> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> 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 new file mode 100644 index 0000000000000..a53a46c61f75d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,369 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + standalone_media_gallery_listing.media_gallery_listing_data_source + </item> + </item> + </argument> + <settings> + <spinner>media_gallery_columns</spinner> + <deps> + <dep>standalone_media_gallery_listing.media_gallery_listing_data_source</dep> + </deps> + <buttons> + <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"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="fulltext" /> + <filters name="listing_filters"> + <filterInput name="path" provider="${ $.parentName }" sortOrder="2000"> + <settings> + <visible>false</visible> + <dataScope>path</dataScope> + <label translate="true">Directory</label> + </settings> + </filterInput> + <filterRange name="created_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="10"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Uploaded Date</label> + <dataScope>created_at</dataScope> + </settings> + </filterRange> + <filterRange name="updated_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="20"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Modification Date</label> + <dataScope>updated_at</dataScope> + </settings> + </filterRange> + <filterSelect name="entity_type" provider="${ $.parentName }" sortOrder="210" component="Magento_Ui/js/form/element/ui-select" template="ui/grid/filters/elements/ui-select"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\UsedIn"/> + <label translate="true">Show Images Used In</label> + <dataScope>entity_type</dataScope> + </settings> + </filterSelect> + <filterSelect name="source" provider="${ $.parentName }" sortOrder="60"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Source\Options"/> + <label translate="true">Source</label> + <dataScope>source</dataScope> + </settings> + </filterSelect> + <filterSelect name="content_status" provider="${ $.parentName }" sortOrder="220"> + <settings> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Status"/> + <label translate="true">Content Status</label> + <caption>All</caption> + <dataScope>content_status</dataScope> + </settings> + </filterSelect> + <filterSelect name="store_id" provider="${ $.parentName }" sortOrder="200"> + <settings> + <captionValue>0</captionValue> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Store"/> + <label translate="true">Store View</label> + <dataScope>store_id</dataScope> + <imports> + <link name="visible">componentType = column, index = ${ $.index }:visible</link> + </imports> + </settings> + </filterSelect> + <filterInput + name="duplicated" + provider="${ $.parentName }" + sortOrder="300" + template="Magento_MediaGalleryUi/grid/filter/checkbox" + component="Magento_Ui/js/form/element/single-checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="description" xsi:type="string" translate="true">Show duplicates</item> + <item name="valueMap" xsi:type="array"> + <item name="true" xsi:type="string">Yes</item> + </item> + </item> + </argument> + <settings> + <dataScope>duplicated</dataScope> + <label translate="true">Show duplicates</label> + </settings> + </filterInput> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + <container + name="sorting" + provider="standalone_media_gallery_listing.media_gallery_listing_data_source" + displayArea="sorting" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/sortBy"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="deps" xsi:type="array"> + <item name="0" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns + </item> + </item> + </item> + </argument> + </container> + <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"> + <item name="config" xsi:type="array"> + <item name="checkboxComponentName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.massaction_checkbox</item> + <item name="imageModelName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryProvider" xsi:type="string">standalone_media_gallery_listing.media_gallery_listing_data_source</item> + </item> + </argument> + </container> + </listingToolbar> + <container name="media_gallery_directories" + 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"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="containerId" xsi:type="string">media-gallery-masonry-grid</item> + </item> + </argument> + <column name="source" component="Magento_Ui/js/grid/columns/overlay" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider"> + <settings> + <label translate="true">Source</label> + <visible>false</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="thumbnail_url" component="Magento_MediaGalleryUi/js/grid/columns/image" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Url"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="fields" xsi:type="array"> + <item name="url" xsi:type="string">thumbnail_url</item> + </item> + <item name="url" xsi:type="string">thumbnail_url</item> + <item name="deleteImageUrl" xsi:type="url" path="media_gallery/image/delete"/> + <item name="massactionComponentName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.media_gallery_massactions</item> + <item name="messagesName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.messages</item> + <item name="mediaGalleryDirectoryComponent" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_directories</item> + </item> + </argument> + <settings> + <label translate="true">Thumbnail Image</label> + <visible>true</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="newest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Newest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="oldest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Oldest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="created_at"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Uploaded Date</label> + <dataType>date</dataType> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="path"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_desc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Descending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_asc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Ascending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="title"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_az"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: A to Z</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_za"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: Z to A</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + </columns> + <container name="media_gallery_image_uploader" + class="Magento\MediaGalleryUi\Ui\Component\ImageUploaderStandAlone" + template="Magento_MediaGalleryUi/image-uploader" + component="Magento_MediaGalleryUi/js/image-uploader"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sortByName" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.sorting + </item> + <item name="listingPagingName" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.listing_paging + </item> + </item> + </argument> + </container> +</listing> 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 new file mode 100644 index 0000000000000..4b0d8f7dec89e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,498 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// +// Variables +// _____________________________________________ + +@color-folders-background: #a6a6a6; +@color-folders-background-selected: #cdecf6; +@color-folders-border: #7185f5; +@color-masonry-overlay: #d9631c; +@color-masonry-grey: #9e9e9e; +@color-masonry-white: #e1e1e1; +@color-masonry-steelblue: #4682b4; +@color-media-gallery-buttons-background: #e3e3e3; +@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, + .delete-folder-confirmation-popup { + + .modal-content { + word-wrap: anywhere; + } + } + + .media-gallery-asset-ui-select-filter, + .edit-image-details { + + .admin__action-multiselect-crumb { + max-width: 70%; + overflow: hidden; + text-overflow: ellipsis + } + + .admin__action-multiselect-label > span { + display: block; + margin-top: -2px; + max-height: 18px; + max-width: 70%; + overflow: hidden; + padding-left: 23px; + position: absolute; + text-overflow: ellipsis; + } + } + + .media-gallery-asset-ui-select-filter, + .edit-image-details { + .admin__action-multiselect-item-path { + float: right; + max-height: 70px; + max-width: 70px; + } + + .admin__action-multiselect-label { + display: inline-block; + width: 100%; + } + } + + .page-actions-buttons > button.no-display { + display: none; + } + + .page-actions-buttons > button.media-gallery-actions-buttons, + .page-actions .page-actions-buttons > button.media-gallery-actions-buttons:focus, + .page-actions-buttons > button.media-gallery-actions-buttons:hover { + background-color: @color-media-gallery-buttons-background; + border-color: @color-media-gallery-buttons-border; + color: @color-media-gallery-buttons-text; + } + + .mediagallery-massaction-checkbox { + background-color: @color-media-gallery-checkbox-background; + border-radius: 4px; + height: 40px; + input[type='checkbox'] { + margin-left: 10px; + margin-top: 11px; + } + margin-left: 15px; + margin-top: 10px; + position: absolute; + width: 40px; + z-index: 10; + } + + .mediagallery-massaction-items-count { + display: inline-block; + margin-left: -15px; + padding-right: 20px; + } + + .media-gallery-container { + + .action-disabled { + opacity: .5; + } + .masonry-image-grid .no-data-message-container, + .masonry-image-grid .error-message-container { + left: 50%; + margin-right: -50%; + position: sticky; + top: 50%; + } + + .admin__action-dropdown-wrap._active .admin__action-dropdown-text::after { + margin-right: 6px; + } + + .admin__data-grid-action-bookmarks .admin__action-dropdown-menu { + left: auto; + right: 0; + } + + .page-main-actions { + .page-actions { + .media-gallery-add-selected { + order: unset; + } + } + + & > .page-actions { + & > button.no-display { + display: none; + } + } + } + .jstree-default .jstree-hovered { + background: @color-folders-background; + border-color: @color-folders-border; + border-radius: 6px; + padding-top: 6px; + } + + .jstree-default .jstree-leaf a .jstree-icon { + background-position: -52px -16px; + } + + + .jstree-default a .jstree-icon { + background-position: -52px -16px; + } + + .jstree-default .jstree-no-dots .jstree-open > a > ins { + background-position: -52px -38px; + height: 20px; + width: 29px; + } + + .jstree a > ins { + float: left; + height: 22px; + margin-top: -3px; + width: 20px; + } + + .jstree-default .jstree-no-dots .jstree-leaf > ins { + background-image: none; + } + + .jstree-default ins { + background-image: url("@{baseDir}Magento_MediaGalleryUi/images/d.png"); + } + + .jstree a { + height: 30px; + margin: 1px; + padding-left: 6px; + padding-right: 10px; + padding-top: 6px; + width: max-content; + } + + .jstree-default .jstree-clicked { + background: @color-folders-background-selected; + border: .14em solid @color-folders-border; + border-radius: 6px; + padding-top: 6px; + } + + .masonry-image-overlay { + background-color: @color-masonry-overlay; + float: right; + font-size: 11px; + margin-left: 120px; + margin-top: 170px; + padding: .3rem; + pointer-events: none; + position: relative; + } + + .media-gallery-image-details { + float: left; + list-style: none; + margin-bottom: 0; + position: absolute; + width: 89%; + + .name { + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + display: -webkit-box; + font-size: 15px; + font-weight: bold; + line-height: 20px; + max-height: 50px; + overflow: hidden; + padding-bottom: 2px; + text-overflow: ellipsis; + white-space: pre-line; + word-wrap: anywhere; + word-wrap: break-word; + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + white-space: nowrap; + } + } + + .type { + display: inline-block; + font-size: 12px; + padding-bottom: 5px; + } + + .dimensions { + display: inline-block; + } + + .source { + display: inline-block; + } + } + + .media-gallery-image-actions { + float: right; + position: absolute; + right: 0; + width: 10%; + + .action-select-wrap { + cursor: pointer; + } + + .three-dots { + &:before { + content: url("@{baseDir}Magento_MediaGalleryUi/images/3-dots.png"); + cursor: pointer; + } + } + } + + .media-gallery-image { + height: 200px; + margin: 0 auto; + position: relative; + text-align: center; + width: 200px; + } + + .masonry-image-description { + background-color: @color-white; + min-height: 90px; + padding-top: 10px; + position: relative; + } + + .masonry-image-column { + background-color: @color-masonry-white; + width: 200px; + } + + .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 { + cursor: pointer; + height: 200px; + margin: 0 auto; + position: relative; + + &.selected { + border: 5px solid @color-masonry-steelblue; + } + } + + .media-gallery-image { + img { + bottom: 0; + height: auto; + left: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + padding: 5px; + position: absolute; + right: 0; + top: 0; + width: auto; + } + + .action-menu { + bottom: 0; + float: right; + left: auto; + top: auto; + z-index: 100; + } + } + + .media-gallery-source-icon { + margin-bottom: -6px; + width: 29px; + } + + .masonry-image-grid { + align-items: first baseline; + display: grid; + grid-template-columns: repeat(auto-fill, 210px); + justify-content: end; + margin: 10px 0; + position: relative; + } + + .admin__data-grid-filters .admin__form-field { + .action-select-wrap { + .action-menu { + width: 110%; + } + .admin__action-multiselect-search-label { + right: 1.5rem; + } + } + + .action-close { + padding: 0; + &:before { + font-size: 6px; + } + } + } + } + + .media-gallery-image-details-modal, + .media-gallery-edit-image-details-modal { + + .admin__action-multiselect-crumb { + .action-close { + padding: 0; + + &:before { + font-size: .5em; + } + } + } + + .edit-image-details { + padding: 50px; + } + + .path-display { + margin-top: 8px; + } + + .page-action-buttons { + float: right; + } + + .image-type { + .media-gallery-source-icon { + margin-bottom: -6px; + width: 29px; + } + + .type { + color: @color-very-dark-gray; + } + } + + .image-details { + .lib-vendor-prefix-display(); + + .image-details-image { + img { + max-height: 650px; + } + } + + .image-details-sidebar { + .lib-vendor-prefix-flex-grow(1); + margin-top: 0; + padding-left: 40px; + + .image-details-section { + margin-bottom: 40px; + max-width: 400px; + min-width: 290px; + word-wrap: anywhere; + .lib-clearfix(); + } + + h3.image-title { + font-weight: bold; + line-height: 1.5; + } + + .attributes { + .attribute { + &:not(:last-child) { + margin-bottom: 20px; + padding-bottom: 20px; + } + + & > * { + float: left; + margin-left: -1px; + width: 50%; + } + + .value { + display: inline; + float: right; + margin-left: 1px; + } + + .title { + color: @color-very-dark-gray; + } + } + } + + .tags { + .tags-list { + margin-bottom: 10px; + + .show-more-item { + display: none; + } + + &.show-all-tags { + margin-bottom: 0; + + .show-more-item { + display: inline; + } + + & + .show-more-link-container { + display: none; + } + } + } + } + } + } + } + .masonry-image-sortby { + display: inline-block; + } + + .masonry-results-number { + display: inline-block; + margin-right: 1.4rem; + } +} + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .media-gallery-image-details-modal { + .image-details { + display: block; + + .image-details-sidebar { + margin-top: 20px; + padding-left: 0; + } + + .image-details-image img { + max-height: 450px; + } + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png new file mode 100644 index 0000000000000..601ba415f2446 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png differ diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png new file mode 100644 index 0000000000000..db5cda9c5512b Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png differ diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png new file mode 100644 index 0000000000000..6516e915624c3 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png differ 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 new file mode 100644 index 0000000000000..28c021fe4728f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -0,0 +1,91 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'Magento_MediaGalleryUi/js/action/getDetails', + 'Magento_MediaGalleryUi/js/action/deleteImages', + 'mage/translate' +], function ($, _, getDetails, deleteImages, $t) { + 'use strict'; + + return { + + /** + * Get information about image use + * + * @param {Array} recordsIds + * @param {String} imageDetailsUrl + * @param {String} deleteImageUrl + */ + deleteImageAction: function (recordsIds, imageDetailsUrl, deleteImageUrl) { + 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 (images) { + confirmationContent = confirmationContent.replace( + '%1', + 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); + }); + }); + + return deferred.promise(); + }, + + /** + * Get information about image use + * + * @param {Object|String} images + * @return {String} + */ + getRecordRelatedContentMessage: function (images) { + var usedInMessage = $t('The selected assets are used in the content of the following entities: '), + usedIn = {}; + + $.each(images, function (key, image) { + $.each(image.details, function (sectionIndex, section) { + if (section.title === 'Used In' && _.isObject(section) && !_.isEmpty(section.value)) { + $.each(section.value, function (entityTypeIndex, entityTypeData) { + usedIn[entityTypeData.name] = entityTypeData.name in usedIn ? + usedIn[entityTypeData.name] + entityTypeData.number : + entityTypeData.number; + }); + } + }); + }); + + if (_.isEmpty(usedIn)) { + return ''; + } + + return usedInMessage + this.usedInObjectToString(usedIn); + }, + + /** + * Fromats usedIn object to string + * + * @param {Object} usedIn + * @return {String} + */ + usedInObjectToString: function (usedIn) { + var entities = []; + + $.each(usedIn, function (entityName, number) { + entities.push(entityName + '(' + number + ')'); + }); + + return entities.join(', ') + '.'; + } + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js new file mode 100644 index 0000000000000..c8ddeaf3d3929 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js @@ -0,0 +1,130 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'mage/url', + 'Magento_MediaGalleryUi/js/grid/messages', + 'Magento_Ui/js/modal/confirm', + 'mage/translate' +], function ($, _, urlBuilder, messages, confirmation, $t) { + 'use strict'; + + return function (ids, deleteUrl, confirmationContent) { + var deferred = $.Deferred(), + title = $t('Delete assets'), + cancelText = $t('Cancel'), + deleteImageText = $t('Delete'); + + /** + * Send deletion request with redords ids + * + * @param {Array} recordIds + * @param {String} serviceUrl + */ + function sendRequest(recordIds, serviceUrl) { + + $.ajax({ + type: 'POST', + url: serviceUrl, + dataType: 'json', + showLoader: true, + data: { + 'form_key': window.FORM_KEY, + 'ids': recordIds + }, + context: this, + + /** + * Success handler for deleting image + * + * @param {Object} response + */ + success: function (response) { + var message = !_.isUndefined(response.message) ? response.message : null; + + if (!response.success) { + message = message || $t('There was an error on attempt to delete the images.'); + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: false, + message: message, + code: 'error' + }); + + deferred.reject(message); + } + + message = message || $t('You have successfully removed the images.'); + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: true, + message: message, + code: 'success' + }); + deferred.resolve(message); + }, + + /** + * Error handler for deleting image + * + * @param {Object} response + */ + error: function (response) { + var message; + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('There was an error on attempt to delete the image.'); + } else { + message = response.responseJSON.message; + } + + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: false, + message: message, + code: 'error' + }); + deferred.reject(message); + } + }); + } + + confirmation({ + title: title, + modalClass: 'media-gallery-delete-image-action', + content: confirmationContent, + buttons: [ + { + text: cancelText, + class: 'action-secondary action-dismiss', + + /** + * Close modal + */ + click: function () { + this.closeModal(); + deferred.resolve({ + status: 'canceled' + }); + } + }, + { + text: deleteImageText, + class: 'action-primary action-accept', + + /** + * Delete Image and close modal + */ + click: function () { + sendRequest(ids, deleteUrl); + this.closeModal(); + } + } + ] + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js new file mode 100644 index 0000000000000..ec750afff29bf --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js @@ -0,0 +1,60 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (imageDetailsUrl, imageIds) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'GET', + url: imageDetailsUrl, + dataType: 'json', + showLoader: true, + data: { + 'ids': imageIds + }, + context: this, + + /** + * Resolve with image details if success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.imageDetails); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not retrieve image details.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js new file mode 100644 index 0000000000000..4d1120badeca0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js @@ -0,0 +1,56 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (saveImageDetailsUrl, data) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'POST', + url: saveImageDetailsUrl, + dataType: 'json', + showLoader: true, + data: data, + + /** + * Resolve with image details if success, reject with response message otherwise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not save image details.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js new file mode 100644 index 0000000000000..f6dd277fb85f5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js @@ -0,0 +1,34 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiElement', + 'jquery' +], function (Element, $) { + 'use strict'; + + return Element.extend({ + defaults: { + containerSelector: '.media-gallery-container', + masonryComponentPath: 'media_gallery_listing.media_gallery_listing.media_gallery_columns', + modules: { + masonry: '${ $.masonryComponentPath }' + } + }, + + /** + * Init component + * + * @return {exports} + */ + initialize: function () { + this._super(); + + $(this.containerSelector).applyBindings(); + + return this; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js new file mode 100644 index 0000000000000..cc4d759069c67 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js @@ -0,0 +1,61 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (createFolderUrl, paths) { + var deferred = $.Deferred(), + message, + data = { + paths: paths + }; + + $.ajax({ + type: 'POST', + url: createFolderUrl, + dataType: 'json', + showLoader: true, + data: data, + context: this, + + /** + * Resolve if success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not create the directory.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js new file mode 100644 index 0000000000000..06277481e1142 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js @@ -0,0 +1,60 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (deleteFolderUrl, path) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'POST', + url: deleteFolderUrl, + dataType: 'json', + showLoader: true, + data: { + path: path + }, + context: this, + + /** + * Resolve if delete folder success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not delete the directory.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + 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 new file mode 100644 index 0000000000000..5555baeabb66a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js @@ -0,0 +1,198 @@ +/** + * Copyright © Magento, Inc. All rights reserved.g + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'Magento_Ui/js/modal/confirm', + 'Magento_Ui/js/modal/alert', + 'underscore', + 'Magento_Ui/js/modal/prompt', + 'Magento_MediaGalleryUi/js/directory/actions/createDirectory', + 'Magento_MediaGalleryUi/js/directory/actions/deleteDirectory', + 'mage/translate', + 'validation' +], function ($, Component, confirm, uiAlert, _, prompt, createDirectory, deleteDirectory, $t) { + 'use strict'; + + 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', + messages: '${ $.messagesName }' + } + }, + + /** + * Initializes media gallery directories component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe(['selectedFolder']); + this.initEvents(); + + return this; + }, + + /** + * Initialize directories events + */ + initEvents: function () { + $(this.deleteButtonSelector).on('delete_folder', function () { + this.deleteFolder(); + }.bind(this)); + + $(this.createFolderButtonSelector).on('create_folder', function () { + this.createFolder(); + }.bind(this)); + }, + + /** + * 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' + }] + }); + }, + + /** + * Return configured path for folder creation. + * + * @param {String} folderName + * @returns {String} + */ + getNewFolderPath: function (folderName) { + if (_.isUndefined(this.selectedFolder()) || _.isNull(this.selectedFolder())) { + return folderName; + } + + return this.selectedFolder() + '/' + folderName; + }, + + /** + * Return configured prompt with input field + */ + getPrompt: function (data) { + prompt({ + title: $t(data.title), + content: $t(data.content), + modalClass: 'media-gallery-folder-prompt', + validation: true, + validationRules: ['required-entry', 'validate-alphanum'], + attributesField: { + name: 'folder_name', + 'data-validate': '{required:true, validate-alphanum}', + maxlength: '128' + }, + attributesForm: { + novalidate: 'novalidate', + action: '' + }, + context: this, + actions: data.actions, + buttons: data.buttons + }); + }, + + /** + * Confirmation popup for delete folder action. + */ + deleteFolder: function () { + confirm({ + title: $t('Are you sure you want to delete this folder?'), + modalClass: 'delete-folder-confirmation-popup', + content: $t('The following folder is going to be deleted: %1') + .replace('%1', this.selectedFolder()), + actions: { + + /** + * Delete folder on button click + */ + confirm: function () { + deleteDirectory( + this.directoryTree().deleteDirectoryUrl, + this.selectedFolder() + ).then(function () { + this.directoryTree().removeNode(); + this.directoryTree().selectStorageRoot(); + $(window).trigger('folderDeleted.enhancedMediaGallery'); + }.bind(this)).fail(function (error) { + uiAlert({ + content: error + }); + }); + }.bind(this) + } + }); + }, + + /** + * Set inactive all nodes, adds disable state to Delete Folder Button + */ + setInActive: function () { + this.selectedFolder(null); + $(this.deleteButtonSelector).attr('disabled', true).addClass('disabled'); + }, + + /** + * Set active node, remove disable state from Delete Forlder button + * + * @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 new file mode 100644 index 0000000000000..6a8e86f2dfa21 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -0,0 +1,497 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* global Base64 */ +define([ + 'jquery', + 'uiComponent', + 'uiLayout', + 'underscore', + 'Magento_MediaGalleryUi/js/directory/actions/createDirectory', + 'jquery/jstree/jquery.jstree', + 'Magento_Ui/js/lib/view/utils/async' +], function ($, Component, layout, _, createDirectory) { + 'use strict'; + + return Component.extend({ + defaults: { + allowedActions: [], + filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', + bookmarkProvider: 'componentType = bookmark, 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: { + bookmarks: '${ $.bookmarkProvider }', + directories: '${ $.name }_directories', + filterChips: '${ $.filterChipsProvider }' + }, + listens: { + '${ $.provider }:params.filters.path': 'updateSelectedDirectory' + }, + viewConfig: [{ + component: 'Magento_MediaGalleryUi/js/directory/directories', + name: '${ $.name }_directories', + allowedActions: '${ $.allowedActions }' + }] + }, + + /** + * Initializes media gallery directories component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe(['activeNode']).initView(); + + $.async( + this.directoryTreeSelector, + this, + function () { + this.renderDirectoryTree().then(function () { + this.initEvents(); + }.bind(this)); + }.bind(this) + ); + + return this; + }, + + /** + * Render directory tree component. + */ + renderDirectoryTree: function () { + return this.getJsonTree().then(function (data) { + this.createFolderIfNotExists(data).then(function (isFolderCreated) { + if (isFolderCreated) { + this.getJsonTree().then(function (newData) { + this.createTree(newData); + }.bind(this)); + } else { + this.createTree(data); + } + }.bind(this)); + }.bind(this)); + }, + + /** + * Set jstree reloaded + * + * @param {Boolean} value + */ + setJsTreeReloaded: function (value) { + this.jsTreeReloaded = value; + }, + + /** + * Create folder by provided current_tree_path param + * + * @param {Array} directories + */ + createFolderIfNotExists: function (directories) { + var requestedDirectory = this.getRequestedDirectory(), + deferred = $.Deferred(), + pathArray; + + if (_.isNull(requestedDirectory)) { + deferred.resolve(false); + + return deferred.promise(); + } + + 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(); + }, + + /** + * Verify if directory exists in array + * + * @param {Array} directories + * @param {String} directoryId + */ + isDirectoryExist: function (directories, directoryId) { + var found = false; + + /** + * Recursive search in array + * + * @param {Array} data + * @param {String} id + */ + function recurse(data, id) { + var i; + + for (i = 0; i < data.length; i++) { + if (data[i].attr.id === id) { + found = data[i]; + break; + } else if (data[i].children && data[i].children.length) { + recurse(data[i].children, id); + } + } + } + + recurse(directories, directoryId); + + return found; + }, + + /** + * Convert path string to path array e.g 'path1/path2' -> ['path1', 'path1/path2'] + * + * @param {String} path + */ + convertPathToPathsArray: function (path) { + var pathsArray = [], + pathString = '', + paths = path.split('/'); + + $.each(paths, function (i, val) { + pathString += i >= 1 ? val : val + '/'; + pathsArray.push(i >= 1 ? pathString : val); + }); + + return pathsArray; + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Wait for condition then call provided callback + */ + waitForCondition: function (condition, callback) { + if (condition()) { + setTimeout(function () { + this.waitForCondition(condition, callback); + }.bind(this), 100); + } else { + callback(); + } + }, + + /** + * Remove ability to multiple select on nodes + */ + disableMultiselectBehavior: function () { + $.jstree.defaults.ui['select_range_modifier'] = false; + $.jstree.defaults.ui['select_multiple_modifier'] = false; + }, + + /** + * Handle jstree events + */ + initEvents: function () { + this.initJsTreeEvents(); + this.disableMultiselectBehavior(); + + $(window).on('reload.MediaGallery', function () { + this.getJsonTree().then(function (data) { + this.createFolderIfNotExists(data).then(function (isCreated) { + if (isCreated) { + this.renderDirectoryTree().then(function () { + this.setJsTreeReloaded(true); + this.initJsTreeEvents(); + }.bind(this)); + } else { + this.updateSelectedDirectory(); + } + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + /** + * Fire event for jstree component + */ + initJsTreeEvents: function () { + $(this.directoryTreeSelector).on('select_node.jstree', function (element, data) { + this.setActiveNodeFilter($(data.rslt.obj).data('path')); + this.setJsTreeReloaded(false); + }.bind(this)); + + $(this.directoryTreeSelector).on('loaded.jstree', function () { + this.updateSelectedDirectory(); + }.bind(this)); + }, + + /** + * Verify directory filter on init event, select folder per directory filter state + */ + updateSelectedDirectory: function () { + var currentFilterPath = this.filterChips().filters.path, + requestedDirectory = this.getRequestedDirectory(), + currentTreePath; + + if (_.isUndefined(currentFilterPath)) { + this.clearFiltersHandle(); + + return; + } + + if (!_.isUndefined(this.bookmarks())) { + if (!_.size(this.bookmarks().getViewData(this.bookmarks().defaultIndex))) { + setTimeout(function () { + this.updateSelectedDirectory(); + }.bind(this), 500); + + return; + } + } + currentTreePath = this.isFilterApplied(currentFilterPath) || _.isNull(requestedDirectory) ? + currentFilterPath : requestedDirectory; + + if (this.folderExistsInTree(currentTreePath)) { + this.locateNode(currentTreePath); + } else { + this.selectStorageRoot(); + } + }, + + /** + * Verify if directory exists in folder tree + * + * @param {String} path + */ + folderExistsInTree: function (path) { + if (!_.isUndefined(path)) { + return $('#' + path.replace(/\//g, '\\/')).length === 1; + } + + 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 + */ + isFilterApplied: function (currentFilterPath) { + return !_.isUndefined(currentFilterPath) && currentFilterPath !== ''; + }, + + /** + * Locate and higlight node in jstree by path id. + * + * @param {String} path + */ + locateNode: function (path) { + if (path === $(this.directoryTreeSelector).jstree('get_selected').attr('id')) { + return; + } + path = path.replace(/\//g, '\\/'); + $(this.directoryTreeSelector).jstree('open_node', '#' + path); + $(this.directoryTreeSelector).jstree('select_node', '#' + path, true); + + }, + + /** + * Clear filters + */ + clearFiltersHandle: function () { + $(this.directoryTreeSelector).jstree('deselect_all'); + this.activeNode(null); + this.directories().setInActive(); + }, + + /** + * Set active node filter, or deselect if the same node clicked + * + * @param {String} nodePath + */ + setActiveNodeFilter: function (nodePath) { + if (this.activeNode() === nodePath && !this.jsTreeReloaded) { + this.selectStorageRoot(); + } else { + this.selectFolder(nodePath); + } + }, + + /** + * Remove folders selection -> select storage root + */ + selectStorageRoot: function () { + var filters = {}, + applied = this.filterChips().get('applied'); + + $(this.directoryTreeSelector).jstree('deselect_all'); + + filters = $.extend(true, filters, applied); + delete filters.path; + this.filterChips().set('applied', filters); + this.activeNode(null); + this.waitForCondition( + function () { + return _.isUndefined(this.directories()); + }.bind(this), + function () { + this.directories().setInActive(); + }.bind(this) + ); + }, + + /** + * Set selected folder + * + * @param {String} path + */ + selectFolder: function (path) { + this.activeNode(path); + + this.waitForCondition( + function () { + return _.isUndefined(this.directories()); + }.bind(this), + function () { + this.directories().setActive(path); + }.bind(this) + ); + + this.applyFilter(path); + }, + + /** + * Remove active node from directory tree, and select next + */ + removeNode: function () { + $(this.directoryTreeSelector).jstree('remove'); + }, + + /** + * Apply folder filter by path + * + * @param {String} path + */ + applyFilter: function (path) { + var filters = {}, + applied = this.filterChips().get('applied'); + + filters = $.extend(true, filters, applied); + filters.path = path; + this.filterChips().set('applied', filters); + }, + + /** + * Reload jstree and update jstree events + */ + reloadJsTree: function () { + var deferred = $.Deferred(); + + this.getJsonTree().then(function (data) { + this.createTree(data); + this.setJsTreeReloaded(true); + this.initEvents(); + deferred.resolve(); + }.bind(this)); + + return deferred.promise(); + }, + + /** + * Get json data for jstree + */ + getJsonTree: function () { + var deferred = $.Deferred(); + + $.ajax({ + url: this.getDirectoryTreeUrl, + type: 'GET', + dataType: 'json', + + /** + * Success handler for request + * + * @param {Object} data + */ + success: function (data) { + deferred.resolve(data); + }, + + /** + * Error handler for request + * + * @param {Object} jqXHR + * @param {String} textStatus + */ + error: function (jqXHR, textStatus) { + deferred.reject(); + throw textStatus; + } + }); + + return deferred.promise(); + }, + + /** + * Initialize directory tree + * + * @param {Array} data + */ + createTree: function (data) { + $(this.directoryTreeSelector).jstree({ + plugins: ['json_data', 'themes', 'ui', 'crrm', 'types', 'hotkeys'], + vcheckbox: { + 'two_state': true, + 'real_checkboxes': true + }, + 'json_data': { + data: data + }, + hotkeys: { + space: this._changeState, + 'return': this._changeState + }, + types: { + 'types': { + 'disabled': { + 'check_node': true, + 'uncheck_node': true + } + } + } + }); + } + }); +}); 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 new file mode 100644 index 0000000000000..e20e5a3235a6c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js @@ -0,0 +1,324 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'Magento_Ui/js/grid/columns/column', + 'uiLayout', + 'underscore' +], function ($, Column, layout, _) { + 'use strict'; + + 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', + gridSelector: '[data-id="media-gallery-masonry-grid"]', + selected: null, + allowedActions: [], + fields: { + id: 'id', + url: 'url', + alt: 'name' + }, + modules: { + actions: '${ $.name }_actions', + provider: '${ $.provider }', + messages: '${ $.messagesName }', + massaction: '${ $.massactionComponentName }' + }, + imports: { + activeDirectory: '${ $.mediaGalleryDirectoryComponent }:activeNode' + }, + listens: { + activeDirectory: 'selectDirectoryHandle', + '${ $.massactionComponentName }:massActionMode': 'updateSelected' + }, + viewConfig: [ + { + component: 'Magento_MediaGalleryUi/js/grid/columns/image/actions', + name: '${ $.name }_actions', + imageModelName: '${ $.name }', + allowedActions: '${ $.allowedActions }' + } + ] + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + this.initView(); + $(window).on('fileDeleted.enhancedMediaGallery', this.reloadMediaGrid.bind(this)); + $(window).on('reload.MediaGallery', this.reloadGrid.bind(this)); + + return this; + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'selected' + ]); + + return this; + }, + + /** + * Is massaction mode active. + */ + isMassActionMode: function () { + return this.massaction().massActionMode(); + }, + + /** + * Returns url to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {String} + */ + getUrl: function (record) { + return record[this.fields.url]; + }, + + /** + * Returns id to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {Number} + */ + getId: function (record) { + return record[this.fields.id]; + }, + + /** + * Update selected items per massaction mode. + */ + updateSelected: function () { + this.selected({}); + this.hideAddSelectedAndDeleteButon(); + }, + + /** + * Returns name to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {String} + */ + getImageAlt: function (record) { + return record[this.fields.alt]; + }, + + /** + * Check if the record is currently selected + * + * @param {Object} record - Data to be preprocessed. + * @returns {Boolean} + */ + isSelected: function (record) { + if (_.isNull(this.selected())) { + return false; + } + + if (this.massaction().massActionMode()) { + return this.selected()[record.id]; + } + + return this.getId(this.selected()) === this.getId(record); + }, + + /** + * Click on image + * + * @param {Object} record + * @param {Boolean} collapsibleOpened + */ + clickOnImage: function (record, collapsibleOpened) { + if (!collapsibleOpened) { + this.select(record); + } + }, + + /** + * Click on three-dots + * + * @param {Object} record + * @param {Boolean} collapsibleOpened + */ + clickOnThreeDots: function (record, collapsibleOpened) { + if (!this.isSelected(record) || collapsibleOpened) { + this.select(record); + } + }, + + /** + * Handle checkbox click. + */ + checkboxClick: function (record) { + var items = this.selected(); + + if (this.selected()[record.id]) { + delete items[record.id]; + this.selected(items); + } else { + items[record.id] = record.id; + this.selected(items); + } + + return true; + }, + + /** + * Set the record as selected + */ + select: function (record) { + if (this.massaction().massActionMode()) { + return this.checkboxClick(record); + } + + this.isSelected(record) ? this.selected(null) : this.selected(record); + this.toggleAddSelectedButton(); + + return true; + }, + + /** + * Deselect the record + */ + deselectImage: function () { + this.selected(null); + this.toggleAddSelectedButton(); + }, + + /** + * Get the selected record + * @returns {Object} + */ + getSelected: function () { + return this.selected(); + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Toggle add selected button + */ + toggleAddSelectedButton: function () { + if (this.selected() === null) { + this.hideAddSelectedAndDeleteButon(); + + return; + } + + if (this.allowedActions.includes('insert')) { + $(this.addSelectedBtnSelector).removeClass('no-display'); + } + + if (this.allowedActions.includes('delete')) { + $(this.deleteSelectedBtnSelector).removeClass('no-display'); + } + }, + + /** + * Hide add selected and Delete button + */ + hideAddSelectedAndDeleteButon: function () { + $(this.addSelectedBtnSelector).addClass('no-display'); + $(this.deleteSelectedBtnSelector).addClass('no-display'); + }, + + /** + * @param {jQuery.event} e + * @param {Object} data + */ + reloadMediaGrid: function (e, data) { + if (data.reload) { + this.reloadGrid(); + } + + if (data.message && data.code) { + this.addMessage(data.code, data.message); + } + this.hideAddSelectedAndDeleteButon(); + }, + + /** + * Reload grid + */ + reloadGrid: function () { + var provider = this.provider(), + dataStorage = provider.storage(); + + dataStorage.clearRequests(); + provider.reload(); + }, + + /** + * Add message + * + * @param {String} code + * @param {String} message + */ + addMessage: function (code, message) { + this.messages().add(code, message); + this.closeContextMenu(); + this.scrollToMessageContent(); + this.messages().scheduleCleanup(); + }, + + /** + * Listener to select directory event + * + * @param {String} path + */ + selectDirectoryHandle: function (path) { + if (this.selected() && + this.selected().directory !== path && + !this.massaction().massActionMode()) { + this.deselectImage(); + } + }, + + /** + * Action to close the context menu in media gallery. + */ + closeContextMenu: function () { + $(this.gridSelector).click(); + }, + + /** + * 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 new file mode 100644 index 0000000000000..76e051072285a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js @@ -0,0 +1,124 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', + 'mage/translate', + 'Magento_Ui/js/lib/view/utils/async' +], function ($, _, Component, deleteImageWithDetailConfirmation, image, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + 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' + } + ], + modules: { + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }', + mediaGalleryEditDetails: '${ $.mediaGalleryEditDetailsName }' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + 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; + }, + + /** + * Initialize image action events + */ + initEvents: function () { + $(this.imageModel().addSelectedBtnSelector).click(function () { + image.insertImage( + this.imageModel().getSelected(), + { + onInsertUrl: this.imageModel().onInsertUrl, + storeId: this.imageModel().storeId + } + ); + }.bind(this)); + $(this.imageModel().deleteSelectedBtnSelector).click(function () { + this.deleteImageAction(this.imageModel().selected()); + }.bind(this)); + + }, + + /** + * Delete image action + * + * @param {Object} record + */ + deleteImageAction: function (record) { + var imageDetailsUrl = this.mediaGalleryImageDetails().imageDetailsUrl, + deleteImageUrl = this.imageModel().deleteImageUrl; + + deleteImageWithDetailConfirmation.deleteImageAction([record.id], imageDetailsUrl, deleteImageUrl); + }, + + /** + * View image details + * + * @param {Object} record + */ + viewImageDetails: function (record) { + var recordId = this.imageModel().getId(record); + + this.mediaGalleryImageDetails().showImageDetailsById(recordId); + }, + + /** + * Edit image details + * + * @param {Object} record + */ + editImageDetails: function (record) { + var recordId = this.imageModel().getId(record); + + this.mediaGalleryEditDetails().showEditDetailsPanel(recordId); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js new file mode 100644 index 0000000000000..322b29c92ca5b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js @@ -0,0 +1,131 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* global FORM_KEY, tinyMceEditors */ +define([ + 'jquery', + 'wysiwygAdapter', + 'underscore', + 'mage/translate' +], function ($, wysiwyg, _, $t) { + 'use strict'; + + return { + + /** + * Insert provided image in wysiwyg if enabled, or widget + * + * @param {Object} record + * @param {Object} config + * @returns {Boolean} + */ + insertImage: function (record, config) { + var targetElement; + + if (record === null) { + return false; + } + targetElement = this.getTargetElement(window.MediabrowserUtility.targetElementId); + + if (!targetElement.length) { + window.MediabrowserUtility.closeDialog(); + throw $t('Target element not found for content update'); + } + + $.ajax({ + url: config.onInsertUrl, + data: { + filename: record['encoded_id'], + 'store_id': config.storeId, + 'as_is': targetElement.is('textarea') ? 1 : 0, + 'force_static_path': targetElement.data('force_static_path') ? 1 : 0, + 'form_key': FORM_KEY + }, + context: this, + showLoader: true + }).done($.proxy(function (data) { + if (targetElement.is('textarea')) { + this.insertAtCursor(targetElement.get(0), data.content); + targetElement.focus(); + $(targetElement).change(); + } else { + targetElement.val(data.content) + .data('size', data.size) + .data('mime-type', data.type) + .trigger('change'); + } + }, this)); + window.MediabrowserUtility.closeDialog(); + targetElement.focus(); + }, + + /** + * Insert image to target instance. + * + * @param {Object} element + * @param {*} value + */ + insertAtCursor: function (element, value) { + var sel, startPos, endPos, scrollTop; + + if ('selection' in document) { + //For browsers like Internet Explorer + element.focus(); + sel = document.selection.createRange(); + sel.text = value; + element.focus(); + } else if (element.selectionStart || element.selectionStart == '0') { //eslint-disable-line eqeqeq + //For browsers like Firefox and Webkit based + startPos = element.selectionStart; + endPos = element.selectionEnd; + scrollTop = element.scrollTop; + element.value = element.value.substring(0, startPos) + value + + element.value.substring(startPos, endPos) + element.value.substring(endPos, element.value.length); + element.focus(); + element.selectionStart = startPos + value.length; + element.selectionEnd = startPos + value.length + element.value.substring(startPos, endPos).length; + element.scrollTop = scrollTop; + } else { + element.value += value; + element.focus(); + } + }, + + /** + * Return opener Window object if it exists, not closed and editor is active + * + * @param {String} targetElementId + * return {Object|null} + */ + getMediaBrowserOpener: function (targetElementId) { + if (!_.isUndefined(wysiwyg) && wysiwyg.get(targetElementId) && !_.isUndefined(tinyMceEditors) && + !tinyMceEditors.get(targetElementId).getMediaBrowserOpener().closed + ) { + return tinyMceEditors.get(targetElementId).getMediaBrowserOpener(); + } + + return null; + }, + + /** + * Get target element + * + * @param {String} targetElementId + * @returns {*|n.fn.init|jQuery|HTMLElement} + */ + getTargetElement: function (targetElementId) { + var opener; + + if (!_.isUndefined(wysiwyg) && wysiwyg.get(targetElementId)) { + opener = this.getMediaBrowserOpener(targetElementId) || window; + targetElementId = tinyMceEditors.get(targetElementId).getMediaBrowserTargetElementId(); + + return $(opener.document.getElementById(targetElementId)); + } + + return $('#' + targetElementId); + } + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js new file mode 100644 index 0000000000000..659fcc0cdcfda --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js @@ -0,0 +1,49 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/grid/masonry', + 'jquery' +], function (Masonry, $) { + 'use strict'; + + return Masonry.extend({ + defaults: { + modules: { + provider: '${ $.provider }' + } + }, + + /** + * Init component + * + * @return {Object} + */ + initialize: function () { + this._super(); + this.initEvents(); + + return this; + }, + + /** + * Initialize events + */ + initEvents: function () { + $(window).on('folderDeleted.enhancedMediaGallery', this.reloadGrid.bind(this)); + }, + + /** + * Reload grid + */ + reloadGrid: function () { + var provider = this.provider(), + dataStorage = provider.storage(); + + dataStorage.clearRequests(); + provider.reload(); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js new file mode 100644 index 0000000000000..ddc5af0ab6296 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js @@ -0,0 +1,110 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'mage/translate', + 'text!Magento_MediaGalleryUi/template/grid/massactions/massactionButtons.html' +], function ($, Component, $t, massactionButtons) { + 'use strict'; + + return Component.extend({ + defaults: { + gridSelector: '[data-id="media-gallery-masonry-grid"]', + standAloneTitle: 'Manage Gallery', + slidePanelTitle: 'Media Gallery', + defaultTitle: null, + are: null, + standAloneArea: 'standalone', + slidepanelArea: 'slidepanel', + massactionButtonsSelector: '.massaction-buttons', + buttonsSelectorStandalone: '.page-actions-buttons', + buttonsSelectorSlidePanel: '.page-actions.floating-header', + buttons: '.page-main-actions :button', + massactionModeTitle: $t('Select Images to Delete') + }, + + /** + * Initializes media gallery massaction component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe([ + 'massActionMode' + ]); + + return this; + }, + + /** + * Switch massaction view state per active mode. + */ + switchView: function () { + this.changePageTitle(); + this.switchButtons(); + }, + + /** + * Hide or show buttons per active mode. + */ + switchButtons: function () { + if (this.massActionMode()) { + this.activateMassactionButtonView(); + } else { + this.revertButtonsToDefaultView(); + } + }, + + /** + * Sets buttons to default regular -mode view. + */ + revertButtonsToDefaultView: function () { + $(this.buttons).removeClass('no-display'); + $(this.massactionButtonsSelector).remove(); + }, + + /** + * Activate mass action buttons view + */ + activateMassactionButtonView: function () { + var buttonsContainer; + + $(this.buttons).addClass('no-display'); + + buttonsContainer = this.area === this.standAloneArea ? + this.buttonsSelectorStandalone : + this.buttonsSelectorSlidePanel; + + $(buttonsContainer).append(massactionButtons); + $(this.massactionButtonsSelector).applyBindings(); + }, + + /** + * Change page title per active mode. + */ + changePageTitle: function () { + var title = $('h1:contains(' + this.standAloneTitle + ')'), + titleSelector; + + if (title.length === 1) { + titleSelector = title; + this.area = this.standAloneArea; + } else { + titleSelector = $('h1:contains(' + this.slidePanelTitle + ')'); + this.area = this.slidepanelArea; + } + + if (this.massActionMode()) { + this.defaultTitle = titleSelector.text(); + titleSelector.text(this.massactionModeTitle); + } else { + titleSelector = $('h1:contains(' + this.massactionModeTitle + ')'); + titleSelector.text(this.defaultTitle); + } + } + }); +}); 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 new file mode 100644 index 0000000000000..a20239fb1165e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js @@ -0,0 +1,156 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'uiLayout', + 'underscore', + 'Magento_Ui/js/modal/alert', + 'mage/translate' +], function ($, Component, DeleteImages, Layout, _, uiAlert, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + allowedActions: [], + deleteButtonSelector: '#delete_selected_massaction', + deleteImagesSelector: '#delete_massaction', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + modules: { + massactionView: '${ $.name }_view', + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }' + }, + viewConfig: [ + { + component: 'Magento_MediaGalleryUi/js/grid/massaction/massactionView', + name: '${ $.name }_view' + } + ], + imports: { + imageItems: '${ $.mediaGalleryProvider }:data.items' + }, + listens: { + imageItems: 'checkButtonVisibility' + }, + exports: { + massActionMode: '${ $.name }_view:massActionMode' + } + }, + + /** + * Initializes media gallery massaction component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe([ + 'massActionMode' + ]); + this.initView(); + this.initEvents(); + + return this; + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + Layout(this.viewConfig); + + return this; + }, + + /** + * Initilize massactions events for media gallery grid. + */ + initEvents: function () { + $(window).on('massAction.MediaGallery', function () { + if (this.massActionMode()) { + return; + } + this.imageModel().selected(null); + this.massActionMode(true); + this.switchMode(); + }.bind(this)); + + $(window).on('terminateMassAction.MediaGallery', function () { + if (!this.massActionMode()) { + return; + } + + this.massActionMode(false); + this.switchMode(); + this.imageModel().updateSelected(); + }.bind(this)); + }, + + /** + * Return total selected items. + */ + getSelectedCount: function () { + if (this.massActionMode() && !_.isNull(this.imageModel().selected())) { + return Object.keys(this.imageModel().selected()).length; + } + + return 0; + }, + + /** + * 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 { + $(this.deleteImagesSelector).removeClass('disabled'); + } + }, + + /** + * Switch massaction per current event. + */ + switchMode: function () { + this.massactionView().switchView(); + this.handleDeleteAction(); + }, + + /** + * Change Default behavior of delete image to bulk deletion. + */ + handleDeleteAction: function () { + if (this.massActionMode()) { + $(this.deleteButtonSelector).on('massDelete.MediaGallery', function () { + if (this.getSelectedCount() < 1) { + uiAlert({ + content: $t('You need to select at least one image') + }); + + } else { + DeleteImages.deleteImageAction( + this.imageModel().selected(), + this.mediaGalleryImageDetails().imageDetailsUrl, + this.imageModel().deleteImageUrl + ).then(function (response) { + if (response.status === 'canceled') { + return; + } + $(window).trigger('terminateMassAction.MediaGallery'); + }); + } + }.bind(this)); + } + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js new file mode 100644 index 0000000000000..8ed802d53825a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js @@ -0,0 +1,89 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiElement', + 'escaper' +], function (Element, escaper) { + 'use strict'; + + return Element.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/grid/messages', + messageDelay: 5, + messages: [], + allowedTags: ['div', 'span', 'b', 'strong', 'i', 'em', 'u', 'a'] + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'messages' + ]); + + return this; + }, + + /** + * Get messages + * + * @returns {Array} + */ + get: function () { + return this.messages(); + }, + + /** + * Add message + * + * @param {String} type + * @param {String} message + */ + add: function (type, message) { + this.messages.push({ + code: type, + message: message + }); + }, + + /** + * Clear messages + */ + clear: function () { + this.messages.removeAll(); + }, + + /** + * Schedule message cleanup + * + * @param {Number} delay + */ + scheduleCleanup: function (delay) { + // eslint-disable-next-line no-unused-vars + var timerId; + + delay = delay || this.messageDelay; + + timerId = setTimeout(function () { + clearTimeout(timerId); + this.clear(); + }.bind(this), Number(delay) * 1000); + }, + + /** + * Prepare the given message to be rendered as HTML + * + * @param {String} message + * @return {String} + */ + prepareMessageUnsanitizedHtml: function (message) { + return escaper.escapeHtml(message, this.allowedTags); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js new file mode 100644 index 0000000000000..15f62d6a7efd1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js @@ -0,0 +1,77 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'Magento_Ui/js/grid/sortBy' +], function (Element) { + 'use strict'; + + return Element.extend({ + defaults: { + columnIndexMap: {} + }, + + /** + * Prepared sort order options + */ + preparedOptions: function (columns) { + var index = 0, + sortBy; + + if (columns && columns.length > 0) { + columns.map(function (column) { + if (column.sortable === true) { + sortBy = column['sort_by'] || {}; + + if (sortBy.excluded) { + return; + } + + this.options.push({ + value: column.index, + label: column.label, + sortByField: sortBy.field, + sortDirection: sortBy.direction + }); + + this.columnIndexMap[column.index] = index++; + + this.isVisible(true); + } else { + this.isVisible(false); + } + }.bind(this)); + } + }, + + /** + * Apply changes + */ + applyChanges: function () { + var column = this.getColumn(this.selectedOption()); + + this.applied({ + field: column.sortByField || this.selectedOption(), + direction: column.sortDirection || this.sorting + }); + }, + + /** + * Get column by index + * + * @param {String} optionIndex + * @returns {Object} + */ + getColumn: function (optionIndex) { + return this.options[this.columnIndexMap[optionIndex]]; + }, + + /** + * Select default option + */ + selectDefaultOption: function () { + this.selectedOption(this.options[0].value); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js new file mode 100644 index 0000000000000..58fff640f9db3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js @@ -0,0 +1,244 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'jquery', + 'underscore', + 'Magento_Ui/js/lib/validation/validator', + 'mage/translate', + 'jquery/file-uploader' +], function (Component, $, _, validator, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + imageUploadInputSelector: '#image-uploader-form', + directoriesPath: 'media_gallery_listing.media_gallery_listing.media_gallery_directories', + actionsPath: 'media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url', + messagesPath: 'media_gallery_listing.media_gallery_listing.messages', + imageUploadUrl: '', + acceptFileTypes: '', + allowedExtensions: '', + maxFileSize: '', + maxFileNameLength: 90, + loader: false, + modules: { + directories: '${ $.directoriesPath }', + actions: '${ $.actionsPath }', + mediaGridMessages: '${ $.messagesPath }', + sortBy: '${ $.sortByName }', + listingPaging: '${ $.listingPagingName }' + } + }, + + /** + * Init component + * + * @return {exports} + */ + initialize: function () { + this._super().observe( + [ + 'loader', + 'count' + ] + ); + + return this; + }, + + /** + * Initializes file upload library + */ + initializeFileUpload: function () { + $(this.imageUploadInputSelector).fileupload({ + url: this.imageUploadUrl, + acceptFileTypes: this.acceptFileTypes, + allowedExtensions: this.allowedExtensions, + maxFileSize: this.maxFileSize, + + /** + * Extending the form data + * + * @param {Object} form + * @returns {Array} + */ + formData: function (form) { + return form.serializeArray().concat( + [{ + name: 'isAjax', + value: true + }, + { + name: 'form_key', + value: window.FORM_KEY + }, + { + name: 'target_folder', + value: this.getTargetFolder() + }] + ); + }.bind(this), + + add: function (e, data) { + if (!this.isSizeExceeded(data.files[0]).passed) { + this.addValidationErrorMessage( + $t('Cannot upload "%1". File exceeds maximum file size limit.') + .replace('%1', data.files[0].name) + ); + + return; + } else if (!this.isFileNameLengthExceeded(data.files[0]).passed) { + this.addValidationErrorMessage( + $t('Cannot upload "%1". Filename is too long, must be 90 characters or less.') + .replace('%1', data.files[0].name) + ); + + return; + } + + this.showLoader(); + this.count(1); + data.submit(); + }.bind(this), + + stop: function () { + this.openNewestImages(); + this.mediaGridMessages().scheduleCleanup(); + }.bind(this), + + start: function () { + this.mediaGridMessages().clear(); + }.bind(this), + + done: function (e, data) { + var response = data.jqXHR.responseJSON; + + if (!response) { + this.showErrorMessage(data, $t('Could not upload the asset.')); + + return; + } + + if (!response.success) { + this.showErrorMessage(data, response.message); + + return; + } + this.showSuccessMessage(data); + this.hideLoader(); + this.actions().reloadGrid(); + }.bind(this) + }); + }, + + /** + * Add error message after validation error. + * + * @param {String} message + */ + addValidationErrorMessage: function (message) { + this.mediaGridMessages().add('error', message); + + this.count() < 2 || this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Checks if size of provided file exceeds + * defined in configuration size limits. + * + * @param {Object} file - File to be checked. + * @returns {Boolean} + */ + isSizeExceeded: function (file) { + return validator('validate-max-size', file.size, this.maxFileSize); + }, + + /** + * Checks if name length of provided file exceeds + * defined in configuration size limits. + * + * @param {Object} file - File to be checked. + * @returns {Boolean} + */ + isFileNameLengthExceeded: function (file) { + return validator('max_text_length', file.name, this.maxFileNameLength); + }, + + /** + * Go to recently uploaded images if at least one uploaded successfully + */ + openNewestImages: function () { + this.mediaGridMessages().get().each(function (message) { + if (message.code === 'success') { + this.actions().deselectImage(); + this.sortBy().selectDefaultOption(); + this.listingPaging().goFirst(); + + return false; + } + }.bind(this)); + }, + + /** + * Show error meassages with file name. + * + * @param {Object} data + * @param {String} message + */ + showErrorMessage: function (data, message) { + data.files.each(function (file) { + this.mediaGridMessages().add( + 'error', + file.name + ': ' + $t(message) + ); + }.bind(this)); + + this.hideLoader(); + }, + + /** + * Show success message, and files counts + */ + showSuccessMessage: function () { + this.mediaGridMessages().messages.remove(function (item) { + return item.code === 'success'; + }); + this.mediaGridMessages().add('success', $t('Assets have been successfully uploaded!')); + this.count(this.count() + 1); + + }, + + /** + * Gets Media Gallery selected folder + * + * @returns {String} + */ + getTargetFolder: function () { + + if (_.isUndefined(this.directories().activeNode()) || + _.isNull(this.directories().activeNode())) { + return '/'; + } + + return this.directories().activeNode(); + }, + + /** + * Shows spinner loader + */ + showLoader: function () { + this.loader(true); + }, + + /** + * Hides spinner loader + */ + hideLoader: function () { + this.loader(false); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js new file mode 100644 index 0000000000000..ea4de9e1feefa --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js @@ -0,0 +1,133 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiElement', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', + 'Magento_MediaGalleryUi/js/action/saveDetails', + 'mage/validation' +], function ($, _, Element, deleteImageWithDetailConfirmation, addSelected, saveDetails) { + 'use strict'; + + return Element.extend({ + defaults: { + modalSelector: '', + modalWindowSelector: '', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + mediaGalleryEditDetailsName: 'mediaGalleryEditDetails', + template: 'Magento_MediaGalleryUi/image/actions', + modules: { + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }', + mediaGalleryEditDetails: '${ $.mediaGalleryEditDetailsName }' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + $(window).on('fileDeleted.enhancedMediaGallery', this.closeViewDetailsModal.bind(this)); + + return this; + }, + + /** + * Close the images details modal + */ + closeModal: function () { + var modalElement = $(this.modalSelector), + modalWindow = $(this.modalWindowSelector); + + if (!modalWindow.hasClass('_show') || !modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + this.mediaGalleryEditDetails().keywordsSelect().cacheOptions.plain = []; + modalElement.modal('closeModal'); + }, + + /** + * Opens the image edit panel + */ + editImageAction: function () { + var record = this.imageModel().getSelected().id; + + this.mediaGalleryEditDetails().showEditDetailsPanel(record); + }, + + /** + * Delete image action + */ + deleteImageAction: function () { + var imageDetailsUrl = this.mediaGalleryImageDetails().imageDetailsUrl, + deleteImageUrl = this.imageModel().deleteImageUrl; + + deleteImageWithDetailConfirmation.deleteImageAction( + [this.imageModel().getSelected().id], + imageDetailsUrl, + deleteImageUrl + ); + }, + + /** + * Save image details action + */ + saveImageDetailsAction: function () { + var saveDetailsUrl = this.mediaGalleryEditDetails().saveDetailsUrl, + modalElement = $(this.modalSelector), + form = modalElement.find('#image-edit-details-form'), + imageId = this.imageModel().getSelected().id, + keywords = this.mediaGalleryEditDetails().selectedKeywords(), + imageDetails = this.mediaGalleryImageDetails(), + imageEditDetails = this.mediaGalleryEditDetails(); + + if (form.validation('isValid')) { + saveDetails( + saveDetailsUrl, + [form.serialize(), $.param({ + 'keywords': keywords + })].join('&') + ).then(function () { + this.closeModal(); + this.imageModel().reloadGrid(); + imageDetails.removeCached(imageId); + imageEditDetails.removeCached(imageId); + + if (imageDetails.isActive()) { + imageDetails.showImageDetailsById(imageId); + } + }.bind(this)); + } + }, + + /** + * Add Image + */ + addImage: function () { + addSelected.insertImage( + this.imageModel().getSelected(), + { + onInsertUrl: this.imageModel().onInsertUrl, + storeId: this.imageModel().storeId + } + ); + this.closeModal(); + }, + + /** + * Close view details modal after confirm deleting image + */ + closeViewDetailsModal: function () { + this.closeModal(); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js new file mode 100644 index 0000000000000..db42f155501c3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js @@ -0,0 +1,184 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/getDetails' +], function ($, _, Component, getDetails) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/image/image-details', + modalSelector: '', + modalWindowSelector: '', + imageDetailsUrl: '/media_gallery/image/details', + images: [], + tagListLimit: 7, + showAllTags: false, + image: null, + modules: { + mediaGridMessages: '${ $.mediaGridMessages }' + } + }, + + /** + * Init observable variables + * + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'image', + 'showAllTags' + ]); + + return this; + }, + + /** + * Show image details by ID + * + * @param {String} imageId + */ + showImageDetailsById: function (imageId) { + if (_.isUndefined(this.images[imageId])) { + getDetails(this.imageDetailsUrl, [imageId]).then(function (imageDetails) { + this.images[imageId] = imageDetails[imageId]; + this.image(this.images[imageId]); + this.openImageDetailsModal(); + }.bind(this)).fail(function (error) { + this.addMediaGridMessage('error', error); + }.bind(this)); + + return; + } + + if (this.image() && this.image().id === imageId) { + this.openImageDetailsModal(); + + return; + } + + this.image(this.images[imageId]); + this.openImageDetailsModal(); + }, + + /** + * Open image details popup + */ + openImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + this.showAllTags(false); + modalElement.modal('openModal'); + }, + + /** + * Close image details popup + */ + closeImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Add media grid message + * + * @param {String} code + * @param {String} message + */ + addMediaGridMessage: function (code, message) { + this.mediaGridMessages().add(code, message); + this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Get tag text + * + * @param {String} tagText + * @param {Number} tagIndex + * @return {String} + */ + getTagText: function (tagText, tagIndex) { + return tagText + (this.image().tags.length - 1 === tagIndex ? '' : ','); + }, + + /** + * Show all image tags + */ + showMoreImageTags: function () { + this.showAllTags(true); + }, + + /** + * Is value an object + * + * @param {*} value + * @returns {Boolean} + */ + isArray: function (value) { + return _.isArray(value); + }, + + /** + * Is value not empty + * + * @param {*} value + * @returns {Boolean} + */ + notEmpty: function (value) { + return value.length > 0; + }, + + /** + * Get name and number text for used in link + * + * @param {Object} item + * @returns {String} + */ + getUsedInText: function (item) { + return item.name + '(' + item.number + ')'; + }, + + /** + * Get filter url + * + * @param {String} link + */ + getFilterUrl: function (link) { + return link + '?filters[asset_id]=[' + this.image().id + ']'; + }, + + /** + * Check if details modal is active + * @return {Boolean} + */ + isActive: function () { + return $(this.modalWindowSelector).hasClass('_show'); + }, + + /** + * Remove image details + * + * @param {String} id + */ + removeCached: function (id) { + delete this.images[id]; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js new file mode 100644 index 0000000000000..e1404a16d7125 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js @@ -0,0 +1,237 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'uiLayout', + 'Magento_Ui/js/lib/key-codes', + 'Magento_MediaGalleryUi/js/action/getDetails', + 'mage/validation' +], function ($, _, Component, layout, keyCodes, getDetails) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/image/image-edit', + modalSelector: '.media-gallery-edit-image-details-modal', + imageEditDetailsUrl: '/media_gallery/image/details', + saveDetailsUrl: '/media_gallery/image/saveDetails', + images: [], + image: null, + keywordOptions: [], + selectedKeywords: [], + newKeyword: '', + newKeywordSelector: '#keyword', + modules: { + mediaGridMessages: '${ $.mediaGridMessages }', + keywordsSelect: '${ $.name }_keywords' + }, + viewConfig: [ + { + component: 'Magento_Ui/js/form/element/ui-select', + name: '${ $.name }_keywords', + template: 'ui/grid/filters/elements/ui-select', + disableLabel: true + } + ], + exports: { + keywordOptions: '${ $.name }_keywords:options' + }, + links: { + selectedKeywords: '${ $.name }_keywords:value' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super().initView(); + + return this; + }, + + /** + * Add a new keyword to select + */ + addKeyword: function () { + var options = this.keywordOptions(), + selected = this.selectedKeywords(), + newKeywordField = $(this.newKeywordSelector); + + newKeywordField.validation(); + + if (!newKeywordField.validation('isValid') || this.newKeyword() === '') { + return; + } + + options.push(this.getOptionForKeyword(this.newKeyword())); + selected.push(this.newKeyword()); + this.newKeyword(''); + + this.keywordOptions(options); + this.selectedKeywords(selected); + }, + + /** + * Create an option object based on keyword string + * + * @param {String} keyword + * @returns {Object} + */ + getOptionForKeyword: function (keyword) { + return { + 'is_active': 1, + level: 1, + value: keyword, + label: keyword + }; + }, + + /** + * Convert array of keywords to options format + * + * @param {Array} tags + */ + setKeywordOptions: function (tags) { + var options = []; + + tags.forEach(function (tag) { + options.push(this.getOptionForKeyword(tag)); + }.bind(this)); + + this.keywordOptions(options); + this.selectedKeywords(tags); + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Init observable variables + * + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'image', + 'keywordOptions', + 'selectedKeywords', + 'newKeyword' + ]); + + return this; + }, + + /** + * Get image details by ID + * + * @param {String} imageId + */ + showEditDetailsPanel: function (imageId) { + if (_.isUndefined(this.images[imageId])) { + getDetails(this.imageEditDetailsUrl, [imageId]).then(function (imageDetails) { + this.images[imageId] = imageDetails[imageId]; + this.image(this.images[imageId]); + this.openEditImageDetailsModal(); + }.bind(this)).fail(function (error) { + this.addMediaGridMessage('error', error); + }.bind(this)); + + return; + } + + if (this.image() && this.image().id === imageId) { + this.openEditImageDetailsModal(); + + return; + } + + this.image(this.images[imageId]); + this.openEditImageDetailsModal(); + }, + + /** + * Open edit image details popup + */ + openEditImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + this.setKeywordOptions(this.image().tags); + this.newKeyword(''); + + modalElement.modal('openModal'); + }, + + /** + * Close image details popup + */ + closeImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Add media grid message + * + * @param {String} code + * @param {String} message + */ + addMediaGridMessage: function (code, message) { + this.mediaGridMessages().add(code, message); + this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Handle Enter key event to save image details + * + * @param {Object} data + * @param {jQuery.Event} event + * @returns {Boolean} + */ + handleEnterKey: function (data, event) { + var modalElement = $(this.modalSelector), + key = keyCodes[event.keyCode]; + + if (key === 'enterKey') { + event.preventDefault(); + modalElement.find('.page-action-buttons button.save').click(); + } + + return true; + }, + + /** + * Remove cached image details in edit form + * + * @param {String} id + */ + removeCached: function (id) { + delete this.images[id]; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js new file mode 100644 index 0000000000000..127f1676015f1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-description', function (value) { + return /^[a-zA-Z0-9\-\_\.\,\n\ ]+$|^$/i.test(value); + + }, $.mage.__('Please use only letters (a-z or A-Z), numbers (0-9), ' + + 'dots (.), commas(,), underscores (_), dashes (-), and spaces on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js new file mode 100644 index 0000000000000..47fa5b19781bc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($, validate, $t) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-keyword', function (value) { + return /^[a-zA-Z0-9\-\_\.\,]+$|^$/i.test(value); + + }, $t('Please use only letters (a-z or A-Z), numbers (0-9), dots (.), commas(,), ' + + 'underscores (_) and dashes(-) on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js new file mode 100644 index 0000000000000..1429be64b7d12 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-title', function (value) { + return /^[a-zA-Z0-9\-\_\.\,\ ]+$/i.test(value); + + }, $.mage.__('Please use only letters (a-z or A-Z), numbers (0-9), dots (.), commas(,), ' + + 'underscores (_), dashes(-) and spaces on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html new file mode 100644 index 0000000000000..3b88c58201be7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html @@ -0,0 +1,45 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="media-gallery-wrap" collapsible> + <div class="mediagallery-massaction-checkbox" if="isMassActionMode()"> + <input type="checkbox" attr="{ 'data-ui-id': $row().title }" visible="isMassActionMode()" ko-checked="isSelected($row())" click="function () { return select($row()); }"/> + </div> + <div class="media-gallery-image"> + <div data-row="file" + class="masonry-image-block media-gallery-image-block" + attr="'data-id': $col.getId($row())" + css="{ selected: isSelected($row()) }" + click="function(){ clickOnImage($row(), $collapsible.opened()) }" + > + <img attr="src: $col.getUrl($row()), alt: $col.getImageAlt($row())" + class="media-gallery-image-column" + data-role="thumbnail"/> + </div> + <ul class="action-menu" css="_active: $collapsible.opened"> + <scope args="actions"> + <render args="template"/> + </scope> + </ul> + </div> + <div class="masonry-image-description"> + <ul class="media-gallery-image-details"> + <li class="name" data-ui-id="title" text="$row().title"></li> + <li class="source"> + <img if="$row().source" class="media-gallery-source-icon" attr="{ src: $row().source }"/> + </li> + <li class="type" data-ui-id="content-type" text="$row().content_type"></li> • + <li class="dimensions" data-ui-id="dimensions" text="$row().width + 'x' + $row().height"></li> + </ul> + <div class="media-gallery-image-actions"> + <div class="action-select-wrap"> + <span class="three-dots" ifnot="isMassActionMode()" + toggleCollapsible + click="function () { clickOnThreeDots($row(), $collapsible.opened()); }"></span> + </div> + </div> + </div> +</div> 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 new file mode 100644 index 0000000000000..72447196cea55 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html @@ -0,0 +1,15 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<each args="{ data: actionsList, as: 'action' }"> + <li> + <a href="#" text="action.title" + click="$parent[action.handler].bind($parent, $row())" + attr="{'data-action': 'item-' + action.name, class: action.classes}"> + </a> + </li> +</each> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html new file mode 100644 index 0000000000000..da835952e2f23 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html @@ -0,0 +1,10 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div class="media-directory-container"> + <div id="media-gallery-directory-tree"></div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html new file mode 100644 index 0000000000000..d1840fdb3dc8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html @@ -0,0 +1,24 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="admin__field admin__media-gallery-image-checkbox" visible="visible" css="$data.additionalClasses"> + <div class="admin__field-control"> + <label class="admin__form-field-label" if="$data.label" attr="for: uid"> + <span translate="label" attr="'data-config-scope': $data.scopeLabel" /> + </label> + </div> + <div class="admin__field admin__field-option"> + <input type="checkbox" + class="admin__control-checkbox" + ko-checked="$data.checked" + disable="disabled" + ko-value="value" + hasFocus="focused" + attr="id: uid, name: inputName"/> + + <label class="admin__field-label" text="description" attr="for: uid"/> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html new file mode 100644 index 0000000000000..a0d21672eafdb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html @@ -0,0 +1,131 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<ifnot args="disableLabel"> + <label class="admin__form-field-label" attr="{for: uid}"> + <span translate="label"></span> + </label> +</ifnot> +<div class="admin__action-multiselect-wrap action-select-wrap media-gallery-asset-ui-select-filter" + tabindex="0" attr="{id: uid}" css="{_active: listVisible,'admin__action-multiselect-tree': isTree()}" + event="{focusin: onFocusIn,focusout: onFocusOut,keydown: keydownSwitcher}" outerClick="outerClick.bind($data)"> + <ifnot args="chipsEnabled"> + <div class="action-select admin__action-multiselect" + data-role="advanced-select" + css="{_active: listVisible}" + click="function(data, event) {toggleListVisible(data, event)}"> + <div class="admin__action-multiselect-text" data-role="selected-option" + ifnot="validationLoading" css="{warning: warn().length}" text="setCaption()"> + </div> + <button if="isRemoveSelectedIcon && hasData() || !validationLoading" class="action-close" + type="button" data-action="remove-selected-item" tabindex="-1" click="clear"> + <span class="action-close-text" translate="'Close'"></span> + </button> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="validationLoading" + if="validationLoading"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> + </div> + </ifnot> + <if args="chipsEnabled"> + <div class="action-select admin__action-multiselect" data-role="advanced-select" + css="{_active: listVisible}" click="function(data, event) {toggleListVisible(data, event)}"> + <div class="admin__action-multiselect-text" visible="!hasData()" + translate="selectedPlaceholders.defaultPlaceholder"> + </div> + <each args="{ data: getSelected(), as: 'option'}"> + <span class="admin__action-multiselect-crumb"> + <span text="label"> + </span> + <button class="action-close" type="button" data-action="remove-selected-item" + tabindex="-1" click="$parent.removeSelected.bind($parent, value)"> + <span class="action-close-text" translate="'Close'"></span> + </button> + </span> + </each> + </div> + </if> + <div class="action-menu" css="{ _active: listVisible}"> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="loading" if="loading"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> + <if args="filterOptions"> + <div class="admin__action-multiselect-search-wrap"> + <input class="admin__control-text admin__action-multiselect-search" data-role="advanced-select-text" + type="text" event="{keydown: filterOptionsKeydown}" attr="{id: uid+2, placeholder: filterPlaceholder}" + ko-focused="filterOptionsFocus" ko-value="filterInputValue" data-bind="valueUpdate:'afterkeydown'"> + <label class="admin__action-multiselect-search-label" + data-action="advanced-select-search" + attr="{for: uid+2}"> + </label> + <div if="itemsQuantity" + text="itemsQuantity" + class="admin__action-multiselect-search-count"> + </div> + </div> + <div ifnot="options().length" + class="admin__action-multiselect-empty-area"> + <ul text="emptyOptionsHtml"/> + </div> + </if> + <ul class="admin__action-multiselect-menu-inner _root" + event="{scroll: function(data, event){onScrollDown(data, event)}}"> + <each args="{ data: options, as: 'option'}"> + <li class="admin__action-multiselect-menu-inner-item _root" + css="{ _parent: $data.optgroup }" + data-role="option-group"> + <div class="action-menu-item" + css="{ + _selected: $parent.isSelectedValue(option), + _hover: $parent.isHovered(option, $element), + _expended: $parent.getLevelVisibility($data) && $parent.showLevels($data), + _unclickable: $parent.isLabelDecoration($data), + _last: $parent.addLastElement($data), + '_with-checkbox': $parent.showCheckbox + }" + click="function(data, event){ + $parent.toggleOptionSelected($data, $index(), event); + }" + data-bind="clickBubble:false"> + <if args="$data.optgroup && $parent.showOpenLevelsActionIcon"> + <div class="admin__action-multiselect-dropdown" + click="function(event){ $parent.showLevels($data); $parent.openChildLevel($data, $element, event);}" + data-bind="clickBubble:false"> + </div> + </if> + <if args="$parent.showCheckbox"> + <input class="admin__control-checkbox" type="checkbox" + tabindex="-1" attr="{ 'checked': $parent.isSelected(option.value) }"> + </if> + <label class="admin__action-multiselect-label"> + <span text="option.label"></span> + <img class="admin__action-multiselect-item-path" + attr="{ src: option.src }"/> + </label> + </div> + <if args="$data.optgroup"> + <render args="{name: $parent.optgroupTmpl, data: {root: $parent, current: $data}}" ></render> + </if> + </li> + </each> + </ul> + <if args="$data.closeBtn"> + <div class="admin__action-multiselect-actions-wrap"> + <button class="action-default" + data-action="close-advanced-select" + type="button" + click="outerClick"> + <span translate="closeBtnLabel"></span> + </button> + </div> + </if> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html new file mode 100644 index 0000000000000..5bbdafebe4095 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html @@ -0,0 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div visible="massActionMode()" class="mediagallery-massaction-items-count"> + <div class="selected_count_text">(<b><text args="getSelectedCount()"/> Selected</b>) </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/massactionButtons.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/massactionButtons.html new file mode 100644 index 0000000000000..a4294434c82bf --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/massactionButtons.html @@ -0,0 +1,13 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<button id="cancel_massaction" type="button" onclick="jQuery(window).trigger('terminateMassAction.MediaGallery')" class="massaction-buttons cancel"> + <span data-bind="i18n: 'Cancel'"/> +</button> +<button id="delete_selected_massaction" onclick="jQuery('#delete_selected_massaction').trigger('massDelete.MediaGallery')" type="button" class="primary massaction-buttons cancel"> + <span data-bind="i18n: 'Delete Selected'"/> +</button> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html new file mode 100644 index 0000000000000..3279856895d77 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html @@ -0,0 +1,15 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<ul class="messages"> + <div class="messages" outereach="messages"> + <div attr="class: 'message message-'+code"> + <div data-ui-id="messages-message-error"> + <span html="$parent.prepareMessageUnsanitizedHtml(message)"></span> + </div> + </div> + </div> +</ul> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html new file mode 100644 index 0000000000000..fb7334a7b0d06 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html @@ -0,0 +1,32 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="admin__data-grid-header" data-role="masonry-main-toolbar" afterRender="$data.setToolbarNode"> + <div class="admin__data-grid-header-row"> + <div class="admin__data-grid-actions-wrap" each="getRegion('dataGridActions')" render=""/> + <each args="getRegion('dataGridFilters')" render=""/> + </div> + <div class="admin__data-grid-header-row row row-gutter"> + <div class="col-xs-2" if="hasChild('listing_massaction')" ko-scope="requestChild('listing_massaction')" render=""/> + <div css=" + 'col-xs-10': hasChild('listing_massaction'), + 'col-xs-12': !hasChild('listing_massaction')"> + <div class="row"> + <div class="col-xs-4"> + <div class="masonry-results-number" ko-scope="requestChild('listing_paging')"> + <render args="totalTmpl"/> + </div> + <each args="getRegion('sorting')" render=""/> + </div> + <div class="col-xs-8" ko-scope="requestChild('listing_paging')"> + <div render=""/> + </div> + </div> + </div> + </div> +</div> + +<render args="stickyTmpl" if="$data.sticky"/> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html new file mode 100644 index 0000000000000..6d5580b1aad6e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html @@ -0,0 +1,17 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="media-gallery-image-uploader-container"> + <form id="image-uploader-form" class="no-display" method="POST" enctype="multipart/form-data"> + <input afterRender="initializeFileUpload" id="image-uploader-input" type="file" name="image" + multiple="multiple"/> + </form> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="loader"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html new file mode 100644 index 0000000000000..3a80116c9225e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html @@ -0,0 +1,19 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons"> + <each args="{ data: actionsList, as: 'action' }"> + <button type="button" click="$parent[action.handler].bind($parent)" + attr="{class: action.classes, id: 'image-details-action-' + action.name, title: $t(action.title)}"> + <span translate="action.title"></span> + </button> + </each> + </div> + </div> +</div> + diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html new file mode 100644 index 0000000000000..15b94f823c2ba --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html @@ -0,0 +1,64 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="image-details" if="image"> + <div class="image-details-image"> + <img attr="src: image().image_url"/> + </div> + <div class="image-details-sidebar"> + <div class="image-details-section"> + <h3 class="image-title" text="image().title"></h3> + <div class="image-type"> + <span class="source"><img if="image().source" class="media-gallery-source-icon" attr="{ src: image().source }" /></span> + <span class="type" data-ui-id="content-type" text="image().content_type"></span> + </div> + </div> + <div class="filename image-details-section"> + <h3 translate="'Filename'"></h3> + <p text="image().path"></p> + </div> + <div class="general-details image-details-section" if="image().details"> + <h3 translate="'Details'"></h3> + <div class="attributes"> + <each args="image().details"> + <div class="attribute" if="value"> + <span if="$parent.notEmpty(value)" class="title" translate="title"></span> + <ifnot args="$parent.isArray(value)"> + <div class="value" text="value"></div> + </ifnot> + <if args="$parent.isArray(value)"> + <each args="{ data: value, as: 'item'}"> + <div class="value"> + <a attr="href: $parents[1].getFilterUrl(item.link)" + text="$parents[1].getUsedInText(item)"></a></br> + </div> + </each> + </if> + </div> + </each> + </div> + </div> + <div class="description image-details-section" if="image().description"> + <h3 translate="'Description'"></h3> + <p text="image().description"></p> + </div> + <div class="tags image-details-section" if="image().tags.length"> + <h3 translate="'Tags'"></h3> + <div class="tags-list" css="{'show-all-tags': showAllTags}"> + <each args="data: image().tags, as: '$tag'"> + <span class="tag-item" text="$parent.getTagText($tag, $index())" + css="{'show-more-item': ($index() + 1) > $parent.tagListLimit}"></span> + </each> + </div> + <div class="show-more-link-container"> + <a href="#" class="show-more-link" if="image().tags.length > tagListLimit" + translate="'Show More'" click="showMoreImageTags"></a> + </div> + </div> + + <each args="getRegion('additional_image_details')" render=""/> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html new file mode 100644 index 0000000000000..80d1e29fd683f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html @@ -0,0 +1,74 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="edit-image-details" if="image"> + <fieldset class="admin__fieldset"> + <input type="hidden" ko-value="image().id" data-ui-id="id" name="id"/> + <div class="admin__field _required"> + <label for="title" class="admin__field-label"> + <span translate="'Title'"></span> + </label> + <div class="admin__field-control"> + <input type="text" id="title" data-ui-id="title" name="title" placeholder="Title" + class="admin__control-text required-entry minimum-length-1 maximum-length-128" + ko-value="image().title" event="{keypress: handleEnterKey}" + data-validate="{'required':true,'validate-image-title':true, 'validate-length':true}"/> + </div> + </div> + <div class="admin__field"> + <label for="path" class="admin__field-label"> + <span translate="'Filename'"></span> + </label> + <div class="admin__field-control path-display"> + <span data-ui-id="path" id="path" text="image().path"></span> + </div> + </div> + <div class="admin__field"> + <label for="description" class="admin__field-label"> + <span translate="'Description'"></span> + </label> + <div class="admin__field-control"> + <textarea id="description" + data-ui-id="description" + name="description" + class="admin__control-textarea minimum-length-0 maximum-length-500" + rows="7" cols="80" + ko-value="image().description" + data-validate="{'validate-image-description':true, 'validate-length':true}"></textarea> + </div> + </div> + <div class="admin__field"> + <label class="admin__field-label"> + <span translate="'Tags'"></span> + </label> + <div class="admin__field-control"> + <div class="admin__field"> + <scope args="keywordsSelect"> + <render args="template"/> + </scope> + </div> + <div class="admin__field"> + <div class="admin__field-control admin__field-option admin__control-grouped"> + <div class="admin__field admin__field-group-additional"> + <div class="admin__field-control"> + <input type="text" id="keyword" data-ui-id="keyword" name="keyword" placeholder="New Tag" + class="admin__control-text minimum-length-0 maximum-length-128" ko-value="newKeyword" + data-validate="{'validate-image-keyword': true, 'validate-length': true}"/> + </div> + </div> + <div class="admin__field admin__field-group-additional admin__field-small"> + <div class="admin__field-control"> + <button type="button" data-ui-id="add-keyword" class="action-basic" click="addKeyword"> + <span translate="'Add New Tag'"></span> + </button> + </div> + </div> + </div> + </div> + </div> + </div> + </fieldset> +</div> diff --git a/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php b/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php new file mode 100644 index 0000000000000..a516ac927fd2d --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUiApi\Api; + +/** + * Class responsible to provide API access to system configuration related to the Media Gallery + */ +interface ConfigInterface +{ + /** + * Check if grid UI is enabled for Magento media gallery + * + * @return bool + */ + public function isEnabled(): bool; +} diff --git a/app/code/Magento/MediaGalleryUiApi/LICENSE.txt b/app/code/Magento/MediaGalleryUiApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/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/MediaGalleryUiApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/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/MediaGalleryUiApi/README.md b/app/code/Magento/MediaGalleryUiApi/README.md new file mode 100644 index 0000000000000..005a445c68b2a --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryUiApi module + +The Magento_MediaGalleryUiApi module is responsible for the media gallery user interface (UI) implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryUiApi 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_MediaGalleryUiApi 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/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json new file mode 100644 index 0000000000000..d577f50523f13 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-gallery-ui-api", + "description": "Magento module responsible for the media gallery UI implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "suggest": { + "magento/module-cms": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryUiApi\\": "" + } + } +} 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/MediaGalleryUiApi/etc/module.xml b/app/code/Magento/MediaGalleryUiApi/etc/module.xml new file mode 100644 index 0000000000000..cf62515ff92b6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/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_MediaGalleryUiApi" /> +</config> diff --git a/app/code/Magento/MediaGalleryUiApi/registration.php b/app/code/Magento/MediaGalleryUiApi/registration.php new file mode 100644 index 0000000000000..b3ee130a1c510 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryUiApi', __DIR__); diff --git a/app/code/Magento/MediaStorage/Model/File/Uploader.php b/app/code/Magento/MediaStorage/Model/File/Uploader.php index 3f3cefe1d6330..173211dfac011 100644 --- a/app/code/Magento/MediaStorage/Model/File/Uploader.php +++ b/app/code/Magento/MediaStorage/Model/File/Uploader.php @@ -6,6 +6,10 @@ namespace Magento\MediaStorage\Model\File; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Validation\ValidationException; +use Magento\MediaStorage\Model\File\Validator\Image; + /** * Core file uploader model * @@ -40,6 +44,11 @@ class Uploader extends \Magento\Framework\File\Uploader */ protected $_validator; + /** + * @var Image + */ + private $imageValidator; + /** * @param string $fileId * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb @@ -130,4 +139,33 @@ public function validateFile() $this->_validateFile(); return $this->_file; } + + /** + * @inheritDoc + * @since 100.4.0 + */ + protected function _validateFile() + { + parent::_validateFile(); + + if (!$this->getImageValidator()->isValid($this->_file['tmp_name'])) { + throw new ValidationException(__('File validation failed.')); + } + } + + /** + * Return image validator class. + * + * Child classes __construct() don't call parent, so we have to retrieve class instance with private function. + * + * @return Image + */ + private function getImageValidator(): Image + { + if (!$this->imageValidator) { + $this->imageValidator = ObjectManager::getInstance()->get(Image::class); + } + + return $this->imageValidator; + } } diff --git a/app/code/Magento/MediaStorage/Model/File/Validator/Image.php b/app/code/Magento/MediaStorage/Model/File/Validator/Image.php new file mode 100644 index 0000000000000..a8bce7cfee20b --- /dev/null +++ b/app/code/Magento/MediaStorage/Model/File/Validator/Image.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaStorage\Model\File\Validator; + +use Magento\Framework\File\Mime; +use Magento\Framework\Filesystem\Driver\File; + +/** + * Image validator + */ +class Image extends \Zend_Validate_Abstract +{ + /** + * @var array + */ + private $imageMimeTypes = [ + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + ]; + + /** + * @var Mime + */ + private $fileMime; + + /** + * @var File + */ + private $file; + + /** + * @param Mime $fileMime + * @param File $file + */ + public function __construct( + Mime $fileMime, + File $file + ) { + $this->fileMime = $fileMime; + $this->file = $file; + } + + /** + * @inheritDoc + */ + public function isValid($filePath): bool + { + $fileMimeType = $this->fileMime->getMimeType($filePath); + $isValid = true; + + if (in_array($fileMimeType, $this->imageMimeTypes)) { + try { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $image = imagecreatefromstring($this->file->fileGetContents($filePath)); + + $isValid = $image ? true : false; + } catch (\Exception $e) { + $isValid = false; + } + } + + return $isValid; + } +} diff --git a/app/code/Magento/MediaStorage/view/adminhtml/templates/system/config/system/storage/media/synchronize.phtml b/app/code/Magento/MediaStorage/view/adminhtml/templates/system/config/system/storage/media/synchronize.phtml index fd437161dfbb0..aaf03b33514c1 100644 --- a/app/code/Magento/MediaStorage/view/adminhtml/templates/system/config/system/storage/media/synchronize.phtml +++ b/app/code/Magento/MediaStorage/view/adminhtml/templates/system/config/system/storage/media/synchronize.phtml @@ -3,11 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<?php /* @var $block \Magento\MediaStorage\Block\System\Config\System\Storage\Media\Synchronize */ ?> +/** + * @var $block \Magento\MediaStorage\Block\System\Config\System\Storage\Media\Synchronize + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ +?> -<script> +<?php +$syncStorageParams = $block->getSyncStorageParams(); +$stateRunning = /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_RUNNING; +$stateFinished = /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_FINISHED; +$stateNotified = /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_NOTIFIED; +$scriptString = <<<script require([ 'jquery', 'prototype', @@ -31,12 +39,14 @@ require([ $('system_media_storage_configuration_media_database').value ); - <?php $syncStorageParams = $block->getSyncStorageParams() ?> - addAllowedStorage(<?= $block->escapeJs($syncStorageParams['storage_type']) ?>, '<?= $block->escapeJs($syncStorageParams['connection_name']) ?>'); + addAllowedStorage({$block->escapeJs($syncStorageParams['storage_type'])}, + '{$block->escapeJs($syncStorageParams['connection_name'])}'); defaultValues = []; - defaultValues['system_media_storage_configuration_media_storage'] = $('system_media_storage_configuration_media_storage').value; - defaultValues['system_media_storage_configuration_media_database'] = $('system_media_storage_configuration_media_database').value; + defaultValues['system_media_storage_configuration_media_storage'] = + $('system_media_storage_configuration_media_storage').value; + defaultValues['system_media_storage_configuration_media_database'] = + $('system_media_storage_configuration_media_database').value; function addAllowedStorage(storageType, connection) @@ -90,7 +100,7 @@ require([ } var checkStatus = function() { - u = new Ajax.PeriodicalUpdater('', '<?= $block->escapeUrl($block->getAjaxStatusUpdateUrl()) ?>', { + u = new Ajax.PeriodicalUpdater('', '{$block->escapeJs($block->getAjaxStatusUpdateUrl())}', { method: 'get', frequency: 5, loaderArea: false, @@ -100,7 +110,7 @@ require([ try { response = JSON.parse(transport.responseText); - if (response.state == '<?= /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_RUNNING ?>' + if (response.state == '{$stateRunning}' && response.message ) { if ($('sync_span').hasClassName('no-display')) { @@ -112,12 +122,12 @@ require([ enableStorageSelection(); $('sync_span').addClassName('no-display'); - if (response.state == '<?= /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_FINISHED ?>') { + if (response.state == '{$stateFinished}') { addAllowedStorage( $('system_media_storage_configuration_media_storage').value, $('system_media_storage_configuration_media_database').value ); - } else if (response.state == '<?= /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_NOTIFIED ?>') { + } else if (response.state == '{$stateNotified}') { if (response.has_errors) { enableSyncButton(); } else { @@ -152,7 +162,7 @@ require([ connection: $('system_media_storage_configuration_media_database').value }; - new Ajax.Request('<?= $block->escapeUrl($block->getAjaxSyncUrl()) ?>', { + new Ajax.Request('{$block->escapeJs($block->getAjaxSyncUrl())}', { parameters: params, loaderArea: false, asynchronous: true @@ -172,11 +182,14 @@ require([ return allowedStorages.include(storage); }, 'Synchronization is required.'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?= $block->getButtonHtml() ?> <span class="sync-indicator no-display" id="sync_span"> - <img alt="Synchronize" style="margin:0 5px" src="<?= $block->escapeUrl($block->getViewFileUrl('images/process_spinner.gif')) ?>"/> + <img alt="Synchronize" src="<?= $block->escapeUrl($block->getViewFileUrl('images/process_spinner.gif')) ?>"/> <span id="sync_message_span"></span> </span> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("margin:0 5px", '#sync_span img') ?> <input type="hidden" id="synchronize-validation-input" class="required-synchronize no-display"/> diff --git a/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php b/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php index fc2207dcd7c86..8ea6290a2a430 100644 --- a/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php +++ b/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php @@ -79,20 +79,15 @@ protected function execute(InputInterface $input, OutputInterface $output) $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); - if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore + if ($singleThread && !$this->lockManager->lock(md5($consumerName),0)) { //phpcs:ignore $output->writeln('<error>Consumer with the same name is running</error>'); return \Magento\Framework\Console\Cli::RETURN_FAILURE; } - if ($singleThread) { - $this->lockManager->lock(md5($consumerName)); //phpcs:ignore - } - $this->appState->setAreaCode($areaCode ?? 'global'); $consumer = $this->consumerFactory->get($consumerName, $batchSize); $consumer->process($numberOfMessages); - if ($singleThread) { $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore } @@ -163,7 +158,7 @@ protected function configure() To specify the preferred area: <comment>%command.full_name% someConsumer --area-code='adminhtml'</comment> - + To do not run multiple copies of one consumer simultaneously: <comment>%command.full_name% someConsumer --single-thread'</comment> diff --git a/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php b/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php new file mode 100644 index 0000000000000..c097f461e621b --- /dev/null +++ b/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model; + +use Magento\Framework\MessageQueue\QueueRepository; + +/** + * Class CheckIsAvailableMessagesInQueue for checking messages available in queue + */ +class CheckIsAvailableMessagesInQueue +{ + /** + * @var QueueRepository + */ + private $queueRepository; + + /** + * Initialize dependencies. + * + * @param QueueRepository $queueRepository + */ + public function __construct(QueueRepository $queueRepository) + { + $this->queueRepository = $queueRepository; + } + + /** + * Checks if there is available messages in the queue + * + * @param string $connectionName connection name + * @param string $queueName queue name + * @return bool + * @throws \LogicException if queue is not available + */ + public function execute($connectionName, $queueName) + { + $queue = $this->queueRepository->get($connectionName, $queueName); + $message = $queue->dequeue(); + if ($message) { + $queue->reject($message); + return true; + } + return false; + } +} diff --git a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php index 056cf4fc57a2e..fd61f96b300d6 100644 --- a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php +++ b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php @@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Process\PhpExecutableFinder; use Magento\Framework\Lock\LockManagerInterface; +use Magento\MessageQueue\Model\CheckIsAvailableMessagesInQueue; /** * Class for running consumers processes by cron @@ -65,6 +66,11 @@ class ConsumersRunner */ private $lockManager; + /** + * @var CheckIsAvailableMessagesInQueue + */ + private $checkIsAvailableMessages; + /** * @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed * for the PHP executable @@ -74,6 +80,7 @@ class ConsumersRunner * @param LockManagerInterface $lockManager The lock manager * @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver * @param LoggerInterface $logger Logger + * @param CheckIsAvailableMessagesInQueue $checkIsAvailableMessages */ public function __construct( PhpExecutableFinder $phpExecutableFinder, @@ -82,7 +89,8 @@ public function __construct( ShellInterface $shellBackground, LockManagerInterface $lockManager, ConnectionTypeResolver $mqConnectionTypeResolver = null, - LoggerInterface $logger = null + LoggerInterface $logger = null, + CheckIsAvailableMessagesInQueue $checkIsAvailableMessages = null ) { $this->phpExecutableFinder = $phpExecutableFinder; $this->consumerConfig = $consumerConfig; @@ -93,6 +101,8 @@ public function __construct( ?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class); $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class); + $this->checkIsAvailableMessages = $checkIsAvailableMessages + ?: ObjectManager::getInstance()->get(CheckIsAvailableMessagesInQueue::class); } /** @@ -166,6 +176,30 @@ private function canBeRun(ConsumerConfigItemInterface $consumerConfig, array $al return false; } + $globalOnlySpawnWhenMessageAvailable = (bool)$this->deploymentConfig->get( + 'queue/only_spawn_when_message_available', + true + ); + if ($consumerConfig->getOnlySpawnWhenMessageAvailable() === true + || ($consumerConfig->getOnlySpawnWhenMessageAvailable() === null && $globalOnlySpawnWhenMessageAvailable)) { + try { + return $this->checkIsAvailableMessages->execute( + $connectionName, + $consumerConfig->getQueue() + ); + } catch (\LogicException $e) { + $this->logger->info( + sprintf( + 'Consumer "%s" skipped as its related queue "%s" is not available. %s', + $consumerName, + $consumerConfig->getQueue(), + $e->getMessage() + ) + ); + return false; + } + } + return true; } } diff --git a/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php b/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php index b73fcc278f970..1aa805f0e323b 100644 --- a/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php +++ b/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php @@ -103,7 +103,6 @@ public function testExecute( $pidFilePath, $singleThread, $lockExpects, - $isLockedExpects, $isLocked, $unlockExpects, $runProcessExpects, @@ -144,14 +143,11 @@ public function testExecute( ->method('get')->with($consumerName, $batchSize)->willReturn($consumer); $consumer->expects($this->exactly($runProcessExpects))->method('process')->with($numberOfMessages); - $this->lockManagerMock->expects($this->exactly($isLockedExpects)) - ->method('isLocked') - ->with(md5($consumerName)) //phpcs:ignore - ->willReturn($isLocked); - $this->lockManagerMock->expects($this->exactly($lockExpects)) ->method('lock') - ->with(md5($consumerName)); //phpcs:ignore + ->with(md5($consumerName))//phpcs:ignore + ->willReturn($isLocked); + $this->lockManagerMock->expects($this->exactly($unlockExpects)) ->method('unlock') ->with(md5($consumerName)); //phpcs:ignore @@ -172,8 +168,7 @@ public function executeDataProvider() 'pidFilePath' => null, 'singleThread' => false, 'lockExpects' => 0, - 'isLockedExpects' => 0, - 'isLocked' => false, + 'isLocked' => true, 'unlockExpects' => 0, 'runProcessExpects' => 1, 'expectedReturn' => Cli::RETURN_SUCCESS, @@ -182,8 +177,7 @@ public function executeDataProvider() 'pidFilePath' => '/var/consumer.pid', 'singleThread' => true, 'lockExpects' => 1, - 'isLockedExpects' => 1, - 'isLocked' => false, + 'isLocked' => true, 'unlockExpects' => 1, 'runProcessExpects' => 1, 'expectedReturn' => Cli::RETURN_SUCCESS, @@ -191,9 +185,8 @@ public function executeDataProvider() [ 'pidFilePath' => '/var/consumer.pid', 'singleThread' => true, - 'lockExpects' => 0, - 'isLockedExpects' => 1, - 'isLocked' => true, + 'lockExpects' => 1, + 'isLocked' => false, 'unlockExpects' => 0, 'runProcessExpects' => 0, 'expectedReturn' => Cli::RETURN_FAILURE, diff --git a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php index fcc4816082919..b907661e14b6b 100644 --- a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php +++ b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php @@ -14,6 +14,7 @@ use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInterface; use Magento\Framework\ShellInterface; use Magento\MessageQueue\Model\Cron\ConsumersRunner; +use Magento\MessageQueue\Model\CheckIsAvailableMessagesInQueue; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\PhpExecutableFinder; @@ -48,10 +49,15 @@ class ConsumersRunnerTest extends TestCase */ private $phpExecutableFinderMock; + /** + * @var CheckIsAvailableMessagesInQueue|MockObject + */ + private $checkIsAvailableMessagesMock; + /** * @var ConnectionTypeResolver */ - private $connectionTypeResover; + private $connectionTypeResolver; /** * @var ConsumersRunner @@ -77,10 +83,11 @@ protected function setUp(): void $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) ->disableOriginalConstructor() ->getMock(); - $this->connectionTypeResover = $this->getMockBuilder(ConnectionTypeResolver::class) + $this->checkIsAvailableMessagesMock = $this->createMock(CheckIsAvailableMessagesInQueue::class); + $this->connectionTypeResolver = $this->getMockBuilder(ConnectionTypeResolver::class) ->disableOriginalConstructor() ->getMock(); - $this->connectionTypeResover->method('getConnectionType')->willReturn('something'); + $this->connectionTypeResolver->method('getConnectionType')->willReturn('something'); $this->consumersRunner = new ConsumersRunner( $this->phpExecutableFinderMock, @@ -88,7 +95,9 @@ protected function setUp(): void $this->deploymentConfigMock, $this->shellBackgroundMock, $this->lockManagerMock, - $this->connectionTypeResover + $this->connectionTypeResolver, + null, + $this->checkIsAvailableMessagesMock ); } @@ -137,22 +146,21 @@ public function testRun( ) { $consumerName = 'consumerName'; - $this->deploymentConfigMock->expects($this->exactly(3)) + $this->deploymentConfigMock ->method('get') ->willReturnMap( [ ['cron_consumers_runner/cron_run', true, true], ['cron_consumers_runner/max_messages', 10000, $maxMessages], ['cron_consumers_runner/consumers', [], $allowedConsumers], + ['queue/only_spawn_when_message_available', null, 0], ] ); /** @var ConsumerConfigInterface|MockObject $firstCunsumer */ $consumer = $this->getMockBuilder(ConsumerConfigItemInterface::class) ->getMockForAbstractClass(); - $consumer->expects($this->any()) - ->method('getName') - ->willReturn($consumerName); + $consumer->method('getName')->willReturn($consumerName); $this->phpExecutableFinderMock->expects($this->once()) ->method('find') @@ -262,4 +270,125 @@ public function runDataProvider() ], ]; } + + /** + * @param boolean $onlySpawnWhenMessageAvailable + * @param boolean $isMassagesAvailableInTheQueue + * @param int $shellBackgroundExpects + * @param boolean $globalOnlySpawnWhenMessageAvailable + * @param int $getOnlySpawnWhenMessageAvailableCallCount + * @param int $isMassagesAvailableInTheQueueCallCount + * @dataProvider runBasedOnOnlySpawnWhenMessageAvailableConsumerConfigurationDataProvider + */ + public function testRunBasedOnOnlySpawnWhenMessageAvailableConsumerConfiguration( + $onlySpawnWhenMessageAvailable, + $isMassagesAvailableInTheQueue, + $shellBackgroundExpects, + $globalOnlySpawnWhenMessageAvailable, + $getOnlySpawnWhenMessageAvailableCallCount, + $isMassagesAvailableInTheQueueCallCount + ) { + $consumerName = 'consumerName'; + $connectionName = 'connectionName'; + $queueName = 'queueName'; + $this->deploymentConfigMock->expects($this->exactly(4)) + ->method('get') + ->willReturnMap( + [ + ['cron_consumers_runner/cron_run', true, true], + ['cron_consumers_runner/max_messages', 10000, 1000], + ['cron_consumers_runner/consumers', [], []], + ['queue/only_spawn_when_message_available', true, $globalOnlySpawnWhenMessageAvailable], + ] + ); + + /** @var ConsumerConfigInterface|MockObject $firstCunsumer */ + $consumer = $this->getMockBuilder(ConsumerConfigItemInterface::class) + ->getMockForAbstractClass(); + $consumer->method('getName')->willReturn($consumerName); + $consumer->expects($this->once()) + ->method('getConnection') + ->willReturn($connectionName); + $consumer->method('getQueue')->willReturn($queueName); + $consumer->expects($this->exactly($getOnlySpawnWhenMessageAvailableCallCount)) + ->method('getOnlySpawnWhenMessageAvailable') + ->willReturn($onlySpawnWhenMessageAvailable); + $this->consumerConfigMock->expects($this->once()) + ->method('getConsumers') + ->willReturn([$consumer]); + + $this->phpExecutableFinderMock->expects($this->once()) + ->method('find') + ->willReturn(''); + + $this->lockManagerMock->expects($this->once()) + ->method('isLocked') + ->willReturn(false); + + $this->checkIsAvailableMessagesMock->expects($this->exactly($isMassagesAvailableInTheQueueCallCount)) + ->method('execute') + ->willReturn($isMassagesAvailableInTheQueue); + + $this->shellBackgroundMock->expects($this->exactly($shellBackgroundExpects)) + ->method('execute'); + + $this->consumersRunner->run(); + } + + /** + * @return array + */ + public function runBasedOnOnlySpawnWhenMessageAvailableConsumerConfigurationDataProvider() + { + return [ + [ + 'onlySpawnWhenMessageAvailable' => true, + 'isMassagesAvailableInTheQueue' => true, + 'shellBackgroundExpects' => 1, + 'globalOnlySpawnWhenMessageAvailable' => false, + 'getOnlySpawnWhenMessageAvailableCallCount' => 1, + 'isMassagesAvailableInTheQueueCallCount' => 1 + ], + [ + 'onlySpawnWhenMessageAvailable' => true, + 'isMassagesAvailableInTheQueue' => false, + 'shellBackgroundExpects' => 0, + 'globalOnlySpawnWhenMessageAvailable' => false, + 'getOnlySpawnWhenMessageAvailableCallCount' => 1, + 'isMassagesAvailableInTheQueueCallCount' => 1 + ], + [ + 'onlySpawnWhenMessageAvailable' => false, + 'isMassagesAvailableInTheQueue' => true, + 'shellBackgroundExpects' => 1, + 'globalOnlySpawnWhenMessageAvailable' => false, + 'getOnlySpawnWhenMessageAvailableCallCount' => 2, + 'isMassagesAvailableInTheQueueCallCount' => 0 + ], + [ + 'onlySpawnWhenMessageAvailable' => null, + 'isMassagesAvailableInTheQueue' => true, + 'shellBackgroundExpects' => 1, + 'globalOnlySpawnWhenMessageAvailable' => true, + 'getOnlySpawnWhenMessageAvailableCallCount' => 2, + 'isMassagesAvailableInTheQueueCallCount' => 1 + ], + [ + 'onlySpawnWhenMessageAvailable' => null, + 'isMassagesAvailableInTheQueue' => true, + 'shellBackgroundExpects' => 1, + 'globalOnlySpawnWhenMessageAvailable' => false, + 'getOnlySpawnWhenMessageAvailableCallCount' => 2, + 'isMassagesAvailableInTheQueueCallCount' => 0 + ], + [ + 'onlySpawnWhenMessageAvailable' => false, + 'isMassagesAvailableInTheQueue' => true, + 'shellBackgroundExpects' => 1, + 'globalOnlySpawnWhenMessageAvailable' => true, + 'getOnlySpawnWhenMessageAvailableCallCount' => 2, + 'isMassagesAvailableInTheQueueCallCount' => 0 + ], + ]; + } } 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/Msrp/Test/Mftf/ActionGroup/AdminSetAdvancedPricingActionGroup.xml b/app/code/Magento/Msrp/Test/Mftf/ActionGroup/AdminSetAdvancedPricingActionGroup.xml new file mode 100644 index 0000000000000..d9a34bbd03e4f --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/ActionGroup/AdminSetAdvancedPricingActionGroup.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="AdminSetAdvancedPricingActionGroup"> + <annotations> + <description>Set advanced pricing and Save product on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="advancedPrice" type="string"/> + </arguments> + + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.msrp}}" stepKey="waitForMsrp"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.msrp}}" userInput="{{advancedPrice}}" stepKey="setMsrpForFirstChildProduct"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <scrollToTopOfPage stepKey="scrollTopPageProduct"/> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveProductButton"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitProductSaveSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveConfirmation"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml index 2bfb1239cba60..1a27bf5aa56a2 100644 --- a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml @@ -132,7 +132,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Clear cache--> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Go to store front and check msrp for products--> <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToConfigProductPage"/> diff --git a/app/code/Magento/Msrp/etc/adminhtml/system.xml b/app/code/Magento/Msrp/etc/adminhtml/system.xml index 8f6c3750c3835..3b9d07be2f7c7 100644 --- a/app/code/Magento/Msrp/etc/adminhtml/system.xml +++ b/app/code/Magento/Msrp/etc/adminhtml/system.xml @@ -14,7 +14,7 @@ <label>Enable MAP</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment> - <![CDATA[<strong style="color:red">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront.]]> + <![CDATA[<strong class="colorRed">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront.]]> </comment> </field> <field id="display_price_type" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" canRestore="1"> diff --git a/app/code/Magento/Msrp/i18n/en_US.csv b/app/code/Magento/Msrp/i18n/en_US.csv index d47d72b2bdc9a..9ed2d2fb86597 100644 --- a/app/code/Magento/Msrp/i18n/en_US.csv +++ b/app/code/Magento/Msrp/i18n/en_US.csv @@ -13,7 +13,7 @@ Price,Price "Add to Cart","Add to Cart" "Minimum Advertised Price","Minimum Advertised Price" "Enable MAP","Enable MAP" -"<strong style=""color:red"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront.","<strong style=""color:red"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront." +"<strong class=""colorRed"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront.","<strong class=""colorRed"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront." "Display Actual Price","Display Actual Price" "Default Popup Text Message","Default Popup Text Message" "Default ""What's This"" Text Message","Default ""What's This"" Text Message" diff --git a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml index b062e911876c3..4e011df66974c 100644 --- a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml +++ b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml @@ -8,6 +8,7 @@ * Template for displaying product price at product view page, gift registry and wish-list * * @var $block \Magento\Msrp\Pricing\Render\PriceBox + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php @@ -32,18 +33,20 @@ $msrpPrice = $block->renderAmount( $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElementIdPrefix() : 'product-price-'; ?> -<?php if ($amount) : ?> +<?php if ($amount): ?> <span class="old-price map-old-price"><?= /* @noEscape */ $msrpPrice ?></span> <span class="map-fallback-price normal-price"><?= /* @noEscape */ $msrpPrice ?></span> <?php endif; ?> -<?php if ($priceType->isShowPriceOnGesture()) : ?> +<?php if ($priceType->isShowPriceOnGesture()): ?> <?php $addToCartUrl = ''; if ($product->isSaleable()) { /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */ - $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton(\Magento\Catalog\Block\Product\AbstractProduct::class); + $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton( + \Magento\Catalog\Block\Product\AbstractProduct::class + ); // phpcs:disable $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl( $product, @@ -86,29 +89,40 @@ $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElem ); } ?> - <span id="<?= $block->escapeHtmlAttr($block->getPriceId() ? $block->getPriceId() : $priceElementId) ?>" style="display:none"></span> - <a href="javascript:void(0);" + <?php $priceId = $block->escapeHtmlAttr($block->getPriceId() ? $block->getPriceId() : $priceElementId); ?> + <span id="s_<?= /* @noEscape*/ $priceId ?>"></span> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none", 'span#s_' . $priceId) ?> + <a href="#" id="<?= /* @noEscape */ ($popupId) ?>" class="action map-show-info" - <?php //phpcs:disable ?> - data-mage-init='{"addToCart":<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($data) ?>}'> - <?php //phpcs:enable ?> + data-mage-init='{"addToCart":<?= /* @noEscape */ $block->jsonEncode($data) ?>}'> <?= $block->escapeHtml(__('Click for price')) ?> </a> -<?php else : ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#' . /* @noEscape */ ($popupId) + ) ?> +<?php else: ?> <span class="msrp-message"> <?= $block->escapeHtml($priceType->getMsrpPriceMessage()) ?> </span> <?php endif; ?> -<?php if ($block->getZone() == \Magento\Framework\Pricing\Render::ZONE_ITEM_VIEW) : ?> +<?php if ($block->getZone() == \Magento\Framework\Pricing\Render::ZONE_ITEM_VIEW): ?> <?php $helpLinkId = 'msrp-help-' . $productId . $block->getRandomString(20); ?> - <a href="javascript:void(0);" + <a href="#" id="<?= /* @noEscape */ $helpLinkId ?>" class="action map-show-info" data-mage-init='{"addToCart":{"origin": "info", "helpLinkId": "#<?= /* @noEscape */ $helpLinkId ?>", - "productName": "<?= $block->escapeJs($block->escapeHtml($product->getName())) ?>", - "closeButtonId": "#map-popup-close"}}'><span><?= $block->escapeHtml(__("What's this?")) ?></span> + "productName": "<?= $block->escapeJs($product->getName()) ?>", + "closeButtonId": "#map-popup-close"}}'> + <span><?= $block->escapeHtml(__("What's this?")) ?></span> </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#' . /* @noEscape */ $helpLinkId + ) ?> <?php endif; ?> diff --git a/app/code/Magento/Msrp/view/frontend/templates/render/item/price_msrp_item.phtml b/app/code/Magento/Msrp/view/frontend/templates/render/item/price_msrp_item.phtml index dfb66e4cc47b2..d2a0982586eed 100644 --- a/app/code/Magento/Msrp/view/frontend/templates/render/item/price_msrp_item.phtml +++ b/app/code/Magento/Msrp/view/frontend/templates/render/item/price_msrp_item.phtml @@ -10,8 +10,10 @@ * Template for displaying product price at product view page, gift registry and wishlist * * @var $block \Magento\Catalog\Block\Product\Price + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> + <?php //phpcs:disable /** @var $pricingHelper \Magento\Framework\Pricing\Helper\Data */ @@ -26,31 +28,47 @@ $_msrpPrice = ''; ?> <div class="price-box msrp"> - <?php if ($_product->getMsrp()) : ?> + <?php if ($_product->getMsrp()): ?> <?php $_msrpPrice = $pricingHelper->currency($_product->getMsrp(), true, false) ?> <span class="old-price"><?= /* @noEscape */ $_msrpPrice ?></span> <?php endif; ?> - <?php if ($_catalogHelper->isShowPriceOnGesture($_product)) : ?> + <?php if ($_catalogHelper->isShowPriceOnGesture($_product)): ?> <?php $priceElementId = 'product-price-' . $_id . $block->getIdSuffix(); ?> - <span id="<?= /* @noEscape */ $priceElementId ?>" style="display: none"></span> + <span id="<?= /* @noEscape */ $priceElementId ?>"/> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none", '#'. $priceElementId) ?> + <?php $popupId = 'msrp-popup-' . $_id . $block->getRandomString(20); ?> - <a href="javascript:void(0);" + <a href="#" id="<?= /* @noEscape */ ($popupId) ?>" data-mage-init='{"addToCart":{"popupId": "#<?= /* @noEscape */ ($popupId) ?>", - "productName": "<?= /* @noEscape */ $block->escapeJs($block->escapeHtml($_product->getName())) ?>", + "productName": "<?= /* @noEscape */ $block->escapeJs($_product->getName()) ?>", "realPrice": <?= /* @noEscape */ $block->getRealPriceJs($_product) ?>, "msrpPrice": "<?= /* @noEscape */ $_msrpPrice ?>", "priceElementId":"<?= /* @noEscape */ $priceElementId ?>", "popupCartButtonId": "#map-popup-button", - "cartForm": "#wishlist-view-form"}}'><?= $block->escapeHtml(__('Click for price')) ?> + "cartForm": "#wishlist-view-form"}}'> + <?= $block->escapeHtml(__('Click for price')) ?> </a> - <?php else : ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#' . /* @noEscape */ ($popupId) + ) ?> + <?php else: ?> <span class="msrp-message"> <?= $block->escapeHtml($_catalogHelper->getMsrpPriceMessage($_product)) ?> </span> <?php endif; ?> <?php $helpLinkId = 'msrp-help-' . $_id . $block->getRandomString(20); ?> - <a href="javascript:void(0);" id="<?= /* @noEscape */ ($helpLinkId) ?>" data-mage-init='{"addToCart":{"helpLinkId": "#<?= /* @noEscape */ ($helpLinkId) ?>", "productName": "<?= /* @noEscape */$block->escapeJs($block->escapeHtml($_product->getName())) ?>"}}' class="link tip"> + <a href="#" id="<?= /* @noEscape */ ($helpLinkId) ?>" + data-mage-init='{"addToCart":{"helpLinkId": "#<?= /* @noEscape */ ($helpLinkId) ?>", + "productName": "<?= /* @noEscape */$block->escapeJs($_product->getName()) ?>"}}' + class="link tip"> <?= $block->escapeHtml(__("What's this?")) ?> </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#' . /* @noEscape */ ($helpLinkId) + ) ?> </div> diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index d17da90c58bef..1ea2dc2618778 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -8,6 +8,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Quote\Model\Quote\Address; +use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Framework\App\ObjectManager; /** * Multishipping checkout overview information @@ -15,6 +17,7 @@ * @api * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Overview extends \Magento\Sales\Block\Items\AbstractItems { @@ -56,6 +59,7 @@ class Overview extends \Magento\Sales\Block\Items\AbstractItems * @param \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector * @param \Magento\Quote\Model\Quote\TotalsReader $totalsReader * @param array $data + * @param CheckoutHelper|null $checkoutHelper */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -64,11 +68,14 @@ public function __construct( PriceCurrencyInterface $priceCurrency, \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector, \Magento\Quote\Model\Quote\TotalsReader $totalsReader, - array $data = [] + array $data = [], + ?CheckoutHelper $checkoutHelper = null ) { $this->_taxHelper = $taxHelper; $this->_multishipping = $multishipping; $this->priceCurrency = $priceCurrency; + $data['taxHelper'] = $this->_taxHelper; + $data['checkoutHelper'] = $checkoutHelper ?? ObjectManager::getInstance()->get(CheckoutHelper::class); parent::__construct($context, $data); $this->_isScopePrivate = true; $this->totalsCollector = $totalsCollector; @@ -393,7 +400,7 @@ public function getQuote() * Get billin address totals * * @return mixed - * @deprecated + * @deprecated 100.2.3 * typo in method name, see getBillingAddressTotals() */ public function getBillinAddressTotals() @@ -405,6 +412,7 @@ public function getBillinAddressTotals() * Get billing address totals * * @return mixed + * @since 100.2.3 */ public function getBillingAddressTotals() { diff --git a/app/code/Magento/Multishipping/Block/Checkout/Results.php b/app/code/Magento/Multishipping/Block/Checkout/Results.php index 35c050d5ff8c1..40cbce1990d00 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Results.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Results.php @@ -21,6 +21,7 @@ * Multi-shipping checkout results information * * @api + * @since 100.2.1 */ class Results extends Success { @@ -66,6 +67,7 @@ public function __construct( * Returns shipping addresses from quote. * * @return array + * @since 100.2.1 */ public function getQuoteShippingAddresses(): array { @@ -76,6 +78,7 @@ public function getQuoteShippingAddresses(): array * Returns all failed addresses from quote. * * @return array + * @since 100.2.1 */ public function getFailedAddresses(): array { @@ -91,6 +94,7 @@ public function getFailedAddresses(): array * * @param int $orderId * @return OrderAddress|null + * @since 100.2.1 */ public function getOrderShippingAddress(int $orderId) { @@ -101,6 +105,7 @@ public function getOrderShippingAddress(int $orderId) * Retrieve quote billing address. * * @return QuoteAddress + * @since 100.2.1 */ public function getQuoteBillingAddress(): QuoteAddress { @@ -112,6 +117,7 @@ public function getQuoteBillingAddress(): QuoteAddress * * @param OrderAddress $address * @return string + * @since 100.2.1 */ public function formatOrderShippingAddress(OrderAddress $address): string { @@ -123,6 +129,7 @@ public function formatOrderShippingAddress(OrderAddress $address): string * * @param QuoteAddress $address * @return string + * @since 100.2.1 */ public function formatQuoteShippingAddress(QuoteAddress $address): string { @@ -134,6 +141,7 @@ public function formatQuoteShippingAddress(QuoteAddress $address): string * * @param QuoteAddress $address * @return bool + * @since 100.2.1 */ public function isShippingAddress(QuoteAddress $address): bool { @@ -158,6 +166,7 @@ private function getAddressOneline(array $address): string * * @param QuoteAddress $address * @return string + * @since 100.2.1 */ public function getAddressError(QuoteAddress $address): string { @@ -171,6 +180,7 @@ public function getAddressError(QuoteAddress $address): string * * @throws LocalizedException * @return Success + * @since 100.2.1 */ protected function _prepareLayout(): Success { 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/RemoveItem.php b/app/code/Magento/Multishipping/Controller/Checkout/RemoveItem.php index 1bb333faaf2e4..d6d8ec9de0d58 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/RemoveItem.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/RemoveItem.php @@ -6,7 +6,14 @@ */ namespace Magento\Multishipping\Controller\Checkout; -class RemoveItem extends \Magento\Multishipping\Controller\Checkout +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class RemoveItem + * + * Removes multishipping items + */ +class RemoveItem extends \Magento\Multishipping\Controller\Checkout implements HttpPostActionInterface { /** * Multishipping checkout remove item action 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/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php index 5d384a5373d5e..85726a8dab0b5 100644 --- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php +++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php @@ -13,6 +13,7 @@ * Place orders during multishipping checkout flow. * * @api + * @since 100.2.1 */ interface PlaceOrderInterface { @@ -21,6 +22,7 @@ interface PlaceOrderInterface * * @param OrderInterface[] $orderList * @return array + * @since 100.2.1 */ public function place(array $orderList): array; } diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontRemoveProductOnCheckoutActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontRemoveProductOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..af0f3e2d597b8 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontRemoveProductOnCheckoutActionGroup.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="StorefrontRemoveProductOnCheckoutActionGroup"> + <arguments> + <argument name="itemNumber" type="string" defaultValue="1"/> + </arguments> + + <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/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml index 02187658a8781..815d406c68bfa 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml @@ -19,12 +19,6 @@ <group value="Multishipment"/> <group value="SalesRule"/> </annotations> - <before> - <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> - </before> - <after> - <magentoCLI command="config:set multishipping/options/checkout_multiple 0" stepKey="disableShippingToMultipleAddresses"/> - </after> <actionGroup ref="AdminCreateCartPriceRuleActionsWithSubtotalActionGroup" before="goToProduct1" stepKey="createSubtotalCartPriceRuleActionsSection"> <argument name="ruleName" value="CartPriceRuleConditionForSubtotalForMultiShipping"/> </actionGroup> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml index a49a37e475409..8205ab962b9fe 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml @@ -22,8 +22,6 @@ <before> <!-- Login as Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <!-- Set configurations --> - <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> <!-- Create simple products --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="firstProduct"> 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/StorefrontOrderWithMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml index 80407a219a841..2e5c0acc32053 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml @@ -27,7 +27,6 @@ <createData entity="SimpleProduct2" stepKey="createProduct2"/> <createData entity="Simple_US_Customer_Two_Addresses" stepKey="createCustomer"/> <!-- Set configurations --> - <magentoCLI command="config:set {{EnableMultiShippingCheckoutMultiple.path}} {{EnableMultiShippingCheckoutMultiple.value}}" stepKey="allowShippingToMultipleAddresses"/> <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShipping"/> <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShipping"/> <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> @@ -43,7 +42,6 @@ <!-- Need logout before customer delete. Fatal error appears otherwise --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <magentoCLI command="config:set {{DisableMultiShippingCheckoutMultiple.path}} {{DisableMultiShippingCheckoutMultiple.value}}" stepKey="withdrawShippingToMultipleAddresses"/> <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllOrdersGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml index caf0ce3a51bae..46f1daad053d5 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml @@ -22,8 +22,6 @@ <before> <!-- Login as Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <!-- Set configurations --> - <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> <!-- Create two simple products --> <createData entity="ApiCategory" stepKey="createCategory"/> <createData entity="_defaultProduct" stepKey="createFirstProduct"> @@ -103,7 +101,7 @@ <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabSecondOrderId})}}" stepKey="seeSecondOrder"/> <waitForPageLoad stepKey="waitForOrderPageLoad"/> <!-- Go to Admin > Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrdersPage"/> <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchFirstOrder"> <argument name="keyword" value="$grabFirstOrderId"/> </actionGroup> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml index e65747f4d63d0..fe33078755ac4 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml @@ -33,19 +33,22 @@ <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"/> <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <deleteData createDataKey="product" stepKey="deleteProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> 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 f9ab587c65fa3..430b16b8cc237 100644 --- a/app/code/Magento/Multishipping/i18n/en_US.csv +++ b/app/code/Magento/Multishipping/i18n/en_US.csv @@ -87,8 +87,9 @@ Options,Options "Maximum Qty Allowed for Shipping to Multiple Addresses","Maximum Qty Allowed for Shipping to Multiple Addresses" "Review Order","Review Order" "Select Shipping Method","Select Shipping Method" -"We received your order!","We received your order!" +"The order was not successful!","The order was not successful!" "Ship to:","Ship to:" "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/Multishipping/view/frontend/templates/checkout/addresses.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml index 418efa5033263..a37ff04a8dc2a 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml @@ -4,8 +4,8 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Files.LineLength - +// phpcs:disable Generic.Files.LineLength +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper ?> <?php /** @@ -16,7 +16,7 @@ ?> <form id="checkout_multishipping_form" data-mage-init='{ - "multiShipping":{}, + "multiShipping": {"itemsQty": <?= /* @noEscape */ (int)$block->getCheckout()->getQuote()->getItemsSummaryQty() ?>}, "cartUpdate": { "validationURL": "<?= $block->escapeUrl($block->getUrl('multishipping/checkout/checkItems')) ?>", "eventName": "updateMulticartItemQty" @@ -43,8 +43,8 @@ </tr> </thead> <tbody> - <?php foreach ($block->getItems() as $_index => $_item) : ?> - <?php if ($_item->getQuoteItem()) : ?> + <?php foreach ($block->getItems() as $_index => $_item): ?> + <?php if ($_item->getQuoteItem()): ?> <tr> <td class="col product" data-th="<?= $block->escapeHtml(__('Product')) ?>"> <?= $block->getItemHtml($_item->getQuoteItem()) ?> @@ -69,11 +69,11 @@ </div> </td> <td class="col address" data-th="<?= $block->escapeHtml(__('Send To')) ?>"> - <?php if ($_item->getProduct()->getIsVirtual()) : ?> + <?php if ($_item->getProduct()->getIsVirtual()): ?> <div class="applicable"> <?= $block->escapeHtml(__('A shipping selection is not applicable.')) ?> </div> - <?php else : ?> + <?php else: ?> <div class="field address"> <label for="ship_<?= $block->escapeHtml($_index) ?>_<?= $block->escapeHtml($_item->getQuoteItemId()) ?>_address" class="label"> @@ -86,8 +86,12 @@ <?php endif; ?> </td> <td class="col actions" data-th="<?= $block->escapeHtml(__('Actions')) ?>"> - <a href="<?= $block->escapeUrl($block->getItemDeleteUrl($_item)) ?>" + <a href="#" title="<?= $block->escapeHtml(__('Remove Item')) ?>" + data-post='<?= /* @noEscape */ + $this->helper(\Magento\Framework\Data\Helper\PostHelper::class) + ->getPostData($block->getItemDeleteUrl($_item)) + ?>' class="action delete" data-multiship-item-remove=""> <span><?= $block->escapeHtml(__('Remove item')) ?></span> @@ -106,7 +110,7 @@ class="action primary continue<?= $block->isContinueDisabled() ? ' disabled' : '' ?>" data-role="can-continue" data-flag="1" - <?php if ($block->isContinueDisabled()) : ?> + <?php if ($block->isContinueDisabled()): ?> disabled="disabled" <?php endif; ?>> <span><?= $block->escapeHtml(__('Go to Shipping Information')) ?></span> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml index 761c1f1a78423..c9ee0a8b12ce3 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml @@ -8,20 +8,24 @@ * Multishipping checkout billing information * * @var $block \Magento\Multishipping\Block\Checkout\Billing + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="checkout-loader" data-role="checkout-loader" class="loading-mask" data-mage-init='{"billingLoader": {}}'> <div class="loader"> <img src="<?= $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')); ?>" - alt="<?= $block->escapeHtml(__('Loading...')); ?>" - style="position: absolute;"> + alt="<?= $block->escapeHtml(__('Loading...')); ?>"> </div> </div> -<script> - window.checkoutConfig = <?= /* @noEscape */ $block->getCheckoutData()->getSerializedCheckoutConfigs(); ?>; +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag('position: absolute;', 'div#checkout-loader .loader img') ?> +<?php $checkoutConfig = /* @noEscape */ $block->getCheckoutData()->getSerializedCheckoutConfigs(); +$scriptString = <<<script + window.checkoutConfig = {$checkoutConfig}; window.isCustomerLoggedIn = window.checkoutConfig.isCustomerLoggedIn; window.customerData = window.checkoutConfig.customerData; -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <div id="checkout" data-bind="scope:'checkoutMessages'"> <!-- ko template: getTemplate() --><!-- /ko --> <script type="text/x-magento-init"> @@ -72,7 +76,7 @@ $methodsCount = count($methods); $methodsForms = $block->hasFormTemplates() ? $block->getFormTemplates(): []; - foreach ($methods as $_method) : + foreach ($methods as $_method): $code = $_method->getCode(); $checked = $block->getSelectedMethodCode() === $code; @@ -82,7 +86,7 @@ ?> <div data-bind="scope: 'payment_method_<?= $block->escapeHtml($code);?>'"> <dt class="item-title"> - <?php if ($methodsCount > 1) : ?> + <?php if ($methodsCount > 1): ?> <input type="radio" id="p_method_<?= $block->escapeHtml($code); ?>" value="<?= $block->escapeHtml($code); ?>" @@ -93,11 +97,11 @@ checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()" - <?php if ($checked) : ?> + <?php if ($checked): ?> checked="checked" <?php endif; ?> class="radio"/> - <?php else : ?> + <?php else: ?> <input type="radio" id="p_method_<?= $block->escapeHtml($code); ?>" value="<?= $block->escapeHtml($code); ?>" @@ -112,7 +116,7 @@ <?= $block->escapeHtml($_method->getTitle()) ?> </label> </dt> - <?php if ($html = $block->getChildHtml('payment.method.' . $code)) : ?> + <?php if ($html = $block->getChildHtml('payment.method.' . $code)): ?> <dd class="item-content <?= $checked ? '' : 'no-display'; ?>"> <?= /* @noEscape */ $html; ?> </dd> @@ -142,12 +146,14 @@ </div> </div> </form> -<script> +<?php $quoteBaseGrandTotal = (float)$block->getQuoteBaseGrandTotal(); +$scriptString = <<<script + require(['jquery', 'mage/mage'], function(jQuery) { var addtocartForm = jQuery('#multishipping-billing-form'); addtocartForm.mage('payment', { - checkoutPrice: <?= (float)$block->getQuoteBaseGrandTotal() ?> + checkoutPrice: {$quoteBaseGrandTotal} }); addtocartForm.mage('validation', { @@ -160,9 +166,13 @@ } }); }); -</script> -<script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + +<?php $scriptString = <<<script + //<![CDATA[ require( [ @@ -171,21 +181,27 @@ 'domReady!' ], function(quote, $) { quote.billingAddress({ - city: '<?= /* @noEscape */ $block->getAddress()->getCity() ?>', - company: '<?= /* @noEscape */ $block->getAddress()->getCompany(); ?>', - countryId: '<?= /* @noEscape */ $block->getAddress()->getCountryId(); ?>', - customerAddressId: '<?= /* @noEscape */ $block->getAddress()->getCustomerAddressId(); ?>', - customerId: '<?= /* @noEscape */ $block->getAddress()->getCustomerId(); ?>', - fax: '<?= /* @noEscape */ $block->getAddress()->getFax(); ?>', - firstname: '<?= /* @noEscape */ $block->getAddress()->getFirstname(); ?>', - lastname: '<?= /* @noEscape */ $block->getAddress()->getLastname(); ?>', - postcode: '<?= /* @noEscape */ $block->getAddress()->getPostcode(); ?>', - regionId: '<?= /* @noEscape */ $block->getAddress()->getRegionId(); ?>', - regionCode: '<?= /* @noEscape */ $block->getAddress()->getRegionCode() ?>', - region: '<?= /* @noEscape */ $block->getAddress()->getRegion(); ?>', - street: <?= /* @noEscape */ json_encode($block->getAddress()->getStreet()); ?>, - telephone: '<?= /* @noEscape */ $block->getAddress()->getTelephone(); ?>' + +script; +$scriptString .= "city: '" . /* @noEscape */ $block->getAddress()->getCity() . "'," . PHP_EOL; +$scriptString .= "company: '" . /* @noEscape */ $block->getAddress()->getCompany() . "'," . PHP_EOL; +$scriptString .= "countryId: '" . /* @noEscape */ $block->getAddress()->getCountryId() . "'," . PHP_EOL; +$scriptString .= "customerAddressId: '" . /* @noEscape */ $block->getAddress()->getCustomerAddressId() . "'," . PHP_EOL; +$scriptString .= "customerId: '" . /* @noEscape */ $block->getAddress()->getCustomerId() . "'," . PHP_EOL; +$scriptString .= "fax: '" . /* @noEscape */ $block->getAddress()->getFax() . "'," . PHP_EOL; +$scriptString .= "firstname: '" . /* @noEscape */ $block->getAddress()->getFirstname() . "'," . PHP_EOL; +$scriptString .= "lastname: '" . /* @noEscape */ $block->getAddress()->getLastname() . "'," . PHP_EOL; +$scriptString .= "postcode: '" . /* @noEscape */ $block->getAddress()->getPostcode() . "'," . PHP_EOL; +$scriptString .= "regionId: '" . /* @noEscape */ $block->getAddress()->getRegionId() . "'," . PHP_EOL; +$scriptString .= "regionCode: '" . /* @noEscape */ $block->getAddress()->getRegionCode() . "'," . PHP_EOL; +$scriptString .= "region: '" . /* @noEscape */ $block->getAddress()->getRegion() . "'," . PHP_EOL; +$scriptString .= "street: " . /* @noEscape */ json_encode($block->getAddress()->getStreet()) . "," . PHP_EOL; +$scriptString .= "telephone: '" . /* @noEscape */ $block->getAddress()->getTelephone() . "'" . PHP_EOL; +$scriptString .= <<<script }); }); //]]> -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml index 5fff0d72e8000..3b72679bfc34e 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml @@ -4,12 +4,18 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** @var \Magento\Multishipping\Block\Checkout\Overview $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> + +<?php +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); +/** @var \Magento\Checkout\Helper\Data $checkoutHelper */ +$checkoutHelper = $block->getData('checkoutHelper'); ?> <?php $errors = $block->getCheckoutData()->getAddressErrors(); ?> -<?php foreach ($errors as $addressId => $error) : ?> +<?php foreach ($errors as $addressId => $error): ?> <div class="message message-error error"> <?= $block->escapeHtml($error); ?> <?= $block->escapeHtml(__('Please see')); ?> @@ -59,8 +65,8 @@ </div> <div class="block block-shipping"> <div class="block-title"><strong><?= $block->escapeHtml(__('Shipping Information')); ?></strong></div> - <?php $mergedCells = ($this->helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices() ? 2 : 1); ?> - <?php foreach ($block->getShippingAddresses() as $index => $address) : ?> + <?php $mergedCells = ($taxHelper->displayCartBothPrices() ? 2 : 1); ?> + <?php foreach ($block->getShippingAddresses() as $index => $address): ?> <div class="block-content"> <a name="<?= $block->escapeHtml($block->getCheckoutData() ->getAddressAnchorName($address->getId())); ?>"></a> @@ -72,7 +78,7 @@ </span> </strong> </div> - <?php if ($error = $block->getCheckoutData()->getAddressError($address)) : ?> + <?php if ($error = $block->getCheckoutData()->getAddressError($address)): ?> <div class="error-description"><?= $block->escapeHtml($error); ?></div> <?php endif;?> <div class="box box-shipping-address"> @@ -93,17 +99,16 @@ <a href="<?= $block->escapeUrl($block->getEditShippingUrl()); ?>" class="action edit"><span><?= $block->escapeHtml(__('Change')); ?></span></a> </strong> - <?php if ($_rate = $block->getShippingAddressRate($address)) : ?> + <?php if ($_rate = $block->getShippingAddressRate($address)): ?> <div class="box-content"> <?= $block->escapeHtml($_rate->getCarrierTitle()) ?> (<?= $block->escapeHtml($_rate->getMethodTitle()) ?>) <?php $exclTax = $block->getShippingPriceExclTax($address); $inclTax = $block->getShippingPriceInclTax($address); - $displayBothPrices = $this->helper(Magento\Tax\Helper\Data::class) - ->displayShippingBothPrices() && $inclTax !== $exclTax; + $displayBothPrices = $taxHelper->displayShippingBothPrices() && $inclTax !== $exclTax; ?> - <?php if ($displayBothPrices) : ?> + <?php if ($displayBothPrices): ?> <span class="price-including-tax" data-label="<?= $block->escapeHtml(__('Incl. Tax')); ?>"> <?= /* @noEscape */ $inclTax ?> @@ -112,7 +117,7 @@ data-label="<?= $block->escapeHtml(__('Excl. Tax')); ?>"> <?= /* @noEscape */ $exclTax; ?> </span> - <?php else : ?> + <?php else: ?> <?= /* @noEscape */ $inclTax ?> <?php endif; ?> </div> @@ -138,7 +143,7 @@ </tr> </thead> <tbody> - <?php foreach ($block->getShippingAddressItems($address) as $item) : ?> + <?php foreach ($block->getShippingAddressItems($address) as $item): ?> <?= /* @noEscape */ $block->getRowItemHtml($item) ?> <?php endforeach; ?> </tbody> @@ -155,13 +160,13 @@ <?php endforeach; ?> </div> - <?php if ($block->getQuote()->hasVirtualItems()) : ?> + <?php if ($block->getQuote()->hasVirtualItems()): ?> <div class="block block-other"> <?php $billingAddress = $block->getQuote()->getBillingAddress(); ?> <a name="<?= $block->escapeHtml($block->getCheckoutData() ->getAddressAnchorName($billingAddress->getId())); ?>"></a> <div class="block-title"><strong><?= $block->escapeHtml(__('Other items in your order')); ?></strong></div> - <?php if ($error = $block->getCheckoutData()->getAddressError($billingAddress)) :?> + <?php if ($error = $block->getCheckoutData()->getAddressError($billingAddress)): ?> <div class="error-description"><?= $block->escapeHtml($error); ?></div> <?php endif;?> <div class="block-content"> @@ -170,7 +175,7 @@ <a href="<?= $block->escapeUrl($block->getVirtualProductEditUrl()); ?>" class="action edit"><span><?= $block->escapeHtml(__('Edit Items')); ?></span></a> </strong> - <?php $mergedCells = ($this->helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices() ? 2 : 1); ?> + <?php $mergedCells = ($taxHelper->displayCartBothPrices() ? 2 : 1); ?> <div class="order-review-wrapper table-wrapper"> <table class="items data table table-order-review" id="virtual-overview-table"> <caption class="table-caption"><?= $block->escapeHtml(__('Items')); ?></caption> @@ -183,7 +188,7 @@ </tr> </thead> <tbody> - <?php foreach ($block->getVirtualItems() as $_item) : ?> + <?php foreach ($block->getVirtualItems() as $_item): ?> <?= /* @noEscape */ $block->getRowItemHtml($_item) ?> <?php endforeach; ?> </tbody> @@ -203,8 +208,7 @@ <div class="grand totals"> <strong class="mark"><?= $block->escapeHtml(__('Grand Total:')); ?></strong> <strong class="amount"> - <?= /* @noEscape */ $this->helper(Magento\Checkout\Helper\Data::class) - ->formatPrice($block->getTotal()); ?> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()); ?> </strong> </div> <div class="actions-toolbar" id="review-buttons-container"> @@ -221,10 +225,10 @@ </div> <span id="review-please-wait" class="please-wait load indicator" - style="display: none;" data-text="<?= $block->escapeHtml(__('Submitting order information...')); ?>"> <span><?= $block->escapeHtml(__('Submitting order information...')); ?></span> </span> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'span#review-please-wait') ?> </div> </div> </form> diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js b/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js index 537abb3aa2071..8af1c1ed06fc1 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js @@ -5,12 +5,14 @@ define([ 'jquery', + 'Magento_Customer/js/customer-data', 'jquery-ui-modules/widget' -], function ($) { +], function ($, customerData) { 'use strict'; $.widget('mage.multiShipping', { options: { + itemsQty: 0, addNewAddressBtn: 'button[data-role="add-new-address"]', // Add a new multishipping address. addNewAddressFlag: '#add_new_address_flag', // Hidden input field with value 0 or 1. canContinueBtn: 'button[data-role="can-continue"]', // Continue (update quantity or go to shipping). @@ -22,10 +24,24 @@ define([ * @private */ _create: function () { + this._prepareCartData(); $(this.options.addNewAddressBtn).on('click', $.proxy(this._addNewAddress, this)); $(this.options.canContinueBtn).on('click', $.proxy(this._canContinue, this)); }, + /** + * Takes cart items qty from current cart data and compare it with current items qty + * Reloads cart data if cart items qty is wrong + * @private + */ + _prepareCartData: function () { + var cartData = customerData.get('cart'); + + if (cartData()['summary_count'] !== this.options.itemsQty) { + customerData.reload(['cart'], false); + } + }, + /** * Add a new address. Set the hidden input field and submit the form. Then enter a new shipping address. * @private 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/Block/Adminhtml/Queue/Preview.php b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php index dd6f51a5342f2..69512775f4e93 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php @@ -26,8 +26,8 @@ class Preview extends \Magento\Newsletter\Block\Adminhtml\Template\Preview /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Newsletter\Model\TemplateFactory $templateFactory - * @param \Magento\Newsletter\Model\QueueFactory $queueFactory * @param \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory + * @param \Magento\Newsletter\Model\QueueFactory $queueFactory * @param array $data */ public function __construct( @@ -42,6 +42,8 @@ public function __construct( } /** + * Return template. + * * @param \Magento\Newsletter\Model\Template $template * @param string $id * @return $this @@ -50,9 +52,11 @@ protected function loadTemplate(\Magento\Newsletter\Model\Template $template, $i { /** @var \Magento\Newsletter\Model\Queue $queue */ $queue = $this->_queueFactory->create()->load($id); + $template->setId($queue->getTemplateId()); $template->setTemplateType($queue->getNewsletterType()); $template->setTemplateText($queue->getNewsletterText()); $template->setTemplateStyles($queue->getNewsletterStyles()); + return $this; } } 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 8fc729ea34078..cf4aad2059a01 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,6 +9,9 @@ use Magento\Framework\App\TemplateTypesInterface; use Magento\Framework\Exception\LocalizedException; +/** + * An action that saves a template. + */ class Save extends \Magento\Newsletter\Controller\Adminhtml\Template implements HttpPostActionInterface { /** @@ -32,9 +34,7 @@ public function execute() } try { - $template->addData( - $request->getParams() - )->setTemplateSubject( + $template->setTemplateSubject( $request->getParam('subject') )->setTemplateCode( $request->getParam('code') @@ -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/Queue/TransportBuilder.php b/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php index aa3a2bcfe0f59..0f20a8379d04b 100644 --- a/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php +++ b/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php @@ -185,7 +185,7 @@ public function setReplyTo($email, $name = null) * @throws MailException * @see setFromByScope() * - * @deprecated This function sets the from address but does not provide + * @deprecated 100.3.3 This function sets the from address but does not provide * a way of setting the correct from addresses based on the scope. */ public function setFrom($from) diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php index 33c539fbba84f..2914a25ba7214 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php @@ -213,6 +213,7 @@ public function addSubscriberFilter($subscriberId) * * @param int $customerId * @return $this + * @since 100.4.0 */ public function addCustomerFilter(int $customerId): Collection { diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php index 6391219e23c7e..fb5a4fd915734 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php @@ -116,6 +116,8 @@ public function setMessagesScope($scope) * @param string $email * @param int $websiteId * @return array + * @since 100.4.0 + * @throws LocalizedException */ public function loadBySubscriberEmail(string $email, int $websiteId): array { @@ -140,6 +142,7 @@ public function loadBySubscriberEmail(string $email, int $websiteId): array * @param int $customerId * @param int $websiteId * @return array + * @since 100.4.0 */ public function loadByCustomerId(int $customerId, int $websiteId): array { @@ -199,7 +202,7 @@ public function received(SubscriberModel $subscriber, \Magento\Newsletter\Model\ * * @param string $subscriberEmail * @return array - * @deprecated The subscription should be loaded by website id + * @deprecated 100.4.0 The subscription should be loaded by website id * @see loadBySubscriberEmail */ public function loadByEmail($subscriberEmail) @@ -213,7 +216,7 @@ public function loadByEmail($subscriberEmail) * * @param CustomerInterface $customer * @return array - * @deprecated The subscription should be loaded by website id + * @deprecated 100.4.0 The subscription should be loaded by website id * @see loadByCustomerId */ public function loadByCustomerData(CustomerInterface $customer) diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php index 5c573f47aa0bf..c2d80f9000792 100644 --- a/app/code/Magento/Newsletter/Model/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/Subscriber.php @@ -382,6 +382,7 @@ public function isSubscribed() * @param string $email * @param int $websiteId * @return $this + * @since 100.4.0 */ public function loadBySubscriberEmail(string $email, int $websiteId): Subscriber { @@ -400,6 +401,7 @@ public function loadBySubscriberEmail(string $email, int $websiteId): Subscriber * @param int $customerId * @param int $websiteId * @return $this + * @since 100.4.0 */ public function loadByCustomer(int $customerId, int $websiteId): Subscriber { @@ -588,6 +590,7 @@ public function getSubscriberFullName() * Set date of last changed status * * @return $this + * @since 100.2.1 */ public function beforeSave() { @@ -603,7 +606,7 @@ public function beforeSave() * * @param string $subscriberEmail * @return $this - * @deprecated The subscription should be loaded by website id + * @deprecated 100.4.0 The subscription should be loaded by website id * @see loadBySubscriberEmail */ public function loadByEmail($subscriberEmail) @@ -619,7 +622,7 @@ public function loadByEmail($subscriberEmail) * * @param int $customerId * @return $this - * @deprecated The subscription should be loaded by website id + * @deprecated 100.4.0 The subscription should be loaded by website id * @see loadByCustomer */ public function loadByCustomerId($customerId) @@ -644,7 +647,7 @@ public function loadByCustomerId($customerId) * * @param string $email * @return int - * @deprecated The subscription should be updated by store id + * @deprecated 100.4.0 The subscription should be updated by store id * @see \Magento\Newsletter\Model\SubscriptionManager::subscribe */ public function subscribe($email) @@ -661,7 +664,7 @@ public function subscribe($email) * * @param int $customerId * @return $this - * @deprecated The subscription should be updated by store id + * @deprecated 100.4.0 The subscription should be updated by store id * @see \Magento\Newsletter\Model\SubscriptionManager::subscribeCustomer */ public function subscribeCustomerById($customerId) @@ -674,7 +677,7 @@ public function subscribeCustomerById($customerId) * * @param int $customerId * @return $this - * @deprecated The subscription should be updated by store id + * @deprecated 100.4.0 The subscription should be updated by store id * @see \Magento\Newsletter\Model\SubscriptionManager::unsubscribeCustomer */ public function unsubscribeCustomerById($customerId) @@ -687,7 +690,7 @@ public function unsubscribeCustomerById($customerId) * * @param int $customerId * @return $this - * @deprecated The subscription should be updated by store id + * @deprecated 100.4.0 The subscription should be updated by store id * @see \Magento\Newsletter\Model\SubscriptionManager::subscribeCustomer */ public function updateSubscription($customerId) @@ -703,7 +706,7 @@ public function updateSubscription($customerId) * @param int $customerId * @param bool $subscribe indicates whether the customer should be subscribed or unsubscribed * @return $this - * @deprecated The subscription should be updated by store id + * @deprecated 100.4.0 The subscription should be updated by store id * @see \Magento\Newsletter\Model\SubscriptionManager::subscribeCustomer */ protected function _updateCustomerSubscription($customerId, $subscribe) 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/Model/Template.php b/app/code/Magento/Newsletter/Model/Template.php index 88fbfb152d14f..58afd45b26f44 100644 --- a/app/code/Magento/Newsletter/Model/Template.php +++ b/app/code/Magento/Newsletter/Model/Template.php @@ -40,7 +40,7 @@ class Template extends \Magento\Email\Model\AbstractTemplate /** * Mail object * - * @deprecated Unused property + * @deprecated 100.3.0 Unused property * */ protected $_mail; diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingDeleteNewsletterSubscriberFromGridActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingDeleteNewsletterSubscriberFromGridActionGroup.xml new file mode 100644 index 0000000000000..ed60c1509e453 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingDeleteNewsletterSubscriberFromGridActionGroup.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="AdminMarketingDeleteNewsletterSubscriberFromGridActionGroup"> + <click selector="{{AdminNewsletterSubscriberGridSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + <selectOption selector="{{AdminNewsletterSubscriberGridSection.actionsDropdown}}" userInput="Delete" stepKey="selectDelete"/> + <click selector="{{AdminNewsletterSubscriberGridSection.submit}}" stepKey="clickSubmitBtn"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminNewsletterSubscriberGridSection.okButton}}" stepKey="clickOkButton"/> + <waitForPageLoad stepKey="waitForResultsLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingFindNewsletterSubscribersInGridActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingFindNewsletterSubscribersInGridActionGroup.xml new file mode 100644 index 0000000000000..9f9231397a1f8 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingFindNewsletterSubscribersInGridActionGroup.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="AdminMarketingFindNewsletterSubscribersInGridActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + + <click stepKey="resetFilter" selector="{{AdminNewsletterSubscriberGridSection.resetFilter}}"/> + <waitForPageLoad stepKey="waitForPageLoading"/> + <fillField stepKey="fillEmailField" selector="{{AdminNewsletterSubscriberGridSection.emailField}}" userInput="{{email}}"/> + <click stepKey="clickSearchButton" selector="{{AdminNewsletterSubscriberGridSection.searchButton}}"/> + <waitForPageLoad stepKey="waitForResultsLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AssertAdminDeletedNewsletterSubscriberIsNotInGridActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AssertAdminDeletedNewsletterSubscriberIsNotInGridActionGroup.xml new file mode 100644 index 0000000000000..e114a4e640fa2 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AssertAdminDeletedNewsletterSubscriberIsNotInGridActionGroup.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="AssertAdminDeletedNewsletterSubscriberIsNotInGridActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + <dontSee selector="{{AdminNewsletterSubscriberGridSection.email('1')}}" userInput="{{email}}" stepKey="dontSeeSubscriber"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewAccountNewsletterUncheckedActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewAccountNewsletterUncheckedActionGroup.xml index d6b0adff53a86..8037baa6b199c 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewAccountNewsletterUncheckedActionGroup.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewAccountNewsletterUncheckedActionGroup.xml @@ -15,10 +15,11 @@ <arguments> <argument name="Customer"/> <argument name="Store"/> + <argument name="StoreGroup"/> </arguments> <amOnPage stepKey="amOnStorefrontPage" url="{{Store.code}}"/> <see stepKey="seeDescriptionNewsletter" userInput="You aren't subscribed to our newsletter." selector="{{CustomerMyAccountPage.DescriptionNewsletter}}"/> - <see stepKey="seeThankYouMessage" userInput="Thank you for registering with NewStore."/> + <see stepKey="seeThankYouMessage" userInput="Thank you for registering with {{StoreGroup.name}}."/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewSubscriberActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewSubscriberActionGroup.xml index 0aee2cb9b2e3c..482ecec583552 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewSubscriberActionGroup.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewSubscriberActionGroup.xml @@ -8,7 +8,8 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="StorefrontCreateNewSubscriberActionGroup"> + <actionGroup name="StorefrontCreateNewSubscriberActionGroup" deprecated="Use StorefrontCreateNewsletterSubscriberActionGroup"> + <!-- Deprecated Due to inconsistency with the best practices --> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> <submitForm selector="{{BasicFrontendNewsletterFormSection.subscribeForm}}" parameterArray="['email' => '{{_defaultNewsletter.senderEmail}}']" diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml new file mode 100644 index 0000000000000..44104f3adf0d9 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.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="StorefrontCreateNewsletterSubscriberActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + <fillField stepKey="fillEmailField" selector="{{BasicFrontendNewsletterFormSection.newsletterEmail}}" userInput="{{email}}"/> + <click selector="{{BasicFrontendNewsletterFormSection.subscribeButton}}" stepKey="submitForm"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/AdminNewsletterSubscriberGridSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/AdminNewsletterSubscriberGridSection.xml index 3332041817150..26512a28c9f3d 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Section/AdminNewsletterSubscriberGridSection.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/AdminNewsletterSubscriberGridSection.xml @@ -12,5 +12,12 @@ <element name="type" type="text" selector="//table[contains(@class, 'data-grid')]/tbody/tr[{{row}}][@data-role='row']/td[@data-column='type']" parameterized="true"/> <element name="firstName" type="text" selector="//table[contains(@class, 'data-grid')]/tbody/tr[{{row}}][@data-role='row']/td[@data-column='firstname']" parameterized="true"/> <element name="lastName" type="text" selector="//table[contains(@class, 'data-grid')]/tbody/tr[{{row}}][@data-role='row']/td[@data-column='lastname']" parameterized="true"/> + <element name="resetFilter" type="button" selector=".action-default.scalable.action-reset.action-tertiary"/> + <element name="emailField" type="input" selector=".col-email #subscriberGrid_filter_email"/> + <element name="searchButton" type="button" selector="//*[@class='admin__filter-actions']//*[text()='Search']"/> + <element name="rowCheckbox" type="checkbox" selector="table.data-grid tbody > tr:nth-of-type({{row}}) td.data-grid-checkbox-cell input" parameterized="true"/> + <element name="actionsDropdown" type="select" selector=".admin__grid-massaction-form #subscriberGrid_massaction-select"/> + <element name="submit" type="button" selector="//*[@class='admin__grid-massaction-form']//*[text()='Submit']"/> + <element name="okButton" type="button" selector="//footer[@class='modal-footer']/button[contains(@class, 'action-accept')]" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection/BasicFrontendNewsletterFormSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection/BasicFrontendNewsletterFormSection.xml index 8475fb4d55b9e..f4c685e730be3 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection/BasicFrontendNewsletterFormSection.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection/BasicFrontendNewsletterFormSection.xml @@ -8,8 +8,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="BasicFrontendNewsletterFormSection"> - <element name="newsletterEmail" type="input" selector="#newsletter"/> - <element name="subscribeButton" type="button" selector=".subscribe" timeout="30"/> + <element name="newsletterEmail" type="input" selector=".control #newsletter"/> + <element name="subscribeButton" type="button" selector=".actions .action.subscribe.primary" timeout="30"/> <element name="subscribeForm" type="input" selector="#newsletter-validate-detail" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml index a763f43d9e4d1..e255f14a83661 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml @@ -35,6 +35,9 @@ <waitForPageLoad stepKey="waitForPageLoad" /> <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> @@ -57,6 +60,10 @@ <seeElementInDOM selector="{{StorefrontNewsletterSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> <closeTab stepKey="closeTab"/> <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml new file mode 100644 index 0000000000000..c472d262a34c8 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingDeleteNewsletterSubscriberTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Subscribers Deleting"/> + <title value="Admin deletes newsletter subscribers"/> + <description value="Admin should be able delete newsletter subscribers"/> + <severity value="CRITICAL"/> + <group value="newsletter"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCreatedCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerNavigateToNewsletterPageActionGroup" stepKey="navigateToNewsletterPage"/> + <actionGroup ref="StorefrontCustomerUpdateGeneralSubscriptionActionGroup" stepKey="subscribeToNewsletter"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterSubscribersPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsNewsletterSubscribers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminMarketingFindNewsletterSubscribersInGridActionGroup" stepKey="findSubscriber"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> + <actionGroup ref="AdminMarketingDeleteNewsletterSubscriberFromGridActionGroup" stepKey="deleteSubscriber"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="Total of 1 record(s) were deleted."/> + </actionGroup> + <actionGroup ref="AdminMarketingFindNewsletterSubscribersInGridActionGroup" stepKey="findDeletedSubscriber"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> + <actionGroup ref="AssertAdminDeletedNewsletterSubscriberIsNotInGridActionGroup" stepKey="dontSeeSubscriber"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest.xml new file mode 100644 index 0000000000000..6c62434f1620e --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Disabled Guest Newsletter Subscription"/> + <title value="Newsletter Subscription for guest is disabled and cannot be performed"/> + <description value="Guest cannot subscribe to Newsletter if it is disallowed in configurations"/> + <severity value="AVERAGE"/> + <group value="newsletter"/> + <group value="configuration"/> + <testCaseId value="MC-35728"/> + </annotations> + <before> + <magentoCLI stepKey="disableGuestSubscription" command="config:set newsletter/subscription/allow_guest_subscribe 0"/> + <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + </before> + <after> + <magentoCLI stepKey="allowGuestSubscription" command="config:set newsletter/subscription/allow_guest_subscribe 1"/> + <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + </after> + + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStorefrontPage"/> + <actionGroup ref="StorefrontCreateNewsletterSubscriberActionGroup" stepKey="createSubscription"> + <argument name="email" value="{{_defaultNewsletter.senderEmail}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertErrorMessageActionGroup" stepKey="assertMessage"> + <argument name="message" value="Sorry, but the administrator denied subscription for guests. Please register."/> + <argument name="messageType" value="error"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml index c38725f263525..8ae592a17d620 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml @@ -28,11 +28,15 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest.xml index cffce8da1d710..dbce742aa0eef 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest.xml @@ -8,7 +8,8 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest"> + <test name="VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest" deprecated="Use StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest"> + <!-- Deprecated Due to inconsistency with the best practices --> <annotations> <features value="Newsletter"/> <stories value="Configure guest newsletter subscription to 'No'"/> @@ -22,6 +23,11 @@ <magentoCLI command="config:set newsletter/subscription/allow_guest_subscribe 0" stepKey="setConfigGuestSubscriptionDisable"/> </before> - <actionGroup ref="StorefrontCreateNewSubscriberActionGroup" stepKey="createSubscriber"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <submitForm selector="{{BasicFrontendNewsletterFormSection.subscribeForm}}" + parameterArray="['email' => '{{_defaultNewsletter.senderEmail}}']" + button="{{BasicFrontendNewsletterFormSection.subscribeButton}}" stepKey="submitForm"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible stepKey="waitForErrorAppears" selector="{{StorefrontMessagesSection.error}}"/> </test> </tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml index a568fb1799ac2..63b2741e7bd15 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml @@ -16,39 +16,46 @@ <title value="Newsletter subscription when user is registered on 2 stores"/> <description value="Newsletter subscription when user is registered on 2 stores"/> <severity value="MAJOR"/> - <testCaseId value="MAGETWO-93836"/> + <testCaseId value="MC-25840"/> </annotations> <before> <!--Log in to Magento as admin.--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> - <argument name="newWebsiteName" value="Second"/> - <argument name="websiteCode" value="Base2"/> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> - <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> - <argument name="website" value="Second"/> - <argument name="storeGroupName" value="NewStore"/> - <argument name="storeGroupCode" value="Base12"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> </actionGroup> - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> - <argument name="StoreGroup" value="staticStoreGroup"/> - <argument name="customStore" value="staticStore"/> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> </actionGroup> - <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> + <after> <!--Delete created data and set Default Configuration--> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> - <argument name="websiteName" value="Second"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> </actionGroup> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> @@ -62,7 +69,8 @@ <!--Create new Account with the same email address. (unchecked Sign Up for Newsletter checkbox)--> <actionGroup ref="StorefrontCreateNewAccountNewsletterUncheckedActionGroup" stepKey="createNewAccountNewsletterUnchecked"> <argument name="Customer" value="CustomerEntityOne"/> - <argument name="Store" value="staticStore"/> + <argument name="Store" value="customStore"/> + <argument name="StoreGroup" value="customStoreGroup"/> </actionGroup> </test> </tests> diff --git a/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php b/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php index 199eeac377759..d3b6495df680f 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php @@ -100,7 +100,23 @@ protected function setUp(): void ->willReturn($backendSession); $templateFactory = $this->createPartialMock(TemplateFactory::class, ['create']); - $this->templateMock = $this->createMock(Template::class); + $this->templateMock = $this->getMockBuilder(Template::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'isPlain', + 'setId', + ] + ) + ->addMethods( + [ + 'setTemplateType', + 'setTemplateText', + 'setTemplateStyles', + ] + ) + ->getMock(); + $templateFactory->expects($this->once()) ->method('create') ->willReturn($this->templateMock); @@ -112,7 +128,22 @@ protected function setUp(): void ->willReturn($this->subscriberMock); $queueFactory = $this->createPartialMock(QueueFactory::class, ['create']); - $this->queueMock = $this->createPartialMock(Queue::class, ['load']); + $this->queueMock = $this->getMockBuilder(Queue::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'load', + ] + ) + ->addMethods( + [ + 'getTemplateId', + 'getNewsletterType', + 'getNewsletterText', + 'getNewsletterStyles', + ] + ) + ->getMock(); $queueFactory->expects($this->any()) ->method('create') ->willReturn($this->queueMock); @@ -130,7 +161,7 @@ protected function setUp(): void 'context' => $context, 'templateFactory' => $templateFactory, 'subscriberFactory' => $subscriberFactory, - 'queueFactory' => $queueFactory + 'queueFactory' => $queueFactory, ] ); } @@ -148,17 +179,29 @@ public function testToHtmlEmpty() public function testToHtmlWithId() { + $templateId = 1; + $newsletterType = 2; + $newsletterText = 'newsletter text'; + $newsletterStyle = 'style'; $this->requestMock->expects($this->any())->method('getParam')->willReturnMap( [ ['id', null, 1], - ['store_id', null, 0] + ['store_id', null, 0], ] ); $this->queueMock->expects($this->once()) ->method('load')->willReturnSelf(); + $this->queueMock->expects($this->once())->method('getTemplateId')->willReturn($templateId); + $this->queueMock->expects($this->once())->method('getNewsletterType')->willReturn($newsletterType); + $this->queueMock->expects($this->once())->method('getNewsletterText')->willReturn($newsletterText); + $this->queueMock->expects($this->once())->method('getNewsletterStyles')->willReturn($newsletterStyle); $this->templateMock->expects($this->any()) ->method('isPlain') ->willReturn(true); + $this->templateMock->expects($this->once())->method('setId')->willReturn($templateId); + $this->templateMock->expects($this->once())->method('setTemplateType')->willReturn($newsletterType); + $this->templateMock->expects($this->once())->method('setTemplateText')->willReturn($newsletterText); + $this->templateMock->expects($this->once())->method('setTemplateStyles')->willReturn($newsletterStyle); /** @var Store $store */ $this->storeManagerMock->expects($this->once()) ->method('getDefaultStoreView') 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 912c6a1df8729..6139d86191f44 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriptionManagerTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriptionManagerTest.php @@ -136,11 +136,8 @@ public function testSubscribe( ->with(Subscriber::XML_PATH_CONFIRMATION_FLAG, ScopeInterface::SCOPE_STORE, $storeId) ->willReturn($isConfirmNeed); - $this->assertEquals( - $subscriber, - $this->subscriptionManager->subscribe($email, $storeId) - ); - $this->assertEquals($subscriber->getData(), $expectedData); + $this->assertEquals($subscriber, $this->subscriptionManager->subscribe($email, $storeId)); + $this->assertEquals($expectedData, $subscriber->getData()); } /** @@ -308,7 +305,7 @@ public function testSubscribeCustomer( $subscriber, $this->subscriptionManager->subscribeCustomer($customerId, $storeId) ); - $this->assertEquals($subscriber->getData(), $expectedData); + $this->assertEquals($expectedData, $subscriber->getData()); } /** @@ -457,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' => [ @@ -553,7 +550,7 @@ public function testUnsubscribeCustomer( $subscriber, $this->subscriptionManager->unsubscribeCustomer($customerId, $storeId) ); - $this->assertEquals($subscriber->getData(), $expectedData); + $this->assertEquals($expectedData, $subscriber->getData()); } /** @@ -621,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' => [ @@ -645,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/Newsletter/composer.json b/app/code/Magento/Newsletter/composer.json index cc0d717a1958d..790370c328644 100644 --- a/app/code/Magento/Newsletter/composer.json +++ b/app/code/Magento/Newsletter/composer.json @@ -14,7 +14,8 @@ "magento/module-email": "*", "magento/module-require-js": "*", "magento/module-store": "*", - "magento/module-widget": "*" + "magento/module-widget": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Newsletter/i18n/en_US.csv b/app/code/Magento/Newsletter/i18n/en_US.csv index f390f6792635d..f8706967117fe 100644 --- a/app/code/Magento/Newsletter/i18n/en_US.csv +++ b/app/code/Magento/Newsletter/i18n/en_US.csv @@ -153,3 +153,6 @@ Store,Store "Newsletter Subscriptions","Newsletter Subscriptions" "We have updated your subscription.","We have updated your subscription." "Are you sure you want to delete the selected subscriber(s)?","Are you sure you want to delete the selected subscriber(s)?" +"Cannot create a newsletter subscription.","Cannot create a newsletter subscription." +"Enter a valid email address.","Enter a valid email address." +"Guests can not subscribe to the newsletter. You must create an account to subscribe.","Guests can not subscribe to the newsletter. You must create an account to subscribe." diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml index 20ff63a60a263..62b368b8911f8 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -5,30 +5,31 @@ */ /** @var \Magento\Backend\Block\Page $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="preview" class="cms-revision-preview"> <div class="toolbar"> - <?php if (!$block->isSingleStoreMode()) :?> - <div class="store-switcher"> - <?= $block->getChildHtml('store_switcher') ?> - </div> + <?php if (!$block->isSingleStoreMode()):?> + <div class="store-switcher"> + <?= $block->getChildHtml('store_switcher') ?> + </div> <?php endif;?> </div> <iframe - name="preview_iframe" - id="preview_iframe" - class="preview_iframe" - frameborder="0" - title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" - width="100%" - sandbox="allow-forms allow-pointer-lock" + name="preview_iframe" + id="preview_iframe" + class="preview_iframe" + frameborder="0" + title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" + width="100%" + sandbox="allow-forms allow-pointer-lock" > </iframe> <?= $block->getChildHtml('preview_form') ?> </div> -<script> +<?php $scriptString = <<<script require(['jquery', 'loadingPopup', 'prototype'], function(jQuery){ //<![CDATA[ @@ -61,4 +62,6 @@ jQuery("#preview_iframe").load(function() { //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml index d5a24fad2ac91..896b8ce773c2d 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($websites = $block->getWebsites()) : ?> +<?php if ($websites = $block->getWebsites()): ?> <div class="field field-store-switcher"> <label class="label" for="store_switcher"><?= $block->escapeHtml(__('Choose Store View:')) ?></label> <div class="control"> @@ -13,22 +15,25 @@ id="store_switcher" class="admin__control-select" name="store_switcher"> - <?php foreach ($websites as $website) : ?> + <?php foreach ($websites as $website): ?> <?php $showWebsite = false; ?> - <?php foreach ($website->getGroups() as $group) : ?> + <?php foreach ($website->getGroups() as $group): ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStores($group) as $store) : ?> - <?php if ($showWebsite == false) : ?> + <?php foreach ($block->getStores($group) as $store): ?> + <?php if ($showWebsite == false): ?> <?php $showWebsite = true; ?> <optgroup label="<?= $block->escapeHtmlAttr($website->getName()) ?>"></optgroup> <?php endif; ?> - <?php if ($showGroup == false) : ?> + <?php if ($showGroup == false): ?> <?php $showGroup = true; ?> <optgroup label="   <?= $block->escapeHtmlAttr($group->getName()) ?>"> <?php endif; ?> - <option value="<?= $block->escapeHtmlAttr($store->getId()) ?>"<?php if ($block->getStoreId() == $store->getId()) : ?> selected="selected"<?php endif; ?>>    <?= $block->escapeHtml($store->getName()) ?></option> + <option value="<?= $block->escapeHtmlAttr($store->getId()) ?>" + <?php if ($block->getStoreId() == $store->getId()): ?> selected="selected"<?php endif; ?>> +     <?= $block->escapeHtml($store->getName()) ?> + </option> <?php endforeach; ?> - <?php if ($showGroup) : ?> + <?php if ($showGroup): ?> </optgroup> <?php endif; ?> <?php endforeach; ?> @@ -37,7 +42,7 @@ </div> <?= $block->getHintHtml() ?> </div> -<script> + <?php $scriptString= <<<script require(['prototype'], function(){ //<![CDATA[ @@ -48,5 +53,7 @@ Event.observe($('store_switcher'), 'change', function(event) { //]]> }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml index b697be4cf753a..8148af3221922 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml @@ -3,21 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= $block->getChildHtml('grid') ?> -<?php if ($block->getShowButtons()) : ?> +<?php if ($block->getShowButtons()): ?> <div class="form-buttons"> <?= $block->getUnsubscribeButtonHtml() ?> <?= $block->getDeleteButtonHtml() ?> </div> <?php endif ?> -<script> +<?php $scriptString = <<<script require(["prototype", "mage/adminhtml/events"], function(){ problemController = { checkCheckboxes:function (controlCheckbox) { - var elements = $$('input.problemCheckbox'); + var elements = \$$('input.problemCheckbox'); if (elements && elements.length) { elements.each(function (obj) { obj.checked = controlCheckbox.checked; @@ -35,7 +37,7 @@ require(["prototype", "mage/adminhtml/events"], function(){ }, unsubscribe:function () { - var elements = $$('input.problemCheckbox'); + var elements = \$$('input.problemCheckbox'); var serializedElements = Form.serializeElements(elements, true); serializedElements._unsubscribe = '1'; serializedElements.form_key = FORM_KEY; @@ -48,7 +50,7 @@ require(["prototype", "mage/adminhtml/events"], function(){ }, deleteSelected:function () { - var elements = $$('input.problemCheckbox'); + var elements = \$$('input.problemCheckbox'); var serializedElements = Form.serializeElements(elements, true); serializedElements._delete = '1'; serializedElements.form_key = FORM_KEY; @@ -65,4 +67,6 @@ require(["prototype", "mage/adminhtml/events"], function(){ //--> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml index 3d52cc0dee777..eb2e3f2b399f2 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml @@ -5,16 +5,16 @@ */ /* @var $block \Magento\Newsletter\Block\Adminhtml\Queue\Edit */ - +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> <?= $block->getBackButtonHtml() ?> <?= $block->getPreviewButtonHtml() ?> - <?php if (!$block->getIsPreview()) : ?> + <?php if (!$block->getIsPreview()): ?> <?= $block->getResetButtonHtml() ?> <?= $block->getSaveButtonHtml() ?> <?php endif ?> - <?php if ($block->getCanResume()) : ?> + <?php if ($block->getCanResume()): ?> <?= $block->getResumeButtonHtml() ?> <?php endif ?> </div> @@ -23,16 +23,18 @@ <?= $block->getBlockHtml('formkey') ?> <?= $block->getChildHtml('form') ?> </form> -<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="newsletter_queue_preview_form" target="_blank"> +<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="newsletter_queue_preview_form" + target="_blank"> <?= $block->getBlockHtml('formkey') ?> <div class="no-display"> - <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->getIsTextType() ? 1 : 2 ?>" /> + <input type="hidden" id="preview_type" name="type" + value="<?= /* @noEscape */ $block->getIsTextType() ? 1 : 2 ?>" /> <input type="hidden" id="preview_text" name="text" value="" /> <input type="hidden" id="preview_styles" name="styles" value="" /> <input type="hidden" id="preview_id" name="id" value="" /> </div> </form> -<script> +<?php $scriptString= <<<script require([ 'jquery', 'wysiwygAdapter', @@ -71,4 +73,6 @@ queueControl = { //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml index 13bd5d5118be0..b69a89fc296dc 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml @@ -5,20 +5,29 @@ */ /** @var \Magento\Newsletter\Block\Adminhtml\Subscriber $block */ - +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= $block->getChildHtml('grid') ?> -<?php if (count($block->getQueueAsOptions())>0 && $block->getShowQueueAdd()) : ?> +<?php if (count($block->getQueueAsOptions())>0 && $block->getShowQueueAdd()): ?> <div class="form-buttons"> <select id="queueList" name="queue"> - <?php foreach ($block->getQueueAsOptions() as $_queue) : ?> - <option value="<?= $block->escapeHtmlAttr($_queue['value']) ?>"><?= $block->escapeHtml($_queue['label']) ?> #<?= $block->escapeHtml($_queue['value']) ?></option> + <?php foreach ($block->getQueueAsOptions() as $_queue): ?> + <option value="<?= $block->escapeHtmlAttr($_queue['value']) ?>"> + <?= $block->escapeHtml($_queue['label']) ?> #<?= $block->escapeHtml($_queue['value']) ?> + </option> <?php endforeach; ?> </select> - <button type="button" class="scalable" onclick="subscriberController.addToQueue();"><span><span><span><?= $block->escapeHtml(__('Add to Queue')) ?></span></span></span></button> + <button type="button" class="scalable" id="addToQueue"> + <span><span><span><?= $block->escapeHtml(__('Add to Queue')) ?></span></span></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'subscriberController.addToQueue();', + 'button#addToQueue' + ) ?> </div> <?php endif ?> -<script> +<?php $scriptString= <<<script require(["prototype", "mage/adminhtml/events"], function(){ subscriberController = { checkCheckboxes: function(controlCheckbox) { @@ -53,4 +62,6 @@ require(["prototype", "mage/adminhtml/events"], function(){ varienGlobalEvents.attachEventHandler('gridRowClick', subscriberController.rowClick.bind(subscriberController)); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml index abc56070b6892..29555130de1ae 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml @@ -7,26 +7,29 @@ use Magento\Framework\App\TemplateTypesInterface; /* @var $block \Magento\Newsletter\Block\Adminhtml\Template\Edit */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="newsletter_template_edit_form"> <?= $block->getBlockHtml('formkey') ?> <div class="no-display"> <input type="hidden" id="change_flag_element" name="_change_type_flag" value="" /> - <input type="hidden" id="save_as_flag" name="_save_as_flag" value="<?= $block->escapeHtmlAttr($block->getSaveAsFlag()) ?>" /> + <input type="hidden" id="save_as_flag" name="_save_as_flag" + value="<?= $block->escapeHtmlAttr($block->getSaveAsFlag()) ?>" /> </div> <?= /* @noEscape */ $block->getForm() ?> </form> -<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="newsletter_template_preview_form" target="_blank"> +<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="newsletter_template_preview_form" + target="_blank"> <div class="no-display"> - <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>" /> + <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>"/> <input type="hidden" id="preview_text" name="text" value="" /> <input type="hidden" id="preview_styles" name="styles" value="" /> <input type="hidden" id="preview_id" name="id" value="" /> <input type="hidden" name="form_key" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" > </div> </form> -<script> +<?php $scriptString = <<<script require([ 'jquery', 'wysiwygAdapter', @@ -91,7 +94,7 @@ require([ var self = this; confirm({ - content: "<?= $block->escapeJs($block->escapeHtml(__('Are you sure that you want to strip all tags?'))) ?>", + content: "{$block->escapeJs(__('Are you sure that you want to strip all tags?'))}", actions: { confirm: function () { if (wysiwyg.activeEditor()) { @@ -140,10 +143,10 @@ require([ $('change_flag_element').value = '1'; } - if ($F('code').blank() || $F('code') == templateControl.templateName) { + if (\$F('code').blank() || \$F('code') == templateControl.templateName) { prompt({ - content: '<?= $block->escapeJs($block->escapeHtml(__('Please enter a new template name.'))) ?>', - value: templateControl.templateName + '<?= $block->escapeJs(__(' Copy')) ?>', + content: '{$block->escapeJs(__('Please enter a new template name.'))}', + value: templateControl.templateName + '{$block->escapeJs(__(' Copy'))}', actions: { confirm: function (value) { $('code').value = value; @@ -174,9 +177,9 @@ require([ preview: function () { if (this.typeChange) { - $('preview_type').value = <?= $block->escapeJs(TemplateTypesInterface::TYPE_TEXT) ?>; + $('preview_type').value = {$block->escapeJs(TemplateTypesInterface::TYPE_TEXT)}; } else { - $('preview_type').value = <?= $block->escapeJs($block->getTemplateType()) ?>; + $('preview_type').value = {$block->escapeJs($block->getTemplateType())}; } if (wysiwyg.activeEditor()) { @@ -200,10 +203,10 @@ require([ deleteTemplate: function () { confirm({ - content: "<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to delete this template?'))) ?>", + content: "{$block->escapeJs(__('Are you sure you want to delete this template?'))}", actions: { confirm: function () { - window.location.href = '<?= $block->escapeUrl($block->getDeleteUrl()) ?>'; + window.location.href = '{$block->escapeJs($block->getDeleteUrl())}'; } } }); @@ -211,8 +214,10 @@ require([ }; templateControl.init(); - templateControl.templateName = "<?= $block->escapeJs($block->getJsTemplateName()) ?>"; + templateControl.templateName = "{$block->escapeJs($block->getJsTemplateName())}"; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html b/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html index d56163c10fdf3..996dff0c973e9 100644 --- a/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html +++ b/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html @@ -13,6 +13,6 @@ {{template config_path="design/email/header_template"}} -{{trans "You have been successfully subscribed to our newsletter."}} +<p>{{trans "You have been successfully subscribed to our newsletter."}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html b/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html index d39b5d8a8b8e9..1f222f85abac7 100644 --- a/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html +++ b/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html @@ -13,6 +13,6 @@ {{template config_path="design/email/header_template"}} -{{trans "You have been unsubscribed from the newsletter."}} +<p>{{trans "You have been unsubscribed from the newsletter."}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml index 429482e5795bf..768c97ef316f7 100644 --- a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml +++ b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml @@ -40,3 +40,12 @@ </form> </div> </div> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "newsletter-validate-detail" + } + } + } +</script> diff --git a/app/code/Magento/NewsletterGraphQl/Model/Resolver/SubscribeEmailToNewsletter.php b/app/code/Magento/NewsletterGraphQl/Model/Resolver/SubscribeEmailToNewsletter.php new file mode 100644 index 0000000000000..a4b3bc43d0a8b --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/Model/Resolver/SubscribeEmailToNewsletter.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\NewsletterGraphQl\Model\Resolver; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\EnumLookup; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Newsletter\Model\SubscriptionManagerInterface; +use Magento\NewsletterGraphQl\Model\SubscribeEmailToNewsletter\Validation; +use Psr\Log\LoggerInterface; + +/** + * Resolver class for the `subscribeEmailToNewsletter` mutation. Adds an email into a newsletter subscription. + */ +class SubscribeEmailToNewsletter implements ResolverInterface +{ + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var EnumLookup + */ + private $enumLookup; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var SubscriptionManagerInterface + */ + private $subscriptionManager; + + /** + * @var Validation + */ + private $validator; + + /** + * SubscribeEmailToNewsletter constructor. + * + * @param CustomerRepositoryInterface $customerRepository + * @param EnumLookup $enumLookup + * @param LoggerInterface $logger + * @param SubscriptionManagerInterface $subscriptionManager + * @param Validation $validator + */ + public function __construct( + CustomerRepositoryInterface $customerRepository, + EnumLookup $enumLookup, + LoggerInterface $logger, + SubscriptionManagerInterface $subscriptionManager, + Validation $validator + ) { + $this->customerRepository = $customerRepository; + $this->enumLookup = $enumLookup; + $this->logger = $logger; + $this->subscriptionManager = $subscriptionManager; + $this->validator = $validator; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $email = trim($args['email']); + + if (empty($email)) { + throw new GraphQlInputException( + __('You must specify an email address to subscribe to a newsletter.') + ); + } + + $currentUserId = (int)$context->getUserId(); + $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); + $websiteId = (int)$context->getExtensionAttributes()->getStore()->getWebsiteId(); + + $this->validator->execute($email, $currentUserId, $websiteId); + + try { + $subscriber = $this->isCustomerSubscription($email, $currentUserId) + ? $this->subscriptionManager->subscribeCustomer($currentUserId, $storeId) + : $this->subscriptionManager->subscribe($email, $storeId); + + $status = $this->enumLookup->getEnumValueFromField( + 'SubscriptionStatusesEnum', + (string)$subscriber->getSubscriberStatus() + ); + } catch (LocalizedException $e) { + $this->logger->error($e->getMessage()); + + throw new GraphQlInputException( + __('Cannot create a newsletter subscription.') + ); + } + + return [ + 'status' => $status + ]; + } + + /** + * Returns true if a provided email equals to a current customer one + * + * @param string $email + * @param int $currentUserId + * @return bool + * @throws LocalizedException + * @throws NoSuchEntityException + */ + private function isCustomerSubscription(string $email, int $currentUserId): bool + { + if ($currentUserId > 0) { + $customer = $this->customerRepository->getById($currentUserId); + + if ($customer->getEmail() == $email) { + return true; + } + } + + return false; + } +} diff --git a/app/code/Magento/NewsletterGraphQl/Model/SubscribeEmailToNewsletter/Validation.php b/app/code/Magento/NewsletterGraphQl/Model/SubscribeEmailToNewsletter/Validation.php new file mode 100644 index 0000000000000..8b8cac0c58cf2 --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/Model/SubscribeEmailToNewsletter/Validation.php @@ -0,0 +1,195 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\NewsletterGraphQl\Model\SubscribeEmailToNewsletter; + +use Magento\Customer\Api\AccountManagementInterface as CustomerAccountManagement; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Validator\EmailAddress as EmailValidator; +use Magento\Newsletter\Model\ResourceModel\Subscriber as SubscriberResourceModel; +use Magento\Newsletter\Model\Subscriber; +use Magento\Store\Model\ScopeInterface; +use Psr\Log\LoggerInterface; + +/** + * Validation class for the "subscribeEmailToNewsletter" mutation + */ +class Validation +{ + /** + * @var CustomerAccountManagement + */ + private $customerAccountManagement; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var EmailValidator + */ + private $emailValidator; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var SubscriberResourceModel + */ + private $subscriberResource; + + /** + * Validation constructor. + * + * @param CustomerAccountManagement $customerAccountManagement + * @param CustomerRepositoryInterface $customerRepository + * @param EmailValidator $emailValidator + * @param LoggerInterface $logger + * @param ScopeConfigInterface $scopeConfig + * @param SubscriberResourceModel $subscriberResource + */ + public function __construct( + CustomerAccountManagement $customerAccountManagement, + CustomerRepositoryInterface $customerRepository, + EmailValidator $emailValidator, + LoggerInterface $logger, + ScopeConfigInterface $scopeConfig, + SubscriberResourceModel $subscriberResource + ) { + $this->customerAccountManagement = $customerAccountManagement; + $this->customerRepository = $customerRepository; + $this->emailValidator = $emailValidator; + $this->logger = $logger; + $this->scopeConfig = $scopeConfig; + $this->subscriberResource = $subscriberResource; + } + + /** + * Validate the next cases: + * - email format + * - email address isn't being used by a different account + * - if a guest user can be subscribed to a newsletter + * - verify if email is already subscribed + * + * @param string $email + * @param int $currentUserId + * @param int $websiteId + * @throws GraphQlAlreadyExistsException + * @throws GraphQlInputException + */ + public function execute(string $email = '', int $currentUserId = 0, int $websiteId = 1): void + { + $this->validateEmailFormat($email); + + if ($currentUserId > 0) { + $this->validateEmailAvailable($email, $currentUserId, $websiteId); + } else { + $this->validateGuestSubscription(); + } + + $this->validateAlreadySubscribed($email, $websiteId); + } + + /** + * Validate the format of the email address + * + * @param string $email + * @throws GraphQlInputException + */ + private function validateEmailFormat(string $email): void + { + if (!$this->emailValidator->isValid($email)) { + throw new GraphQlInputException(__('Enter a valid email address.')); + } + } + + /** + * Validate that the email address isn't being used by a different account. + * + * @param string $email + * @param int $currentUserId + * @param int $websiteId + * @throws GraphQlInputException + */ + private function validateEmailAvailable(string $email, int $currentUserId, int $websiteId): void + { + try { + $customer = $this->customerRepository->getById($currentUserId); + $customerEmail = $customer->getEmail(); + } catch (LocalizedException $e) { + $customerEmail = ''; + } + + try { + $emailAvailable = $this->customerAccountManagement->isEmailAvailable($email, $websiteId); + } catch (LocalizedException $e) { + $emailAvailable = false; + } + + if (!$emailAvailable && $customerEmail != $email) { + $this->logger->error( + __('This email address is already assigned to another user.') + ); + + throw new GraphQlInputException( + __('Cannot create a newsletter subscription.') + ); + } + } + + /** + * Validate if a guest user can be subscribed to a newsletter. + * + * @throws GraphQlInputException + */ + private function validateGuestSubscription(): void + { + if (!$this->scopeConfig->getValue( + Subscriber::XML_PATH_ALLOW_GUEST_SUBSCRIBE_FLAG, + ScopeInterface::SCOPE_STORE + )) { + throw new GraphQlInputException( + __('Guests can not subscribe to the newsletter. You must create an account to subscribe.') + ); + } + } + + /** + * Verify if email is already subscribed + * + * @param string $email + * @param int $websiteId + * @throws GraphQlAlreadyExistsException + */ + private function validateAlreadySubscribed(string $email, int $websiteId): void + { + try { + $subscriberData = $this->subscriberResource->loadBySubscriberEmail($email, $websiteId); + } catch (LocalizedException $e) { + $subscriberData = []; + } + + if (isset($subscriberData['subscriber_status']) + && (int)$subscriberData['subscriber_status'] === Subscriber::STATUS_SUBSCRIBED) { + throw new GraphQlAlreadyExistsException( + __('This email address is already subscribed.') + ); + } + } +} diff --git a/app/code/Magento/NewsletterGraphQl/README.md b/app/code/Magento/NewsletterGraphQl/README.md new file mode 100644 index 0000000000000..c65d44fbcfeba --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/README.md @@ -0,0 +1 @@ +The Magento_NewsletterGraphQl module allows a shopper to subscribe to a newsletter using GraphQL. diff --git a/app/code/Magento/NewsletterGraphQl/composer.json b/app/code/Magento/NewsletterGraphQl/composer.json new file mode 100644 index 0000000000000..92352a8a9adfe --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-newsletter-graph-ql", + "description": "Provides GraphQl functionality for the newsletter subscriptions.", + "config": { + "sort-packages": true + }, + "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-customer": "*", + "magento/module-newsletter": "*", + "magento/module-store": "*" + }, + "suggest": { + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\NewsletterGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml b/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..302a562ec4700 --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Framework\GraphQl\Schema\Type\Enum\DefaultDataMapper"> + <arguments> + <argument name="map" xsi:type="array"> + <item name="SubscriptionStatusesEnum" xsi:type="array"> + <item name="subscribed" xsi:type="string">1</item> + <item name="not_active" xsi:type="string">2</item> + <item name="unsubscribed" xsi:type="string">3</item> + <item name="unconfirmed" xsi:type="string">4</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/NewsletterGraphQl/etc/module.xml b/app/code/Magento/NewsletterGraphQl/etc/module.xml new file mode 100644 index 0000000000000..8bda85d80c830 --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_NewsletterGraphQl"> + <sequence> + <module name="Magento_Newsletter"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls b/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..d96756e12caea --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls @@ -0,0 +1,17 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Mutation { + subscribeEmailToNewsletter(email: String!): SubscribeEmailToNewsletterOutput @doc(description:"Subscribes the specified email to a newsletter") @resolver(class: "Magento\\NewsletterGraphQl\\Model\\Resolver\\SubscribeEmailToNewsletter") +} + +type SubscribeEmailToNewsletterOutput { + status: SubscriptionStatusesEnum @doc(description: "Returns the status of the subscription request") +} + +enum SubscriptionStatusesEnum { + NOT_ACTIVE + SUBSCRIBED + UNSUBSCRIBED + UNCONFIRMED +} diff --git a/app/code/Magento/NewsletterGraphQl/registration.php b/app/code/Magento/NewsletterGraphQl/registration.php new file mode 100644 index 0000000000000..82d5512f4afb5 --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_NewsletterGraphQl', __DIR__); diff --git a/app/code/Magento/OfflinePayments/Model/Purchaseorder.php b/app/code/Magento/OfflinePayments/Model/Purchaseorder.php index 464142df5b996..fe30570aba50d 100644 --- a/app/code/Magento/OfflinePayments/Model/Purchaseorder.php +++ b/app/code/Magento/OfflinePayments/Model/Purchaseorder.php @@ -62,6 +62,7 @@ public function assignData(\Magento\Framework\DataObject $data) * @return $this * @throws LocalizedException * @api + * @since 100.2.3 */ public function validate() { diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml index a251c609ea324..01ed26d5e57a6 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml @@ -6,16 +6,21 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Banktransfer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions) : ?> +<?php if ($instructions): ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> - <ul class="form-list checkout-agreements" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display:none;"> + <ul class="form-list checkout-agreements" id="payment_form_<?= /* @noEscape */ $methodCode ?>"> <li> <div class="<?= /* @noEscape */ $methodCode ?>-instructions-content checkout-agreement-item-content"> <?= /* @noEscape */ nl2br($block->escapeHtml($instructions)) ?> </div> </li> </ul> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'ul#payment_form_' . /* @noEscape */ $methodCode + ) ?> <?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml index 8e8730640a8a7..c1b07f08d4ce3 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml @@ -6,16 +6,21 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Cashondelivery + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions) : ?> +<?php if ($instructions): ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> - <ul class="form-list checkout-agreements" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display:none;"> + <ul class="form-list checkout-agreements" id="payment_form_<?= /* @noEscape */ $methodCode ?>"> <li> <div class="<?= /* @noEscape */ $methodCode ?>-instructions-content checkout-agreement-item-content"> <?= /* @noEscape */ nl2br($block->escapeHtml($instructions)) ?> </div> </li> </ul> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'ul#payment_form_' . /* @noEscape */ $methodCode + ) ?> <?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml index db1d7c87ada0e..789a3921b2c21 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml @@ -6,13 +6,15 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Checkmo + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<fieldset class="admin__fieldset payment-method" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none"> - <?php if ($block->getMethod()->getPayableTo()) : ?> - <label class="label"><span><?= $block->escapeHtml(__('Make Check payable to:')) ?></span></label> <?= $block->escapeHtml($block->getMethod()->getPayableTo()) ?> +<fieldset class="admin__fieldset payment-method" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" > + <?php if ($block->getMethod()->getPayableTo()): ?> + <label class="label"><span><?= $block->escapeHtml(__('Make Check payable to:')) ?></span></label> + <?= $block->escapeHtml($block->getMethod()->getPayableTo()) ?> <?php endif; ?> - <?php if ($block->getMethod()->getMailingAddress()) : ?> + <?php if ($block->getMethod()->getMailingAddress()): ?> <div class="admin__field"> <label class="admin__field-label"><span><?= $block->escapeHtml(__('Send Check to:')) ?></span></label> <div class="admin__field-control checkmo-mailing-address"> @@ -21,3 +23,7 @@ </div> <?php endif; ?> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . $block->escapeJs($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml index c115765697fc5..a1e3da2713811 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml @@ -6,15 +6,23 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Purchaseorder + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<fieldset class="admin__fieldset payment-method" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display: none"> +<fieldset class="admin__fieldset payment-method" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>"> <div class="admin__field _required"> - <label for="po_number" class="admin__field-label"><span><?= $block->escapeHtml(__('Purchase Order Number')) ?></span></label> + <label for="po_number" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Purchase Order Number')) ?></span> + </label> <div class="admin__field-control"> <input type="text" id="po_number" name="payment[po_number]" - title="<?= $block->escapeHtml(__("Purchase Order Number")) ?>" class="required-entry admin__control-text" + title="<?= $block->escapeHtml(__("Purchase Order Number")) ?>" + class="required-entry admin__control-text" value="<?= /* @noEscape */ $block->getInfoData('po_number') ?>"/> </div> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . $block->escapeJs($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml index 568ef7c3f69f2..97288194342ba 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml @@ -6,12 +6,18 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Banktransfer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions) : ?> +<?php if ($instructions): ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> - <div class="items <?= /* @noEscape */ $methodCode ?> instructions agreement checkout-agreement-item-content" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display: none;"> + <div class="items <?= /* @noEscape */ $methodCode ?> instructions agreement checkout-agreement-item-content" + id="payment_form_<?= /* @noEscape */ $methodCode ?>"> <?= /* @noEscape */ nl2br($block->escapeHtml($instructions)) ?> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'div#payment_form_' . /* @noEscape */ $methodCode + ) ?> <?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml index 2943f59be4ab3..160c1d27052f0 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml @@ -6,12 +6,18 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Cashondelivery + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions) : ?> +<?php if ($instructions): ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> - <div class="items <?= /* @noEscape */ $methodCode ?> instructions agreement" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display: none;"> + <div class="items <?= /* @noEscape */ $methodCode ?> instructions agreement" + id="payment_form_<?= /* @noEscape */ $methodCode ?>"> <?= /* @noEscape */ nl2br($block->escapeHtml($instructions)) ?> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'div#payment_form_' . /* @noEscape */ $methodCode + ) ?> <?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml index 36f58fc155a18..3b381bbf72f4f 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml @@ -6,15 +6,16 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Checkmo + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->getMethod()->getMailingAddress() || $block->getMethod()->getPayableTo()) : ?> - <dl class="items check payable" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none;"> - <?php if ($block->getMethod()->getPayableTo()) : ?> +<?php if ($block->getMethod()->getMailingAddress() || $block->getMethod()->getPayableTo()): ?> + <dl class="items check payable" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>"> + <?php if ($block->getMethod()->getPayableTo()): ?> <dt class="title"><?= $block->escapeHtml(__('Make Check payable to:')) ?></dt> <dd class="content"><?= $block->escapeHtml($block->getMethod()->getPayableTo()) ?></dd> <?php endif; ?> - <?php if ($block->getMethod()->getMailingAddress()) : ?> + <?php if ($block->getMethod()->getMailingAddress()): ?> <dt class="title"><?= $block->escapeHtml(__('Send Check to:')) ?></dt> <dd class="content"> <address class="checkmo mailing address"> @@ -23,4 +24,8 @@ </dd> <?php endif; ?> </dl> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'dl#payment_form_' . $block->escapeJs($block->getMethodCode()) + ) ?> <?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml index 52b7df9fb9187..35ef5d9db8616 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml @@ -6,16 +6,23 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Purchaseorder + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $methodCode = $block->escapeHtml($block->getMethodCode()); ?> -<fieldset class="fieldset items <?= /* @noEscape */ $methodCode ?>" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display: none"> +<fieldset class="fieldset items <?= /* @noEscape */ $methodCode ?>" + id="payment_form_<?= /* @noEscape */ $methodCode ?>"> <div class="field number required"> <label for="po_number" class="label"><span><?= $block->escapeHtml(__('Purchase Order Number')) ?></span></label> <div class="control"> - <input type="text" id="po_number" name="payment[po_number]" title="<?= $block->escapeHtml(__('Purchase Order Number')) ?>" + <input type="text" id="po_number" name="payment[po_number]" + title="<?= $block->escapeHtml(__('Purchase Order Number')) ?>" class="input-text required-entry" value="<?= $block->escapeHtml($block->getInfoData('po_number')) ?>" /> </div> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . /* @noEscape */ $methodCode +) ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml index b96918243a7a7..730976a15be5d 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require([ 'uiLayout', 'jquery' @@ -25,4 +28,6 @@ $('body').trigger('contentUpdated'); }) }) -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php index e015f7b54637d..bd75a1ffe698c 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php @@ -155,7 +155,7 @@ protected function loadRegions() * @param int $countryId * @param string $regionCode * @return string - * @deprecated + * @deprecated 100.3.1 */ public function getRegionId($countryId, $regionCode) { diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php index bd36b899ce89b..062e428ef68b1 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php @@ -18,6 +18,7 @@ use Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; class ImportTest extends TestCase { @@ -38,13 +39,16 @@ protected function setUp(): void ->onlyMethods(['addSuffixToName']) ->disableOriginalConstructor() ->getMock(); + $randomMock = $this->getMockBuilder(Random::class)->disableOriginalConstructor()->getMock(); + $randomMock->method('getRandomString')->willReturn('123456abcdefg'); $testData = ['name' => 'test_name', 'html_id' => 'test_html_id']; $testHelper = new ObjectManager($this); $this->_object = $testHelper->getObject( Import::class, [ 'data' => $testData, - '_escaper' => $testHelper->getObject(Escaper::class) + '_escaper' => $testHelper->getObject(Escaper::class), + 'random' => $randomMock ] ); $this->_object->setForm($this->_formMock); @@ -87,9 +91,9 @@ public function testGetElementHtml() '<input id="time_condition" type="hidden" name="test_name" value="', $testString ); - $this->assertStringEndsWith( + $this->assertStringContainsString( '<input id="test_name_prefixtest_html_idtest_name_suffix" ' . - 'name="test_name" data-ui-id="form-element-test_name" value="" type="file"/>', + 'name="test_name" data-ui-id="form-element-test_name" value="" type="file"', $testString ); } diff --git a/app/code/Magento/PageCache/Model/Config.php b/app/code/Magento/PageCache/Model/Config.php index 10ae41be21d4d..bf144cc46637e 100644 --- a/app/code/Magento/PageCache/Model/Config.php +++ b/app/code/Magento/PageCache/Model/Config.php @@ -121,7 +121,7 @@ public function __construct( */ public function getType() { - return $this->_scopeConfig->getValue(self::XML_PAGECACHE_TYPE); + return (int)$this->_scopeConfig->getValue(self::XML_PAGECACHE_TYPE); } /** diff --git a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php index 762f393f2a1b9..1b64f3b635c03 100644 --- a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php +++ b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php @@ -8,10 +8,12 @@ namespace Magento\PageCache\Model\Layout; use Magento\Framework\App\MaintenanceMode; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResponseInterface; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\View\Layout; use Magento\PageCache\Model\Config; +use Magento\PageCache\Model\Spi\PageCacheTagsPreprocessorInterface; /** * Append cacheable pages response headers. @@ -28,6 +30,11 @@ class LayoutPlugin */ private $response; + /** + * @var PageCacheTagsPreprocessorInterface + */ + private $pageCacheTagsPreprocessor; + /** * @var MaintenanceMode */ @@ -37,15 +44,19 @@ class LayoutPlugin * @param ResponseInterface $response * @param Config $config * @param MaintenanceMode $maintenanceMode + * @param PageCacheTagsPreprocessorInterface|null $pageCacheTagsPreprocessor */ public function __construct( ResponseInterface $response, Config $config, - MaintenanceMode $maintenanceMode + MaintenanceMode $maintenanceMode, + ?PageCacheTagsPreprocessorInterface $pageCacheTagsPreprocessor = null ) { $this->response = $response; $this->config = $config; $this->maintenanceMode = $maintenanceMode; + $this->pageCacheTagsPreprocessor = $pageCacheTagsPreprocessor + ?? ObjectManager::getInstance()->get(PageCacheTagsPreprocessorInterface::class); } /** @@ -74,10 +85,11 @@ public function afterGetOutput(Layout $subject, $result) { if ($subject->isCacheable() && $this->config->isEnabled()) { $tags = [[]]; + $isVarnish = $this->config->getType() === Config::VARNISH; + foreach ($subject->getAllBlocks() as $block) { if ($block instanceof IdentityInterface) { $isEsiBlock = $block->getTtl() > 0; - $isVarnish = $this->config->getType() == Config::VARNISH; if ($isVarnish && $isEsiBlock) { continue; } @@ -85,6 +97,7 @@ public function afterGetOutput(Layout $subject, $result) } } $tags = array_unique(array_merge(...$tags)); + $tags = $this->pageCacheTagsPreprocessor->process($tags); $this->response->setHeader('X-Magento-Tags', implode(',', $tags)); } diff --git a/app/code/Magento/PageCache/Model/PageCacheTagsPreprocessorComposite.php b/app/code/Magento/PageCache/Model/PageCacheTagsPreprocessorComposite.php new file mode 100644 index 0000000000000..caaf3b378571c --- /dev/null +++ b/app/code/Magento/PageCache/Model/PageCacheTagsPreprocessorComposite.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PageCache\Model; + +use InvalidArgumentException; +use Magento\Framework\App\RequestInterface; +use Magento\PageCache\Model\Spi\PageCacheTagsPreprocessorInterface; + +/** + * Composite page cache preprocessors + */ +class PageCacheTagsPreprocessorComposite implements PageCacheTagsPreprocessorInterface +{ + /** + * @var PageCacheTagsPreprocessorInterface[][] + */ + private $preprocessors; + /** + * @var RequestInterface + */ + private $request; + + /** + * @param RequestInterface $request + * @param PageCacheTagsPreprocessorInterface[][] $preprocessors + */ + public function __construct( + RequestInterface $request, + array $preprocessors = [] + ) { + foreach ($preprocessors as $group) { + foreach ($group as $preprocessor) { + if (!$preprocessor instanceof PageCacheTagsPreprocessorInterface) { + throw new InvalidArgumentException( + sprintf( + 'Instance of %s is expected, got %s instead.', + PageCacheTagsPreprocessorInterface::class, + get_class($preprocessor) + ) + ); + } + } + } + $this->preprocessors = $preprocessors; + $this->request = $request; + } + + /** + * @inheritDoc + */ + public function process(array $tags): array + { + $forwardInfo = $this->request->getBeforeForwardInfo(); + $actionName = $forwardInfo + ? implode('_', [$forwardInfo['route_name'], $forwardInfo['controller_name'], $forwardInfo['action_name']]) + : $this->request->getFullActionName(); + if (isset($this->preprocessors[$actionName])) { + foreach ($this->preprocessors[$actionName] as $preprocessor) { + $tags = $preprocessor->process($tags); + } + } + return $tags; + } +} diff --git a/app/code/Magento/PageCache/Model/Spi/PageCacheTagsPreprocessorInterface.php b/app/code/Magento/PageCache/Model/Spi/PageCacheTagsPreprocessorInterface.php new file mode 100644 index 0000000000000..19f9eedf7546d --- /dev/null +++ b/app/code/Magento/PageCache/Model/Spi/PageCacheTagsPreprocessorInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PageCache\Model\Spi; + +/** + * Interface for page tags preprocessors + */ +interface PageCacheTagsPreprocessorInterface +{ + /** + * Change page tags and returned the modified tags + * + * @param array $tags + * @return array + */ + public function process(array $tags): array; +} diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php index da6a71a0c2655..14b72e75d9473 100644 --- a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php +++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php @@ -17,7 +17,7 @@ * * Page Cache State Observer * - * @deprecated Originally used by now removed observer SwitchPageCacheOnMaintenance + * @deprecated 100.4.0 Originally used by now removed observer SwitchPageCacheOnMaintenance */ class PageCacheState { 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/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml index d2c738398aae1..1c280acd63a7b 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -39,7 +39,9 @@ <requiredEntity createDataKey="createCategoryA"/> </createData> - <magentoCLI command="cache:clean" arguments="full_page" stepKey="clearCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="full_page"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> <resetCookie userInput="PHPSESSID" stepKey="resetSessionCookie"/> @@ -61,8 +63,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> <!-- 2. Navigate Go to "Catalog"->"Products" --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="onCatalogProductPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="onCatalogProductPage"/> <!-- 3. Open separate tab with Storefront --> <openNewTab stepKey="openNewTab"/> diff --git a/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php index a7f4a1e844264..2cb52dee43e40 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php @@ -15,6 +15,7 @@ use Magento\Framework\View\Layout; use Magento\PageCache\Model\Config; use Magento\PageCache\Model\Layout\LayoutPlugin; +use Magento\PageCache\Model\Spi\PageCacheTagsPreprocessorInterface; use Magento\PageCache\Test\Unit\Block\Controller\StubBlock; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -58,6 +59,8 @@ protected function setUp(): void $this->responseMock = $this->createMock(Http::class); $this->configMock = $this->createMock(Config::class); $this->maintenanceModeMock = $this->createMock(MaintenanceMode::class); + $preprocessor = $this->createMock(PageCacheTagsPreprocessorInterface::class); + $preprocessor->method('process')->willReturnArgument(0); $this->model = (new ObjectManagerHelper($this))->getObject( LayoutPlugin::class, @@ -65,6 +68,7 @@ protected function setUp(): void 'response' => $this->responseMock, 'config' => $this->configMock, 'maintenanceMode' => $this->maintenanceModeMock, + 'pageCacheTagsPreprocessor' => $preprocessor ] ); } diff --git a/app/code/Magento/PageCache/etc/di.xml b/app/code/Magento/PageCache/etc/di.xml index 9bc86b6f1e3f9..f70a561342763 100644 --- a/app/code/Magento/PageCache/etc/di.xml +++ b/app/code/Magento/PageCache/etc/di.xml @@ -37,9 +37,6 @@ <argument name="layoutCacheKey" xsi:type="object">Magento\Framework\View\Layout\LayoutCacheKeyInterface</argument> </arguments> </type> - <type name="Magento\Framework\App\FrontControllerInterface"> - <plugin name="page_cache_from_key_from_cookie" type="Magento\PageCache\Plugin\RegisterFormKeyFromCookie" /> - </type> <type name="Magento\Framework\App\Cache\RuntimeStaleCacheStateModifier"> <arguments> <argument name="cacheTypes" xsi:type="array"> @@ -49,4 +46,5 @@ </type> <preference for="Magento\PageCache\Model\VclGeneratorInterface" type="Magento\PageCache\Model\Varnish\VclGenerator"/> <preference for="Magento\PageCache\Model\VclTemplateLocatorInterface" type="Magento\PageCache\Model\Varnish\VclTemplateLocator"/> + <preference for="Magento\PageCache\Model\Spi\PageCacheTagsPreprocessorInterface" type="Magento\PageCache\Model\PageCacheTagsPreprocessorComposite"/> </config> diff --git a/app/code/Magento/PageCache/etc/frontend/di.xml b/app/code/Magento/PageCache/etc/frontend/di.xml index a396a46ae7346..1aaa331da7025 100644 --- a/app/code/Magento/PageCache/etc/frontend/di.xml +++ b/app/code/Magento/PageCache/etc/frontend/di.xml @@ -9,6 +9,7 @@ <type name="Magento\Framework\App\FrontControllerInterface"> <plugin name="front-controller-builtin-cache" type="Magento\PageCache\Model\App\FrontController\BuiltinPlugin"/> <plugin name="front-controller-varnish-cache" type="Magento\PageCache\Model\App\FrontController\VarnishPlugin"/> + <plugin name="page_cache_form_key_from_cookie" type="Magento\PageCache\Plugin\RegisterFormKeyFromCookie" /> </type> <type name="Magento\Framework\Controller\ResultInterface"> <plugin name="result-builtin-cache" type="Magento\PageCache\Model\Controller\Result\BuiltinPlugin"/> diff --git a/app/code/Magento/PageCache/etc/graphql/di.xml b/app/code/Magento/PageCache/etc/graphql/di.xml new file mode 100644 index 0000000000000..93714465f4d72 --- /dev/null +++ b/app/code/Magento/PageCache/etc/graphql/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="page_cache_form_key_from_cookie" type="Magento\PageCache\Plugin\RegisterFormKeyFromCookie" /> + </type> +</config> diff --git a/app/code/Magento/PageCache/etc/varnish6.vcl b/app/code/Magento/PageCache/etc/varnish6.vcl index eef5e99862538..b23bec4c45fb8 100644 --- a/app/code/Magento/PageCache/etc/varnish6.vcl +++ b/app/code/Magento/PageCache/etc/varnish6.vcl @@ -23,6 +23,10 @@ acl purge { } sub vcl_recv { + if (req.restarts > 0) { + set req.hash_always_miss = true; + } + if (req.method == "PURGE") { if (client.ip !~ purge) { return (synth(405, "Method not allowed")); 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/PageCache/view/adminhtml/templates/page_cache_validation.phtml b/app/code/Magento/PageCache/view/adminhtml/templates/page_cache_validation.phtml index 57bb5be87e138..b83e0a172574b 100644 --- a/app/code/Magento/PageCache/view/adminhtml/templates/page_cache_validation.phtml +++ b/app/code/Magento/PageCache/view/adminhtml/templates/page_cache_validation.phtml @@ -4,9 +4,12 @@ * See COPYING.txt for license details. */ -/** @var \Magento\PageCache\Block\System\Config\Form\Field\Export $block */ +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script> + +<?php $scriptString = <<<script require(['jquery'], function($){ //<![CDATA[ @@ -31,4 +34,6 @@ require(['jquery'], function($){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Payment/Block/Transparent/Iframe.php b/app/code/Magento/Payment/Block/Transparent/Iframe.php index 672db1b065b74..6999a722dbeda 100644 --- a/app/code/Magento/Payment/Block/Transparent/Iframe.php +++ b/app/code/Magento/Payment/Block/Transparent/Iframe.php @@ -5,6 +5,9 @@ */ namespace Magento\Payment\Block\Transparent; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; + /** * Iframe block for register specific params in layout * @@ -28,13 +31,16 @@ class Iframe extends \Magento\Framework\View\Element\Template * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Registry $registry * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->coreRegistry = $registry; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Payment/Block/Transparent/Redirect.php b/app/code/Magento/Payment/Block/Transparent/Redirect.php index 1be6dec4cc1d8..b62e86e0f831c 100644 --- a/app/code/Magento/Payment/Block/Transparent/Redirect.php +++ b/app/code/Magento/Payment/Block/Transparent/Redirect.php @@ -13,6 +13,7 @@ * Redirect block for register specific params in layout * * @api + * @since 100.3.5 */ class Redirect extends Template { @@ -44,6 +45,7 @@ public function __construct( * Returns url for redirect. * * @return string + * @since 100.3.5 */ public function getRedirectUrl(): string { @@ -53,10 +55,22 @@ public function getRedirectUrl(): string /** * Returns params to be redirected. * + * Encodes invalid UTF-8 values to UTF-8 to prevent character escape error. + * Some payment methods like PayPal, send data in merchant defined language encoding + * which can be different from the system character encoding (UTF-8). + * * @return array + * @since 100.3.5 */ public function getPostParams(): array { - return (array)$this->_request->getPostValue(); + $params = []; + foreach ($this->_request->getPostValue() as $name => $value) { + if (!empty($value) && mb_detect_encoding($value, 'UTF-8', true) === false) { + $value = utf8_encode($value); + } + $params[$name] = $value; + } + return $params; } } diff --git a/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php index 2072615a39b92..8a9f08e83005e 100644 --- a/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php +++ b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php @@ -17,6 +17,7 @@ * In that case, this implementation can be extended via di.xml and configured with appropriate mappers. * * @api + * @since 100.2.2 */ class ErrorMessageMapper implements ErrorMessageMapperInterface { @@ -35,6 +36,7 @@ public function __construct(DataInterface $messageMapping) /** * @inheritdoc + * @since 100.2.2 */ public function getMessage(string $code) { diff --git a/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php index f09f49b7f8100..fc8c69902f373 100644 --- a/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php +++ b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php @@ -13,6 +13,7 @@ * Interface to provide customization for payment validation errors. * * @api + * @since 100.2.2 */ interface ErrorMessageMapperInterface { @@ -22,6 +23,7 @@ interface ErrorMessageMapperInterface * * @param string $code * @return Phrase|null + * @since 100.2.2 */ public function getMessage(string $code); } diff --git a/app/code/Magento/Payment/Gateway/Validator/ResultInterface.php b/app/code/Magento/Payment/Gateway/Validator/ResultInterface.php index c1ad947e49c5b..9ed30b1c56cf4 100644 --- a/app/code/Magento/Payment/Gateway/Validator/ResultInterface.php +++ b/app/code/Magento/Payment/Gateway/Validator/ResultInterface.php @@ -33,6 +33,7 @@ public function getFailsDescription(); * Returns list of error codes. * * @return string[] + * @since 100.3.0 */ public function getErrorCodes(); } diff --git a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php index f96c08a9605a8..8c8d13300849e 100644 --- a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php +++ b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php @@ -14,6 +14,7 @@ * Compiles a result using the results of multiple validators * * @api + * @since 100.0.2 */ class ValidatorComposite extends AbstractValidator { diff --git a/app/code/Magento/Payment/Model/Method/Adapter.php b/app/code/Magento/Payment/Model/Method/Adapter.php index dba48efaae837..1a9831c47f5b1 100644 --- a/app/code/Magento/Payment/Model/Method/Adapter.php +++ b/app/code/Magento/Payment/Model/Method/Adapter.php @@ -668,6 +668,7 @@ public function getConfigPaymentAction() /** * @inheritdoc + * @since 100.4.0 */ public function canSale(): bool { @@ -676,6 +677,7 @@ public function canSale(): bool /** * @inheritdoc + * @since 100.4.0 */ public function sale(InfoInterface $payment, float $amount) { diff --git a/app/code/Magento/Payment/Model/Method/ConfigInterface.php b/app/code/Magento/Payment/Model/Method/ConfigInterface.php index 06afde4657f26..7c74736cf2ef1 100644 --- a/app/code/Magento/Payment/Model/Method/ConfigInterface.php +++ b/app/code/Magento/Payment/Model/Method/ConfigInterface.php @@ -8,7 +8,7 @@ /** * Interface for payment methods config * - * @deprecated This interface has no semantic meaning and all it implementations has no joint points. + * @deprecated 100.3.0 This interface has no semantic meaning and all it implementations has no joint points. */ interface ConfigInterface extends \Magento\Payment\Gateway\ConfigInterface { diff --git a/app/code/Magento/Payment/Model/MethodList.php b/app/code/Magento/Payment/Model/MethodList.php index 746306cbd0bbf..0700f25fcbee5 100644 --- a/app/code/Magento/Payment/Model/MethodList.php +++ b/app/code/Magento/Payment/Model/MethodList.php @@ -19,7 +19,7 @@ class MethodList { /** * @var \Magento\Payment\Helper\Data - * @deprecated 100.1.3 Do not use this property in case of inheritance. + * @deprecated 100.1.0 Do not use this property in case of inheritance. */ protected $paymentHelper; diff --git a/app/code/Magento/Payment/Model/SaleOperationInterface.php b/app/code/Magento/Payment/Model/SaleOperationInterface.php index 384913a75ae26..da7dc5dfcb390 100644 --- a/app/code/Magento/Payment/Model/SaleOperationInterface.php +++ b/app/code/Magento/Payment/Model/SaleOperationInterface.php @@ -11,6 +11,7 @@ * Responsible for support of `sale` payment operation via Magento payment provider gateway. * * @api + * @since 100.4.0 */ interface SaleOperationInterface { @@ -18,6 +19,7 @@ interface SaleOperationInterface * Checks `sale` payment operation availability. * * @return bool + * @since 100.4.0 */ public function canSale(): bool; @@ -27,6 +29,7 @@ public function canSale(): bool; * @param InfoInterface $payment * @param float $amount * @return void + * @since 100.4.0 */ public function sale(InfoInterface $payment, float $amount); } diff --git a/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php new file mode 100644 index 0000000000000..1cd1230a14634 --- /dev/null +++ b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Payment\Test\Unit\Block\Transparent; + +use Magento\Payment\Block\Transparent\Redirect; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class RedirectTest extends TestCase +{ + /** + * @var \Magento\Framework\View\Element\Context|MockObject + */ + private $context; + /** + * @var \Magento\Framework\UrlInterface|MockObject + */ + private $url; + /** + * @var Redirect + */ + private $model; + /** + * @var \Magento\Framework\App\RequestInterface|MockObject + */ + private $request; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); + $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->context->method('getRequest') + ->willReturn($this->request); + $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); + $this->model = new Redirect( + $this->context, + $this->url + ); + } + + /** + * @param array $postData + * @param array $expected + * @dataProvider getPostParamsDataProvider + */ + public function testGetPostParams(array $postData, array $expected): void + { + $this->request->method('getPostValue') + ->willReturn($postData); + $this->assertEquals($expected, $this->model->getPostParams()); + } + + /** + * @return array + */ + public function getPostParamsDataProvider(): array + { + return [ + [ + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ], + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ] + ], + [ + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => mb_convert_encoding('Ãtienne', 'ISO-8859-1'), + 'BILLTOFIRSTNAME' => mb_convert_encoding('Ãillin', 'ISO-8859-1'), + ], + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ] + ] + ]; + } +} diff --git a/app/code/Magento/Payment/composer.json b/app/code/Magento/Payment/composer.json index 6ee0baec247f3..72246c5698f80 100644 --- a/app/code/Magento/Payment/composer.json +++ b/app/code/Magento/Payment/composer.json @@ -12,7 +12,8 @@ "magento/module-directory": "*", "magento/module-quote": "*", "magento/module-sales": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml b/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml index 678bde815d370..2ff4df6e4885a 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml @@ -6,14 +6,14 @@ /** * @var \Magento\Payment\Block\Adminhtml\Transparent\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $code = $block->escapeHtml($block->getMethodCode()); $ccType = $block->getInfoData('cc_type'); $ccExpMonth = $block->getInfoData('cc_exp_month'); $ccExpYear = $block->getInfoData('cc_exp_year'); ?> -<fieldset class="admin__fieldset payment-method" id="payment_form_<?= /* @noEscape */ $code ?>" - style="display:none"> +<fieldset class="admin__fieldset payment-method" id="payment_form_<?= /* @noEscape */ $code ?>"> <div class="field-type admin__field _required"> <label class="admin__field-label" for="<?= /* @noEscape */ $code ?>_cc_type"> <span><?= $block->escapeHtml(__('Credit Card Type')) ?></span> @@ -22,8 +22,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <select id="<?= /* @noEscape */ $code ?>_cc_type" name="payment[cc_type]" class="required-entry validate-cc-type-select admin__control-select"> <option value=""></option> - <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName) : ?> - <option value="<?= $block->escapeHtml($typeCode) ?>" <?php if ($typeCode == $ccType) : ?>selected="selected"<?php endif ?>> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> + <option value="<?= $block->escapeHtml($typeCode) ?>" + <?php if ($typeCode == $ccType): ?>selected="selected"<?php endif ?>> <?= $block->escapeHtml($typeName) ?> </option> <?php endforeach ?> @@ -36,7 +37,8 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); </label> <div class="admin__field-control"> <input type="text" id="<?= /* @noEscape */ $code ?>_cc_number" name="payment[cc_number]" - title="<?= $block->escapeHtml(__('Credit Card Number')) ?>" class="admin__control-text validate-cc-number" + title="<?= $block->escapeHtml(__('Credit Card Number')) ?>" + class="admin__control-text validate-cc-number" value="<?= /* @noEscape */ $block->getInfoData('cc_number') ?>"/> </div> </div> @@ -47,18 +49,18 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <div class="admin__field-control"> <select id="<?= /* @noEscape */ $code ?>_expiration" name="payment[cc_exp_month]" class="admin__control-select admin__control-select-month validate-cc-exp required-entry"> - <?php foreach ($block->getCcMonths() as $k => $v) : ?> + <?php foreach ($block->getCcMonths() as $k => $v): ?> <option value="<?= $block->escapeHtml($k) ?>" - <?php if ($k == $ccExpMonth) : ?>selected="selected"<?php endif ?>> + <?php if ($k == $ccExpMonth): ?>selected="selected"<?php endif ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach; ?> </select> <select id="<?= /* @noEscape */ $code ?>_expiration_yr" name="payment[cc_exp_year]" class="admin__control-select admin__control-select-year required-entry"> - <?php foreach ($block->getCcYears() as $k => $v) : ?> + <?php foreach ($block->getCcYears() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpYear) : ?>selected="selected"<?php endif ?>> + <?php if ($k == $ccExpYear): ?>selected="selected"<?php endif ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> @@ -66,7 +68,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); </div> </div> - <?php if ($block->hasVerification()) : ?> + <?php if ($block->hasVerification()): ?> <div class="field-number required admin__field _required"> <label class="admin__field-label" for="<?= /* @noEscape */ $code ?>_cc_cid"> <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> @@ -80,3 +82,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); </div> <?php endif; ?> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml b/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml index 36b8c978c339f..60fbeed2c542f 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml @@ -5,6 +5,8 @@ */ /** @var \Magento\Payment\Block\Transparent\Form $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $code = $block->escapeHtml($block->getMethodCode()); $ccType = $block->getInfoData('cc_type'); $ccExpYear = $block->getInfoData('cc_exp_year'); @@ -17,8 +19,11 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); allowtransparency="true" frameborder="0" name="iframeTransparent" - style="display: none; width: 100%; background-color: transparent;" src="<?= $block->escapeUrl($block->getViewFileUrl('blank.html')) ?>"></iframe> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display: none; width: 100%; background-color: transparent;", + 'iframe#' . /* @noEscape */ $code . '-transparent-iframe' +) ?> <fieldset id="payment_form_<?= /* @noEscape */ $code ?>" class="admin__fieldset" @@ -31,9 +36,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); "orderSaveUrl":"<?= $block->escapeUrl($block->getOrderUrl()) ?>", "cgiUrl":"<?= $block->escapeUrl($block->getCgiUrl()) ?>", "expireYearLength":"<?= $block->escapeHtml($block->getMethodConfigData('cc_year_length')) ?>", - "nativeAction":"<?= $block->escapeUrl($block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()])) ?>" - }, "validation":[]}' - style="display: none;"> + "nativeAction":"<?= $block->escapeUrl( + $block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()]) + ) ?>" + }, "validation":[]}'> <div class="admin__field _required"> <label for="<?= /* @noEscape */ $code ?>_cc_type" class="admin__field-label"> <span><?= $block->escapeHtml(__('Credit Card Type')) ?></span> @@ -46,9 +52,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-validate='{required:true, "validate-cc-type-select":"#<?= /* @noEscape */ $code ?>_cc_number"}' class="admin__control-select"> <option value=""><?= $block->escapeHtml(__('Please Select')) ?></option> - <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName) : ?> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> <option - value="<?= $block->escapeHtml($typeCode) ?>"<?php if ($typeCode == $ccType) : ?> selected="selected"<?php endif ?>> + value="<?= $block->escapeHtml($typeCode) ?>" + <?php if ($typeCode == $ccType): ?> selected="selected"<?php endif ?>> <?= $block->escapeHtml($typeName) ?> </option> <?php endforeach ?> @@ -86,10 +93,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-container="<?= /* @noEscape */ $code ?>-cc-month" class="admin__control-select admin__control-select-month" data-validate='{required:true, "validate-cc-exp":"#<?= /* @noEscape */ $code ?>_expiration_yr"}'> - <?php foreach ($block->getCcMonths() as $k => $v) : ?> + <?php foreach ($block->getCcMonths() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpMonth) : ?> selected="selected"<?php endif; ?>> + <?php if ($k == $ccExpMonth): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> @@ -98,17 +105,17 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); <select id="<?= /* @noEscape */ $code ?>_expiration_yr" name="payment[cc_exp_year]" class="admin__control-select admin__control-select-year" data-container="<?= /* @noEscape */ $code ?>-cc-year" data-validate='{required:true}'> - <?php foreach ($block->getCcYears() as $k => $v) : ?> + <?php foreach ($block->getCcYears() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpYear) : ?> selected="selected"<?php endif ?>> + <?php if ($k == $ccExpYear): ?> selected="selected"<?php endif ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> </select> </div> </div> - <?php if ($block->hasVerification()) : ?> + <?php if ($block->hasVerification()): ?> <div class="admin__field _required field-cvv" id="<?= /* @noEscape */ $code ?>_cc_type_cvv_div"> <label for="<?= /* @noEscape */ $code ?>_cc_cid" class="admin__field-label"> <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> @@ -120,19 +127,24 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); class="admin__control-text cvv" id="<?= /* @noEscape */ $code ?>_cc_cid" name="payment[cc_cid]" value="" - data-validate='{"required-number":true, "validate-cc-cvn":"#<?= /* @noEscape */ $code ?>_cc_type"}' + data-validate='{"required-number":true, "validate-cc-cvn":"#<?=/* @noEscape */ $code?>_cc_type"}' autocomplete="off"/> </div> </div> <?php endif; ?> <?= $block->getChildHtml() ?> </fieldset> - -<script> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> +<?php $scriptString = <<<script /** * Disable card server validation in admin */ require(["Magento_Sales/order/create/form"], function () { - order.addExcludedPaymentMethod('<?= /* @noEscape */ $code ?>'); + order.addExcludedPaymentMethod('{$code}'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/transparent/iframe.phtml b/app/code/Magento/Payment/view/adminhtml/templates/transparent/iframe.phtml index ece7106e91236..77d881257f10a 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/transparent/iframe.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/transparent/iframe.phtml @@ -6,22 +6,39 @@ /** * @var \Magento\Payment\Block\Transparent\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $params = $block->getParams(); +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <html> <head> -<script> -<?php if (isset($params['redirect'])) : ?> - window.location="<?= $block->escapeUrl($params['redirect']) ?>"; -<?php elseif (isset($params['redirect_parent'])) : ?> - window.top.location="<?= $block->escapeUrl($params['redirect_parent']) ?>"; -<?php elseif (isset($params['error_msg'])) : ?> - window.top.alert(<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($params['error_msg']) ?>); -<?php elseif (isset($params['order_success'])) : ?> - window.top.location = "<?= $block->escapeUrl($params['order_success']) ?>"; -<?php else : ?> + <?php $scriptString = '' ?> +<?php if (isset($params['redirect'])): ?> + <?php $scriptString .= <<<script + window.location="{$block->escapeJs($params['redirect'])}"; +script; + ?> +<?php elseif (isset($params['redirect_parent'])): ?> + <?php $scriptString .= <<<script + window.top.location="{$block->escapeJs($params['redirect_parent'])}"; +script; + ?> +<?php elseif (isset($params['error_msg'])): ?> + <?php $encodedErrorMsg = /* @noEscape */ $jsonHelper->jsonEncode($params['error_msg']); + $scriptString .= <<<script + window.top.alert({$encodedErrorMsg}); +script; + ?> +<?php elseif (isset($params['order_success'])): ?> + <?php $scriptString .= <<<script + window.top.location = "{$block->escapeJs($params['order_success'])}"; +script; + ?> +<?php else: ?> + <?php $scriptString .= <<<script var require = window.top.require; require(['jquery'], function($) { $('#edit_form').trigger('processStop'); @@ -34,8 +51,10 @@ $params = $block->getParams(); $('#edit_form').trigger('realOrder'); }); +script; + ?> <?php endif; ?> -</script> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </head> <body> </body> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/transparent/info.phtml b/app/code/Magento/Payment/view/adminhtml/templates/transparent/info.phtml index fb06f1a4dbf33..5997648ed5582 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/transparent/info.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/transparent/info.phtml @@ -6,9 +6,14 @@ /** * @var \Magento\Payment\Block\Transparent\Info $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Payment\Block\Transparent\Info */ ?> -<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none" class="fieldset items redirect"> +<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" class="fieldset items redirect"> <div><?= $block->escapeHtml(__('We\'ll ask for your payment details before you place an order.')) ?></div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/Payment/view/adminhtml/web/js/transparent.js b/app/code/Magento/Payment/view/adminhtml/web/js/transparent.js index 0ec866ee6a831..296d542f3adf6 100644 --- a/app/code/Magento/Payment/view/adminhtml/web/js/transparent.js +++ b/app/code/Magento/Payment/view/adminhtml/web/js/transparent.js @@ -52,28 +52,29 @@ define([ * @param {String} method */ _setPlaceOrderHandler: function (event, method) { + var $editForm = $(this.options.editFormSelector); + + $editForm.off('beforeSubmitOrder.' + this.options.gateway); + if (method === this.options.gateway) { - $(this.options.editFormSelector) - .off('submitOrder') - .on('submitOrder.' + this.options.gateway, this._placeOrderHandler.bind(this)); - } else { - $(this.options.editFormSelector) - .off('submitOrder.' + this.options.gateway); + $editForm.on('beforeSubmitOrder.' + this.options.gateway, this._placeOrderHandler.bind(this)); } }, /** * Handler for form submit to call gateway for credit card validation. * + * @param {Event} event * @return {Boolean} * @private */ - _placeOrderHandler: function () { + _placeOrderHandler: function (event) { if ($(this.options.editFormSelector).valid()) { this._orderSave(); } else { $('body').trigger('processStop'); } + event.stopImmediatePropagation(); return false; }, diff --git a/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml b/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml index 5f61a3ee1d400..7ddc89aac4f6c 100644 --- a/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml @@ -6,6 +6,7 @@ /** * @var \Magento\Payment\Block\Transparent\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $code = $block->escapeHtml($block->getMethodCode()); $ccType = $block->getInfoData('cc_type'); @@ -13,7 +14,7 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); $ccExpYear = $block->getInfoData('cc_exp_year'); ?> <fieldset class="fieldset payment items ccard <?= /* @noEscape */ $code ?>" - id="payment_form_<?= /* @noEscape */ $code ?>" style="display: none;"> + id="payment_form_<?= /* @noEscape */ $code ?>"> <div class="field type required"> <label for="<?= /* @noEscape */ $code ?>_cc_type" class="label"> <span><?= $block->escapeHtml(__('Credit Card Type')) ?></span> @@ -29,9 +30,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); }' class="select"> <option value=""><?= $block->escapeHtml(__('--Please Select--')) ?></option> - <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName) : ?> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> <option value="<?= $block->escapeHtml($typeCode) ?>" - <?php if ($typeCode == $ccType) : ?> selected="selected"<?php endif; ?>> + <?php if ($typeCode == $ccType): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($typeName) ?> </option> <?php endforeach; ?> @@ -60,11 +61,14 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <div class="fields group group-2"> <div class="field no-label month"> <div class="control"> - <select id="<?= /* @noEscape */ $code ?>_expiration" name="payment[cc_exp_month]" class="select month" - data-validate='{required:true, "validate-cc-exp":"#<?= /* @noEscape */ $code ?>_expiration_yr"}'> - <?php foreach ($block->getCcMonths() as $k => $v) : ?> + <select id="<?= /* @noEscape */ $code ?>_expiration" + name="payment[cc_exp_month]" + class="select month" + data-validate='{required:true, "validate-cc-exp":"#<?= /* @noEscape */ $code + ?>_expiration_yr"}'> + <?php foreach ($block->getCcMonths() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpMonth) : ?> selected="selected"<?php endif; ?>> + <?php if ($k == $ccExpMonth): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach; ?> @@ -75,9 +79,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <div class="control"> <select id="<?= /* @noEscape */ $code ?>_expiration_yr" name="payment[cc_exp_year]" class="select year" data-validate='{required:true}'> - <?php foreach ($block->getCcYears() as $k => $v) : ?> + <?php foreach ($block->getCcYears() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?> - "<?php if ($k == $ccExpYear) : ?> selected="selected"<?php endif; ?>> + "<?php if ($k == $ccExpYear): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach; ?> @@ -87,7 +91,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); </div> </div> </div> - <?php if ($block->hasVerification()) : ?> + <?php if ($block->hasVerification()): ?> <div class="field cvv required" id="<?= /* @noEscape */ $code ?>_cc_type_cvv_div"> <label for="<?= /* @noEscape */ $code ?>_cc_cid" class="label"> <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> @@ -95,7 +99,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <div class="control"> <input type="number" title="<?= $block->escapeHtml(__('Card Verification Number')) ?>" class="input-text cvv" id="<?= /* @noEscape */ $code ?>_cc_cid" name="payment[cc_cid]" value="" - data-validate='{"required-number":true, "validate-cc-cvn":"#<?= /* @noEscape */ $code ?>_cc_type"}' /> + data-validate='{"required-number":true, "validate-cc-cvn":"#<?= /* @noEscape */ $code ?>_cc_type"}'/> <?php $content = '<img src=\"' . $block->getViewFileUrl('Magento_Checkout::cvv.png') . '\" alt=\"' . $block->escapeHtml(__('Card Verification Number Visual Reference')) . '\" title=\"' . $block->escapeHtml(__('Card Verification Number Visual Reference')) . '\" />'; ?> @@ -110,3 +114,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <?php endif; ?> <?= $block->getChildHtml() ?> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml index 290c8384537fb..b8c2c083a7e98 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml @@ -5,6 +5,8 @@ */ /** @var \Magento\Payment\Block\Transparent\Form $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $code = $block->escapeHtml($block->getMethodCode()); $ccExpMonth = $block->getInfoData('cc_exp_month'); $ccExpYear = $block->getInfoData('cc_exp_year'); @@ -17,8 +19,12 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che <!-- IFRAME for request to Payment Gateway --> <iframe width="0" height="0" id="<?= /* @noEscape */ $code ?>-transparent-iframe" data-container="<?= /* @noEscape */ $code ?>-transparent-iframe" allowtransparency="true" - frameborder="0" name="iframeTransparent" style="display:none;width:100%;background-color:transparent" + frameborder="0" name="iframeTransparent" src="<?= $block->escapeUrl($block->getViewFileUrl('blank.html')) ?>"></iframe> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display: none; width: 100%; background-color: transparent;", + 'iframe#' . /* @noEscape */ $code . '-transparent-iframe' +) ?> <form class="form" id="co-transparent-form" action="#" method="post" data-mage-init='{ "transparent":{ "controller":"<?= $block->escapeHtml($block->getRequest()->getControllerName()) ?>", @@ -27,7 +33,9 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che "cgiUrl":"<?= $block->escapeUrl($block->getCgiUrl()) ?>", "dateDelim":"<?= $block->escapeHtml($block->getDateDelim()) ?>", "cardFieldsMap":<?= $block->escapeHtml($block->getCardFieldsMap()) ?>, - "nativeAction":"<?= $block->escapeUrl($block->getUrl('checkout/onepage/saveOrder', ['_secure' => $block->getRequest()->isSecure()])) ?>" + "nativeAction":"<?= $block->escapeUrl( + $block->getUrl('checkout/onepage/saveOrder', ['_secure' => $block->getRequest()->isSecure()]) + ) ?>" }, "validation":[]}'> <fieldset class="fieldset ccard <?= /* @noEscape */ $code ?>" id="payment_form_<?= /* @noEscape */ $code ?>"> <legend class="legend"> @@ -45,9 +53,9 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che "validate-cc-type-select":"#<?= /* @noEscape */ $code ?>_cc_number" }'> <option value=""><?= $block->escapeHtml(__('--Please Select--')) ?></option> - <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName) : ?> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> <option value="<?= $block->escapeHtml($typeCode) ?>" - <?php if ($typeCode == $ccType) : ?> selected="selected"<?php endif; ?>> + <?php if ($typeCode == $ccType): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($typeName) ?></option> <?php endforeach ?> </select> @@ -83,9 +91,9 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che required:true, "validate-cc-exp":"#<?= /* @noEscape */ $code ?>_expiration_yr" }'> - <?php foreach ($block->getCcMonths() as $k => $v) : ?> + <?php foreach ($block->getCcMonths() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpMonth) : ?> selected="selected"<?php endif; ?>> + <?php if ($k == $ccExpMonth): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> @@ -97,9 +105,9 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che <select id="<?= /* @noEscape */ $code ?>_expiration_yr" name="payment[cc_exp_year]" class="year" data-container="<?= /* @noEscape */ $code ?>-cc-year" data-validate='{required:true}'> - <?php foreach ($block->getCcYears() as $k => $v) : ?> + <?php foreach ($block->getCcYears() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpYear) : ?> selected="selected"<?php endif; ?>> + <?php if ($k == $ccExpYear): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> @@ -109,7 +117,7 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che </div> </div> </div> - <?php if ($block->hasVerification()) : ?> + <?php if ($block->hasVerification()): ?> <div class="field required cvv" id="<?= /* @noEscape */ $code ?>_cc_type_cvv_div"> <label for="<?= /* @noEscape */ $code ?>_cc_cid" class="label"> <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml index d45f014de08a6..233d932e5f642 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml @@ -5,14 +5,20 @@ */ /** @var \Magento\Payment\Block\Transparent\Iframe $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $params = $block->getParams(); ?> <html> <head> - <script> - <?php if (isset($params['redirect'])) : ?> - window.location="<?= $block->escapeUrl($params['redirect']) ?>"; - <?php elseif (isset($params['redirect_parent'])) : ?> + <?php $scriptString = '' ?> + <?php if (isset($params['redirect'])): ?> + <?php $scriptString .= <<<script + window.location="{$block->escapeJs($params['redirect'])}"; +script; + ?> + <?php elseif (isset($params['redirect_parent'])): ?> + <?php $scriptString .= <<<script var require = window.parent.require; require( [ @@ -21,10 +27,15 @@ $params = $block->getParams(); function($) { var parent = window.parent; $(parent).trigger('clearTimeout'); - parent.location="<?= $block->escapeUrl($params['redirect_parent']) ?>"; + parent.location="{$block->escapeJs($params['redirect_parent'])}"; } ); - <?php elseif (isset($params['error_msg'])) : ?> +script; + ?> + <?php elseif (isset($params['error_msg'])): ?> + <?php + $encodedMsg = /* @noEscape */ json_encode($params['error_msg']); + $scriptString .= <<<script var require = window.parent.require; require( [ @@ -33,16 +44,19 @@ $params = $block->getParams(); 'mage/translate', 'Magento_Checkout/js/model/full-screen-loader' ], - function($, globalMessageList, $t, fullScreenLoader) { + function($, globalMessageList, \$t, fullScreenLoader) { var parent = window.parent; $(parent).trigger('clearTimeout'); fullScreenLoader.stopLoader(); globalMessageList.addErrorMessage({ - message: $t(<?= /* @noEscape */ json_encode($params['error_msg'])?>) + message: \$t({$encodedMsg}) }); } ); - <?php elseif (isset($params['multishipping'])) : ?> +script; + ?> + <?php elseif (isset($params['multishipping'])): ?> + <?php $scriptString .= <<<script var require = window.parent.require; require( [ @@ -54,9 +68,15 @@ $params = $block->getParams(); $(parent.document).find('#multishipping-billing-form').submit(); } ); - <?php elseif (isset($params['order_success'])) : ?> - window.parent.location = "<?= $block->escapeUrl($params['order_success']) ?>"; - <?php else : ?> +script; + ?> + <?php elseif (isset($params['order_success'])): ?> + <?php $scriptString .= <<<script + window.parent.location = "{$block->escapeJs($params['order_success'])}"; +script; + ?> + <?php else: ?> + <?php $scriptString .= <<<script var require = window.parent.require; require( [ @@ -85,8 +105,10 @@ $params = $block->getParams(); ); } ); +script; + ?> <?php endif; ?> - </script> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </head> <body></body> </html> diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/info.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/info.phtml index 084e1e0ebf329..49c35e844c39a 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/info.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/info.phtml @@ -6,11 +6,16 @@ /** * @var \Magento\Payment\Block\Transparent\Info $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Payment\Block\Transparent\Info */ ?> -<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none" class="fieldset items redirect"> +<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" class="fieldset items redirect"> <div> <?= $block->escapeHtml(__('We\'ll ask for your payment details before you place an order.')) ?> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/Paypal/Block/Adminhtml/Order/View.php b/app/code/Magento/Paypal/Block/Adminhtml/Order/View.php index 0cbd82798a2c1..82f2b6ab577e0 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/Order/View.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/Order/View.php @@ -19,6 +19,7 @@ /** * Adminhtml sales order view. * @api + * @since 100.2.2 */ class View extends OrderView { @@ -59,6 +60,7 @@ public function __construct( * * @return void * @throws LocalizedException + * @since 100.2.2 */ protected function _construct() { @@ -97,6 +99,7 @@ private function getPaymentAuthorizationUrl(): string * @param Order $order * @return bool * @throws LocalizedException + * @since 100.2.2 */ public function canAuthorize(Order $order): bool { diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Country.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Country.php index 340c34fc2635c..6f29e607df58d 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Country.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Country.php @@ -9,6 +9,7 @@ */ namespace Magento\Paypal\Block\Adminhtml\System\Config\Field; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use Magento\Paypal\Model\Config\StructurePlugin; class Country extends \Magento\Config\Block\System\Config\Form\Field @@ -51,15 +52,17 @@ class Country extends \Magento\Config\Block\System\Config\Form\Field * @param \Magento\Framework\View\Helper\Js $jsHelper * @param \Magento\Directory\Helper\Data $directoryHelper * @param array $data + * @param SecureHtmlRenderer|null $secureHtmlRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Model\Url $url, \Magento\Framework\View\Helper\Js $jsHelper, \Magento\Directory\Helper\Data $directoryHelper, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureHtmlRenderer = null ) { - parent::__construct($context, $data); + parent::__construct($context, $data, $secureHtmlRenderer); $this->_url = $url; $this->_jsHelper = $jsHelper; $this->directoryHelper = $directoryHelper; @@ -132,7 +135,7 @@ protected function _getElementHtml(\Magento\Framework\Data\Form\Element\Abstract } return parent::_getElementHtml($element) . $this->_jsHelper->getScript( - 'require([\'prototype\'], function(){document.observe("dom:loaded", function() {' . $jsString . '});});' + 'require([\'prototype\'], function() { document.observe("dom:loaded", function() {' . $jsString . '}); });' ); } } diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php index 88a33f19de2f4..76fa0856fd23c 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php @@ -7,7 +7,7 @@ /** * Class Bml - * @deprecated + * @deprecated 100.3.1 * "Enable PayPal Credit" setting was removed. Please @see "Disable Funding Options" */ class BmlApi extends AbstractEnable diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Hidden.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Hidden.php index 656d9049b5a40..bf8c563d6ce1d 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Hidden.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Hidden.php @@ -9,8 +9,29 @@ */ namespace Magento\Paypal\Block\Adminhtml\System\Config\Field; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Hidden extends \Magento\Config\Block\System\Config\Form\Field { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct(Context $context, array $data = [], ?SecureHtmlRenderer $secureRenderer = null) + { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($context, $data, $secureRenderer); + $this->secureRenderer = $secureRenderer; + } + /** * Decorate field row html to be invisible * @@ -20,6 +41,10 @@ class Hidden extends \Magento\Config\Block\System\Config\Form\Field */ protected function _decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html) { - return '<tr id="row_' . $element->getHtmlId() . '" style="display: none;">' . $html . '</tr>'; + return '<tr id="row_' . $element->getHtmlId() . '" >' . $html . '</tr>' . + /* @noEscape */ $this->secureRenderer->renderStyleAsTag( + "display: none;", + 'tr#row_' . $element->getHtmlId() + ); } } diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Hint.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Hint.php index 944b30f5792ae..567322fd89372 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Hint.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Hint.php @@ -17,7 +17,7 @@ class Hint extends Template implements RendererInterface { /** * @var string - * @deprecated 100.1.2 + * @deprecated 100.1.0 */ protected $_template = 'Magento_Paypal::system/config/fieldset/hint.phtml'; diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php index b3a575cc8ea9f..d34646a4138eb 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php @@ -5,6 +5,9 @@ */ namespace Magento\Paypal\Block\Adminhtml\System\Config\Fieldset; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Fieldset renderer for PayPal solution */ @@ -15,22 +18,31 @@ class Payment extends \Magento\Config\Block\System\Config\Form\Fieldset */ protected $_backendConfig; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Backend\Model\Auth\Session $authSession * @param \Magento\Framework\View\Helper\Js $jsHelper * @param \Magento\Config\Model\Config $backendConfig * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Backend\Model\Auth\Session $authSession, \Magento\Framework\View\Helper\Js $jsHelper, \Magento\Config\Model\Config $backendConfig, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_backendConfig = $backendConfig; - parent::__construct($context, $authSession, $jsHelper, $data); + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($context, $authSession, $jsHelper, $data, $secureRenderer); + $this->secureRenderer = $secureRenderer; } /** @@ -90,19 +102,20 @@ protected function _getHeaderTitleHtml($element) ' class="button action-configure' . (empty($groupConfig['paypal_ec_separate']) ? '' : ' paypal-ec-separate') . $disabledClassString . - '" id="' . - $htmlId . - '-head" onclick="paypalToggleSolution.call(this, \'' . - $htmlId . - "', '" . - $this->getUrl( - 'adminhtml/*/state' - ) . '\'); return false;"><span class="state-closed">' . __( + '" id="' . $htmlId . '-head" >' . + '<span class="state-closed">' . __( 'Configure' ) . '</span><span class="state-opened">' . __( 'Close' ) . '</span></button>'; + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "paypalToggleSolution.call(this, '" . $htmlId . "', '" . $this->getUrl('adminhtml/*/state') . + "');event.preventDefault();", + 'button#' . $htmlId . '-head' + ); + if (!empty($groupConfig['more_url'])) { $html .= '<a class="link-more" href="' . $groupConfig['more_url'] . '" target="_blank">' . __( 'Learn More' @@ -151,6 +164,8 @@ protected function _isCollapseState($element) } /** + * Return extra Js. + * * @param \Magento\Framework\Data\Form\Element\AbstractElement $element * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -162,7 +177,7 @@ protected function _getExtraJs($element) var doScroll = false; Fieldset.toggleCollapse(id, url); if ($(this).hasClassName(\"open\")) { - $$(\".with-button button.button\").each(function(anotherButton) { + \$$(\".with-button button.button\").each(function(anotherButton) { if (anotherButton != this && $(anotherButton).hasClassName(\"open\")) { $(anotherButton).click(); doScroll = true; diff --git a/app/code/Magento/Paypal/Block/Express/InContext/Component.php b/app/code/Magento/Paypal/Block/Express/InContext/Component.php index bb5c17a18fe95..d1adf058e3b4f 100644 --- a/app/code/Magento/Paypal/Block/Express/InContext/Component.php +++ b/app/code/Magento/Paypal/Block/Express/InContext/Component.php @@ -5,14 +5,16 @@ */ namespace Magento\Paypal\Block\Express\InContext; +use Magento\Framework\App\ObjectManager; use Magento\Paypal\Model\Config; use Magento\Paypal\Model\ConfigFactory; use Magento\Framework\View\Element\Template; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\Json\Helper\Data as JsonHelper; /** - * Class Component + * Paypal Express InContext Component. * * @api * @since 100.1.0 @@ -32,15 +34,20 @@ class Component extends Template private $config; /** - * @inheritdoc + * @param Context $context * @param ResolverInterface $localeResolver + * @param ConfigFactory $configFactory + * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( Context $context, ResolverInterface $localeResolver, ConfigFactory $configFactory, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); $this->localeResolver = $localeResolver; $this->config = $configFactory->create(); @@ -62,6 +69,8 @@ protected function _toHtml() } /** + * Check if is in Context. + * * @return bool */ private function isInContext() @@ -70,6 +79,8 @@ private function isInContext() } /** + * Return environment. + * * @return string * @since 100.1.0 */ @@ -79,6 +90,8 @@ public function getEnvironment() } /** + * Return locale. + * * @return string * @since 100.1.0 */ @@ -88,6 +101,8 @@ public function getLocale() } /** + * Return merchant id. + * * @return string * @since 100.1.0 */ @@ -97,6 +112,8 @@ public function getMerchantId() } /** + * Check if button is in context. + * * @return bool * @since 100.1.0 */ diff --git a/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php index 8d1e04c1397fc..6b4071120b511 100644 --- a/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php +++ b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php @@ -16,7 +16,7 @@ /** * Class Button - * @deprecated @see \Magento\Paypal\Block\Express\InContext\Minicart\SmartButton + * @deprecated 100.3.1 @see \Magento\Paypal\Block\Express\InContext\Minicart\SmartButton */ class Button extends Template implements ShortcutInterface { diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/Cancel.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/Cancel.php index c469338d03961..375a2639ab073 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/Cancel.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/Cancel.php @@ -31,6 +31,7 @@ public function execute() ->unsLastSuccessQuoteId() ->unsLastOrderId() ->unsLastRealOrderId(); + $this->_getSession()->unsQuoteId(); // clean quote from session that was set in OnAuthorization $this->messageManager->addSuccessMessage( __('Express Checkout and Order have been canceled.') ); diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php index 055af4162d5f3..29d4a5bd1f25c 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php @@ -11,7 +11,7 @@ use Magento\Paypal\Model\Api\ProcessableException as ApiProcessableException; /** - * Class PlaceOrder + * Creates order on backend and prepares session to show appropriate next step in flow * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PlaceOrder extends \Magento\Paypal\Controller\Express\AbstractExpress @@ -127,6 +127,7 @@ public function execute() return; } $this->_initToken(false); // no need in token anymore + $this->_getSession()->unsQuoteId(); // clean quote from session that was set in OnAuthorization $this->_redirect('checkout/onepage/success'); return; } catch (ApiProcessableException $e) { diff --git a/app/code/Magento/Paypal/Model/AbstractConfig.php b/app/code/Magento/Paypal/Model/AbstractConfig.php index cf7e8009dab65..c8ad56f242a63 100644 --- a/app/code/Magento/Paypal/Model/AbstractConfig.php +++ b/app/code/Magento/Paypal/Model/AbstractConfig.php @@ -58,7 +58,7 @@ abstract class AbstractConfig implements ConfigInterface /** * @var string */ - private static $bnCode = 'Magento_Cart_%s'; + private static $bnCode = 'Magento_2_%s'; /** * @var \Magento\Framework\App\Config\ScopeConfigInterface @@ -229,7 +229,7 @@ public function shouldUseUnilateralPayments() /** * Check whether WPP API credentials are available for this method * - * @deprecated + * @deprecated 100.3.1 * @return bool */ public function isWppApiAvailabe() diff --git a/app/code/Magento/Paypal/Model/Api/Nvp.php b/app/code/Magento/Paypal/Model/Api/Nvp.php index 9e4f7693f4bfb..b35f783482e06 100644 --- a/app/code/Magento/Paypal/Model/Api/Nvp.php +++ b/app/code/Magento/Paypal/Model/Api/Nvp.php @@ -1423,7 +1423,7 @@ protected function _validateResponse($method, $response) */ protected function _deformatNVP($nvpstr) { - $intial = 0; + $initial = 0; $nvpArray = []; $nvpstr = strpos($nvpstr, "\r\n\r\n") !== false ? substr($nvpstr, strpos($nvpstr, "\r\n\r\n") + 4) : $nvpstr; @@ -1435,7 +1435,7 @@ protected function _deformatNVP($nvpstr) $valuepos = strpos($nvpstr, '&') ? strpos($nvpstr, '&') : strlen($nvpstr); /*getting the Key and Value values and storing in a Associative Array*/ - $keyval = substr($nvpstr, $intial, $keypos); + $keyval = substr($nvpstr, $initial, $keypos); $valval = substr($nvpstr, $keypos + 1, $valuepos - $keypos - 1); //decoding the response $nvpArray[urldecode($keyval)] = urldecode($valval); @@ -1465,7 +1465,7 @@ protected function _exportLineItems(array &$request, $i = 0) * * @param array $data * @return void - * @deprecated 100.2.2 typo in method name + * @deprecated 100.2.4 typo in method name * @see _exportAddresses */ protected function _exportAddressses($data) diff --git a/app/code/Magento/Paypal/Model/Payflowpro.php b/app/code/Magento/Paypal/Model/Payflowpro.php index 778cd0c728de3..f8d17c5a5fdfe 100644 --- a/app/code/Magento/Paypal/Model/Payflowpro.php +++ b/app/code/Magento/Paypal/Model/Payflowpro.php @@ -8,6 +8,8 @@ use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\State\InvalidTransitionException; +use Magento\Payment\Gateway\Command\CommandException; use Magento\Payment\Helper\Formatter; use Magento\Payment\Model\InfoInterface; use Magento\Payment\Model\Method\ConfigInterface; @@ -85,6 +87,8 @@ class Payflowpro extends \Magento\Payment\Model\Method\Cc implements GatewayInte const RESPONSE_CODE_VOID_ERROR = 108; + private const RESPONSE_CODE_AUTHORIZATION_EXPIRED = 10601; + const PNREF = 'pnref'; /**#@-*/ @@ -376,7 +380,7 @@ public function getConfigPaymentAction() * @param float $amount * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\State\InvalidTransitionException + * @throws InvalidTransitionException */ public function authorize(\Magento\Payment\Model\InfoInterface $payment, $amount) { @@ -410,7 +414,7 @@ protected function _getCaptureAmount($amount) * @param float $amount * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\State\InvalidTransitionException + * @throws InvalidTransitionException */ public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount) { @@ -448,7 +452,7 @@ public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount) * @param InfoInterface|Payment|Object $payment * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\State\InvalidTransitionException + * @throws InvalidTransitionException */ public function void(\Magento\Payment\Model\InfoInterface $payment) { @@ -491,14 +495,23 @@ public function canVoid() * * @param InfoInterface|Object $payment * @return $this + * @throws CommandException */ public function cancel(\Magento\Payment\Model\InfoInterface $payment) { if (!$payment->getOrder()->getInvoiceCollection()->count()) { - return $this->void($payment); + try { + $this->void($payment); + } catch (CommandException $e) { + // Ignore error about expiration of authorization transaction. + if (strpos($e->getMessage(), (string)self::RESPONSE_CODE_AUTHORIZATION_EXPIRED) === false) { + throw $e; + } + } + } - return false; + return $this; } /** @@ -508,7 +521,7 @@ public function cancel(\Magento\Payment\Model\InfoInterface $payment) * @param float $amount * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\State\InvalidTransitionException + * @throws InvalidTransitionException */ public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount) { @@ -558,7 +571,7 @@ public function fetchTransactionInfo(InfoInterface $payment, $transactionId) * @return bool * phpcs:disable Magento2.Functions.StaticFunction */ - protected static function _isTransactionUnderReview($status) + protected function _isTransactionUnderReview($status) { if (in_array($status, [self::RESPONSE_CODE_APPROVED, self::RESPONSE_CODE_DECLINED_BY_MERCHANT])) { return false; @@ -650,21 +663,22 @@ public function buildBasicRequest() * * @param DataObject $response * @return void - * @throws \Magento\Payment\Gateway\Command\CommandException - * @throws \Magento\Framework\Exception\State\InvalidTransitionException + * @throws CommandException + * @throws InvalidTransitionException */ public function processErrors(DataObject $response) { - if ($response->getResultCode() == self::RESPONSE_CODE_VOID_ERROR) { - throw new \Magento\Framework\Exception\State\InvalidTransitionException( + $resultCode = (int)$response->getResultCode(); + if ($resultCode === self::RESPONSE_CODE_VOID_ERROR) { + throw new InvalidTransitionException( __("The verification transaction can't be voided. ") ); - } elseif ($response->getResultCode() != self::RESPONSE_CODE_APPROVED && - $response->getResultCode() != self::RESPONSE_CODE_FRAUDSERVICE_FILTER - ) { - throw new \Magento\Payment\Gateway\Command\CommandException(__($response->getRespmsg())); - } elseif ($response->getOrigresult() == self::RESPONSE_CODE_DECLINED_BY_FILTER) { - throw new \Magento\Payment\Gateway\Command\CommandException(__($response->getRespmsg())); + } + if (!in_array($resultCode, [self::RESPONSE_CODE_APPROVED, self::RESPONSE_CODE_FRAUDSERVICE_FILTER])) { + throw new CommandException(__($response->getRespmsg())); + } + if ((int)$response->getOrigresult() === self::RESPONSE_CODE_DECLINED_BY_FILTER) { + throw new CommandException(__($response->getRespmsg())); } } diff --git a/app/code/Magento/Paypal/Model/SmartButtonConfig.php b/app/code/Magento/Paypal/Model/SmartButtonConfig.php index 88d68511ae5fe..8adff75df205b 100644 --- a/app/code/Magento/Paypal/Model/SmartButtonConfig.php +++ b/app/code/Magento/Paypal/Model/SmartButtonConfig.php @@ -11,7 +11,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Locale\ResolverInterface; use Magento\Store\Model\ScopeInterface; -use Magento\Paypal\Model\Config as PayPalConfig; +use Magento\Store\Model\StoreManagerInterface; /** * Provides configuration values for PayPal in-context checkout @@ -50,6 +50,11 @@ class SmartButtonConfig */ private $unsupportedPaymentMethods; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * Base url for Paypal SDK */ @@ -59,6 +64,7 @@ class SmartButtonConfig * @param ResolverInterface $localeResolver * @param ConfigFactory $configFactory * @param ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager * @param array $defaultStyles * @param array $disallowedFundingMap * @param array $unsupportedPaymentMethods @@ -67,6 +73,7 @@ public function __construct( ResolverInterface $localeResolver, ConfigFactory $configFactory, ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager, $defaultStyles = [], $disallowedFundingMap = [], $unsupportedPaymentMethods = [] @@ -75,6 +82,7 @@ public function __construct( $this->config = $configFactory->create(); $this->config->setMethod(Config::METHOD_EXPRESS); $this->scopeConfig = $scopeConfig; + $this->storeManager = $storeManager; $this->defaultStyles = $defaultStyles; $this->disallowedFundingMap = $disallowedFundingMap; $this->unsupportedPaymentMethods = $unsupportedPaymentMethods; @@ -123,6 +131,7 @@ private function generatePaypalSdkUrl(string $page): string 'merchant-id' => $this->config->getValue('merchant_id'), 'locale' => $this->localeResolver->getLocale(), 'intent' => $this->getIntent(), + 'currency' => $this->storeManager->getStore()->getBaseCurrencyCode(), ]; if ($disallowedFunding) { $params['disable-funding'] = $disallowedFunding; diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml index 5619aa27860ce..a2c7b7d82a349 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml @@ -9,7 +9,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup"> <arguments> - <argument name="payerName" defaultValue="MPI" type="string"/> <argument name="credentials" defaultValue="_CREDS"/> </arguments> <!--Check in-context--> @@ -17,6 +16,8 @@ <waitForPageLoad stepKey="waitForPageLoad"/> <seeCurrentUrlMatches regex="~\//www.sandbox.paypal.com/~" stepKey="seeCurrentUrlMatchesConfigPath1"/> <conditionalClick selector="{{PayPalPaymentSection.notYouLink}}" dependentSelector="{{PayPalPaymentSection.notYouLink}}" visible="true" stepKey="selectNotYouSection"/> + <conditionalClick selector="{{PayPalPaymentSection.existingAccountLoginBtn}}" dependentSelector="{{PayPalPaymentSection.existingAccountLoginBtn}}" visible="true" stepKey="skipAccountCreationAndLogin"/> + <waitForPageLoad stepKey="waitForLoginPageLoad"/> <waitForElement selector="{{PayPalPaymentSection.email}}" stepKey="waitForLoginForm" /> <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{credentials.magento/paypal_sandbox_login_email}}" stepKey="fillEmail"/> <click selector="{{PayPalPaymentSection.nextButton}}" stepKey="clickNext"/> @@ -25,6 +26,5 @@ <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{credentials.magento/paypal_sandbox_login_password}}" stepKey="fillPassword"/> <click selector="{{PayPalPaymentSection.loginBtn}}" stepKey="login"/> <waitForPageLoad stepKey="wait"/> - <see userInput="{{payerName}}" selector="{{PayPalPaymentSection.reviewUserInfo}}" stepKey="seePayerName"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml index f627b9158f868..aa682cb7a3bb3 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml @@ -8,7 +8,10 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="StorefrontLoginToPayPalPaymentFromCartAccountActionGroup" extends="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup"> + <arguments> + <argument name="payerName" defaultValue="MPI" type="string"/> + </arguments> <seeElement selector="{{PayPalCheckoutAsGuestSection.CreditDebitBtn}}" stepKey="assertCheckoutAsGuest" before="waitForLoginForm"/> - <see userInput="{{payerName}}" selector="{{PayPalPaymentSection.userName}}" stepKey="seePayerName"/> + <see userInput="{{payerName}}" selector="{{PayPalPaymentSection.userName}}" stepKey="seePayerName" after="assertCheckoutAsGuest"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml index 361016c40539c..e53c1bbc1ec29 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml @@ -10,6 +10,7 @@ <section name="PayPalPaymentSection"> <element name="guestCheckout" type="input" selector="#guest"/> <element name="loginSection" type="input" selector=" #main>#login"/> + <element name="existingAccountLoginBtn" type="input" selector="#loginSection a"/> <element name="email" type="input" selector="//input[contains(@name, 'email') and not(contains(@style, 'display:none'))]"/> <element name="password" type="input" selector="//input[contains(@name, 'password') and not(contains(@style, 'display:none'))]"/> <element name="loginBtn" type="input" selector="button#btnLogin"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml b/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml index b52fc05ca5a11..44ec500722e58 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml @@ -13,7 +13,9 @@ <!--Config PayPal Express Checkout--> <actionGroup ref="ConfigPayPalExpressCheckoutActionGroup" stepKey="ConfigPayPalExpressCheckout"/> <!-- Configure PayPal Express Checkout --> - <magentoCLI command="cache:clean" arguments="config full_page" stepKey="cleanFullPageCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanFullPageCache"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <!-- Cleanup Paypal configurations --> @@ -21,7 +23,9 @@ <magentoCLI command="config:set {{StorefrontPaypalDisableInContextCheckoutConfigData.path}} {{StorefrontPaypalDisableInContextCheckoutConfigData.value}}" stepKey="disableInContextPayPal"/> <magentoCLI command="config:set {{StorefrontPaypalDisableConfigData.path}} {{StorefrontPaypalDisableConfigData.value}}" stepKey="disablePaypal"/> <createData entity="SamplePaypalConfig" stepKey="setDefaultPaypalConfig"/> - <magentoCLI command="cache:clean" arguments="config full_page" stepKey="cleanFullPageCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanFullPageCache"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <include> <group name="paypalExpress"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml index cae67f411200c..e21655763e7a3 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml @@ -36,7 +36,9 @@ <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonVerticalLayoutConfigData.path}} {{StorefrontPaypalCheckoutPageButtonVerticalLayoutConfigData.value}}" stepKey="setLayoutForPayPalSmartButton"/> <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonPillShapeConfigData.path}} {{StorefrontPaypalCheckoutPageButtonPillShapeConfigData.value}}" stepKey="setShapeForPayPalSmartButton"/> <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonBlueColorConfigData.path}} {{StorefrontPaypalCheckoutPageButtonBlueColorConfigData.value}}" stepKey="setColorForPayPalSmartButton"/> - <magentoCLI command="cache:clean" arguments="full_page" stepKey="cleanFullPageCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanFullPageCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageDisableCustomizeButtonConfigData.path}} {{StorefrontPaypalCheckoutPageDisableCustomizeButtonConfigData.value}}" stepKey="disableCustomizeButton"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml index 2cc94caf4c1b1..cea228ac7a344 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml @@ -16,10 +16,10 @@ <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"/> - <group value="paypalExpress"/> <skip> - <issueId value="MC-33951"/> + <issueId value="MC-37236"/> </skip> + <group value="paypalExpress"/> </annotations> <before> <!-- Login --> @@ -64,9 +64,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Free Shipping')}}" stepKey="selectFlatShippingMethod"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> - - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> <!--Assert grand total--> <actionGroup ref="VerifyCheckoutPaymentOrderSummaryActionGroup" stepKey="verifyCheckoutPaymentOrderSummary"> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml index 41578eed67625..53f9f8adf4d44 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml @@ -79,9 +79,8 @@ <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" stepKey="clickPayPalBtn"/> <!--Login to Paypal in-context--> - <actionGroup ref="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup" stepKey="LoginToPayPal"> - <argument name="payerName" value="{{Payer.firstName}}"/> - </actionGroup> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup" stepKey="LoginToPayPal"/> + <!--Transfer Cart Line and Shipping Method assertion--> <actionGroup ref="PayPalAssertTransferLineAndShippingMethodNotExistActionGroup" stepKey="assertPayPalSettings"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml index 69ec26a8ea806..f949235e98025 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml @@ -22,7 +22,12 @@ </skip> </annotations> <before> - + <!--Set price scope global--> + <magentoCLI command="config:set {{CatalogPriceScopeGlobalConfigData.path}} {{CatalogPriceScopeGlobalConfigData.value}}" stepKey="setCatalogPriceScopeWebsite"/> + <!--Remove Currency options for Website--> + <remove keyForRemoval="setCurrencyBaseEURWebsites"/> + <remove keyForRemoval="setAllowedCurrencyWebsitesForEURandUSD"/> + <remove keyForRemoval="setCurrencyDefaultEURWebsites"/> <!--Enable Advanced Setting--> <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> <!--Set merchant country--> @@ -31,10 +36,6 @@ <magentoCLI command="config:set {{SetCurrencyAUDBaseConfig.path}} {{SetCurrencyAUDBaseConfig.value}}" stepKey="setCurrencyBaseEUR"/> <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForAUD.value}}" stepKey="setAllowedCurrencyEURandUSD"/> <magentoCLI command="config:set {{SetDefaultCurrencyAUDConfig.path}} {{SetDefaultCurrencyAUDConfig.value}}" stepKey="setCurrencyDefaultEUR"/> - <!--Set Currency options for Website--> - <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseEURWebsites"/> - <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForAUD.value}}" stepKey="setAllowedCurrencyWebsitesForEURandUSD"/> - <magentoCLI command="config:set --scope={{SetDefaultCurrencyAUDConfig.scope}} --scope-code={{SetDefaultCurrencyAUDConfig.scope_code}} {{SetDefaultCurrencyAUDConfig.path}} {{SetDefaultCurrencyAUDConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> </before> <after> <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml index 5077544ea0b39..bd756e4df176d 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml @@ -22,6 +22,12 @@ </skip> </annotations> <before> + <!--Set price scope global--> + <magentoCLI command="config:set {{CatalogPriceScopeGlobalConfigData.path}} {{CatalogPriceScopeGlobalConfigData.value}}" stepKey="setCatalogPriceScopeWebsite"/> + <!--Remove Currency options for Website--> + <remove keyForRemoval="setCurrencyBaseEURWebsites"/> + <remove keyForRemoval="setAllowedCurrencyWebsitesForEURandUSD"/> + <remove keyForRemoval="setCurrencyDefaultEURWebsites"/> <!--Set merchant country--> <magentoCLI command="config:set {{MerchantUnitedKingdom.path}} {{MerchantUnitedKingdom.value}}" stepKey="setMerchantCountryUK"/> <!--Enable Advanced Setting--> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml index 22997b7005f91..a4d99ecbf7e61 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml @@ -16,17 +16,20 @@ <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"/> - <group value="paypalExpress"/> <skip> - <issueId value="MC-33951"/> + <issueId value="MC-37236"/> </skip> + <group value="paypalExpress"/> </annotations> <before> + <!--Set price scope global--> + <magentoCLI command="config:set {{CatalogPriceScopeGlobalConfigData.path}} {{CatalogPriceScopeGlobalConfigData.value}}" stepKey="setCatalogPriceScopeWebsite"/> <!--Set merchant country--> <magentoCLI command="config:set {{MerchantFrance.path}} {{MerchantFrance.value}}" stepKey="setMerchantCountryUK"/> <!--Enable Advanced Setting--> <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> @@ -34,6 +37,11 @@ <magentoCLI command="config:set {{MerchantUnitedStates.path}} {{MerchantUnitedStates.value}}" stepKey="setMerchantCountryDefault"/> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> </after> + <!-- Switch to USD-US Dollar--> + <actionGroup ref="StorefrontSwitchCurrencyActionGroup" after="waitForProductPagePageLoad" stepKey="switchCurrency"> + <argument name="currency" value="USD"/> + </actionGroup> + <!-- click on PayPal payment radio button --> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" after="guestCheckoutFillingShippingSection" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" stepKey="guestSelectCheckMoneyOrderPayment"/> @@ -52,5 +60,10 @@ <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" after="LoginToPayPal" stepKey="submitPayment"/> <waitForElement after="submitPayment" selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="waitForOrderNumber"/> + <see selector="{{AdminOrderDetailsInformationSection.orderInformationTable}}" userInput="USD / EUR rate" stepKey="seeEURandUSDRate"/> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">grabRate</actualResult> + <expectedResult type="array">[USD / EUR rate:]</expectedResult> + </assertEquals> </test> </tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml index cf0e4b3d0b370..f17fa203af9f5 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml @@ -28,11 +28,15 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/CountryTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/CountryTest.php index eebe7c2201689..c06bb6d847225 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/CountryTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/CountryTest.php @@ -12,12 +12,13 @@ use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Helper\Js; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use Magento\Paypal\Block\Adminhtml\System\Config\Field\Country; use Magento\Paypal\Model\Config\StructurePlugin; -use PHPUnit\Framework\Constraint\LogicalAnd; use PHPUnit\Framework\Constraint\StringContains; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Directory\Helper\Data as DirectoryHelper; class CountryTest extends TestCase { @@ -46,6 +47,11 @@ class CountryTest extends TestCase */ protected $_url; + /** + * @var DirectoryHelper + */ + private $helper; + protected function setUp(): void { $helper = new ObjectManager($this); @@ -70,9 +76,29 @@ protected function setUp(): void $this->_request = $this->getMockForAbstractClass(RequestInterface::class); $this->_jsHelper = $this->createMock(Js::class); $this->_url = $this->createMock(Url::class); + $this->helper = $this->createMock(DirectoryHelper::class); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); $this->_model = $helper->getObject( Country::class, - ['request' => $this->_request, 'jsHelper' => $this->_jsHelper, 'url' => $this->_url] + [ + 'request' => $this->_request, + 'jsHelper' => $this->_jsHelper, + 'url' => $this->_url, + 'directoryHelper' => $this->helper, + 'secureHtmlRenderer' => $secureRendererMock + ] ); } @@ -104,15 +130,7 @@ public function testRender($requestCountry, $requestDefaultCountry, $canUseDefau '$("' . $this->_element->getHtmlId() . '").observe("change", function () {' ), ]; - if ($canUseDefault && ($requestCountry == 'US') && $requestDefaultCountry) { - $constraints[] = new StringContains( - '$("' . $this->_element->getHtmlId() . '_inherit").observe("click", function () {' - ); - } - $this->_jsHelper->expects($this->once()) - ->method('getScript') - ->with(new LogicalAnd($constraints)); - $this->_url->expects($this->once()) + $this->_url->expects($this->at(0)) ->method('getUrl') ->with( '*/*/*', @@ -123,6 +141,27 @@ public function testRender($requestCountry, $requestDefaultCountry, $canUseDefau StructurePlugin::REQUEST_PARAM_COUNTRY => '__country__' ] ); + if ($canUseDefault && ($requestCountry == 'US') && $requestDefaultCountry) { + $this->helper->method('getDefaultCountry')->willReturn($requestDefaultCountry); + $constraints[] = new StringContains( + '$("' . $this->_element->getHtmlId() . '_inherit").observe("click", function () {' + ); + $this->_url->expects($this->at(1)) + ->method('getUrl') + ->with( + '*/*/*', + [ + 'section' => 'section', + 'website' => 'website', + 'store' => 'store', + StructurePlugin::REQUEST_PARAM_COUNTRY => '__country__', + Country::REQUEST_PARAM_DEFAULT_COUNTRY => '__default__' + ] + ); + } + $this->_jsHelper->expects($this->once()) + ->method('getScript') + ->with(self::logicalAnd(...$constraints)); $this->_model->render($this->_element); } diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php index 42578f5a53e39..a7b08499df7cd 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php @@ -7,12 +7,15 @@ namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; use Magento\Framework\Data\Form; use Magento\Framework\Data\Form\Element\AbstractElement; -use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable\Stub; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use PHPUnit\Framework\MockObject\MockObject; +use Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable\Stub; use PHPUnit\Framework\TestCase; /** @@ -34,6 +37,22 @@ class AbstractEnableTest extends TestCase */ protected $elementMock; + /** + * Create mock objects. + * + * @param string[] $classes + * @return MockObject[] + */ + private function createMocks(array $classes): array + { + $mocks = []; + foreach ($classes as $class) { + $mocks[] = $this->getMockBuilder($class)->disableOriginalConstructor()->getMock(); + } + + return $mocks; + } + /** * Set up * @@ -43,14 +62,24 @@ protected function setUp(): void { $objectManager = new ObjectManager($this); + $randomMock = $this->getMockBuilder(Random::class)->disableOriginalConstructor()->getMock(); + $randomMock->method('getRandomString')->willReturn('12345abcdef'); + $mockArguments = $this->createMocks([ + \Magento\Framework\Data\Form\Element\Factory::class, + CollectionFactory::class, + Escaper::class + ]); + $mockArguments[] = []; + $mockArguments[] = $this->createMock(SecureHtmlRenderer::class); + $mockArguments[] = $randomMock; $this->elementMock = $this->getMockBuilder(AbstractElement::class) ->setMethods( [ 'getHtmlId', 'getTooltip', - 'getForm', + 'getForm' ] - )->disableOriginalConstructor() + )->setConstructorArgs($mockArguments) ->getMockForAbstractClass(); $objectManager = new ObjectManager($this); diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php index af9908a5ec20c..8ac4c5268094d 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php @@ -14,6 +14,7 @@ use Magento\User\Model\User; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class GroupTest extends TestCase { @@ -79,9 +80,23 @@ protected function setUp(): void ->method('__call') ->with('getUser') ->willReturn($this->_user); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); + $this->_model = $helper->getObject( \Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Group::class, - ['authSession' => $this->_authSession] + ['authSession' => $this->_authSession, 'secureRenderer' => $secureRendererMock] ); $this->_model->setGroup($this->_group); } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php index 44151dc81a34b..e532e50a51209 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php @@ -15,6 +15,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Paypal\Model\AbstractConfig + */ class AbstractConfigTest extends TestCase { @@ -353,7 +356,7 @@ public function testGetBuildNotationCode() $productMetadata ); - self::assertEquals('Magento_Cart_SomeEdition', $this->config->getBuildNotationCode()); + self::assertEquals('Magento_2_SomeEdition', $this->config->getBuildNotationCode()); } /** diff --git a/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php index fe7619e4166ba..f7ee15efa3ab9 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php @@ -12,6 +12,8 @@ use Magento\Paypal\Model\Config; use Magento\Paypal\Model\ConfigFactory; use Magento\Paypal\Model\SmartButtonConfig; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -56,10 +58,22 @@ protected function setUp(): void ->setMethods(['create']) ->getMock(); $configFactoryMock->expects($this->once())->method('create')->willReturn($this->configMock); + + /** @var Store|MockObject $storeMock */ + $storeMock = $this->createMock(Store::class); + $storeMock->method('getBaseCurrencyCode') + ->willReturn('USD'); + + /** @var StoreManagerInterface|MockObject $storeManagerMock */ + $storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + $storeManagerMock->method('getStore') + ->willReturn($storeMock); + $this->model = new SmartButtonConfig( $this->localeResolverMock, $configFactoryMock, $scopeConfigMock, + $storeManagerMock, $this->getDefaultStyles(), $this->getDisallowedFundingMap(), $this->getUnsupportedPaymentMethods() diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php index 6089b8b20b1ac..a7bd43e53085f 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php @@ -46,6 +46,7 @@ function generateExpectedPaypalSdkUrl(array $params) : String 'merchant-id' => 'merchant', 'locale' => 'es_MX', 'intent' => 'authorize', + 'currency' => 'USD', 'disable-funding' => implode( ',', ['credit', 'venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] @@ -84,6 +85,7 @@ function generateExpectedPaypalSdkUrl(array $params) : String 'merchant-id' => 'merchant', 'locale' => 'en_BR', 'intent' => 'authorize', + 'currency' => 'USD', 'disable-funding' => implode( ',', ['venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] @@ -121,6 +123,7 @@ function generateExpectedPaypalSdkUrl(array $params) : String 'merchant-id' => 'merchant', 'locale' => 'en_US', 'intent' => 'authorize', + 'currency' => 'USD', 'disable-funding' => implode( ',', ['venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] @@ -158,6 +161,7 @@ function generateExpectedPaypalSdkUrl(array $params) : String 'merchant-id' => 'merchant', 'locale' => 'en_US', 'intent' => 'authorize', + 'currency' => 'USD', 'disable-funding' => implode( ',', ['credit','venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] @@ -196,6 +200,7 @@ function generateExpectedPaypalSdkUrl(array $params) : String 'merchant-id' => 'merchant', 'locale' => 'en_BR', 'intent' => 'authorize', + 'currency' => 'USD', 'disable-funding' => implode( ',', ['card','venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml index fe3ed6ce5e00e..2c5d669ccd9e9 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml @@ -623,7 +623,7 @@ <label>Header Image URL</label> <config_path>paypal/style/paypal_hdrimg</config_path> <tooltip> - <![CDATA[The image at the top left of the checkout page. Max size is 750x90-pixel. <strong style="color:red">https</strong> is highly encouraged.]]> + <![CDATA[The image at the top left of the checkout page. Max size is 750x90-pixel. <strong class="colorRed">https</strong> is highly encouraged.]]> </tooltip> <attribute type="shared">1</attribute> </field> diff --git a/app/code/Magento/Paypal/etc/csp_whitelist.xml b/app/code/Magento/Paypal/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..932664bde9e09 --- /dev/null +++ b/app/code/Magento/Paypal/etc/csp_whitelist.xml @@ -0,0 +1,35 @@ +<?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="paypal_analytics" type="host">t.paypal.com</value> + <value id="www_paypal" type="host">www.paypal.com</value> + <value id="paypal_objects" type="host">www.paypalobjects.com</value> + <value id="paypal_fpdbs" type="host">fpdbs.paypal.com</value> + <value id="paypal_fpdbs_sandbox" type="host">fpdbs.sandbox.paypal.com</value> + </values> + </policy> + <policy id="script-src"> + <values> + <value id="www_paypal" type="host">www.paypal.com</value> + <value id="www_sandbox_paypal" type="host">www.sandbox.paypal.com</value> + <value id="paypal_objects" type="host">www.paypalobjects.com</value> + <value id="paypal_analytics" type="host">t.paypal.com</value> + </values> + </policy> + <policy id="frame-src"> + <values> + <value id="www_paypal" type="host">www.paypal.com</value> + <value id="www_sandbox_paypal" type="host">www.sandbox.paypal.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/Paypal/i18n/en_US.csv b/app/code/Magento/Paypal/i18n/en_US.csv index 852bf39c57966..8db6285dc157e 100644 --- a/app/code/Magento/Paypal/i18n/en_US.csv +++ b/app/code/Magento/Paypal/i18n/en_US.csv @@ -585,9 +585,9 @@ Schedule,Schedule " "Header Image URL","Header Image URL" " - The image at the top left of the checkout page. Max size is 750x90-pixel. <strong style=""color:red"">https</strong> is highly encouraged. + The image at the top left of the checkout page. Max size is 750x90-pixel. <strong class=""colorRed"">https</strong> is highly encouraged. "," - The image at the top left of the checkout page. Max size is 750x90-pixel. <strong style=""color:red"">https</strong> is highly encouraged. + The image at the top left of the checkout page. Max size is 750x90-pixel. <strong class=""colorRed"">https</strong> is highly encouraged. " "Header Background Color","Header Background Color" " diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/billing/agreement/form.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/billing/agreement/form.phtml index 19cebe863b7ef..7413c29fdd59e 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/billing/agreement/form.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/billing/agreement/form.phtml @@ -4,10 +4,13 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Paypal\Block\Adminhtml\Billing\Agreement\View\Form $block */ +/** + * @var \Magento\Paypal\Block\Adminhtml\Billing\Agreement\View\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?php $code = $block->escapeHtml($block->getMethodCode()) ?> -<fieldset class="form-list" id="payment_form_<?= /* @noEscape */ $code ?>" style="display:none;"> +<fieldset class="form-list" id="payment_form_<?= /* @noEscape */ $code ?>"> <div class="admin__field _required"> <label for="<?= /* @noEscape */ $code ?>_ba_agreement_id" class="admin__field-label"> <span><?= $block->escapeHtml(__('Billing Agreement')) ?></span> @@ -17,7 +20,7 @@ name="payment[<?= $block->escapeHtml($block->getTransportBAId()) ?>]" class="required-entry admin__control-select"> <option value=""><?= $block->escapeHtml(__('Please Select')) ?></option> - <?php foreach ($block->getBillingAgreements() as $id => $referenceId) : ?> + <?php foreach ($block->getBillingAgreements() as $id => $referenceId): ?> <option value="<?= $block->escapeHtml($id) ?>"> <?= $block->escapeHtml($referenceId) ?> </option> @@ -26,3 +29,7 @@ </div> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/payment/form/billing/agreement.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/payment/form/billing/agreement.phtml index a4e7b6974c737..b37bd261ce1a5 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/payment/form/billing/agreement.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/payment/form/billing/agreement.phtml @@ -4,11 +4,13 @@ * See COPYING.txt for license details. */ -/* @var $block \Magento\Paypal\Block\Payment\Form\Billing\Agreement */ +/** + * @var $block \Magento\Paypal\Block\Payment\Form\Billing\Agreement + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?php $code = $block->escapeHtml($block->getMethodCode()) ?> -<fieldset class="admin__fieldset payment-method form-list" - id="payment_form_<?= /* @noEscape */ $code ?>" style="display:none;"> +<fieldset class="admin__fieldset payment-method form-list" id="payment_form_<?= /* @noEscape */ $code ?>"> <div class="admin__field _required"> <label class="admin__field-label" for="<?= /* @noEscape */ $code ?>_ba_agreement_id"> @@ -19,7 +21,7 @@ name="payment[<?= $block->escapeHtml($block->getTransportName()) ?>]" class="required-entry admin__control-select"> <option value=""><?= $block->escapeHtml(__('Please Select')) ?></option> - <?php foreach ($block->getBillingAgreements() as $id => $referenceId) : ?> + <?php foreach ($block->getBillingAgreements() as $id => $referenceId): ?> <option value="<?= $block->escapeHtml($id) ?>" <?= ($id == $block->getInfoData($block->getTransportName())) ? ' selected="selected"' : ''; @@ -31,3 +33,7 @@ </div> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/api_wizard.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/api_wizard.phtml index f906a08425aa4..0268ab4f4c482 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/api_wizard.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/api_wizard.phtml @@ -7,11 +7,12 @@ /** * @see \Magento\Paypal\Block\Adminhtml\System\Config\ApiWizard * @var \Magento\Paypal\Block\Adminhtml\System\Config\ApiWizard $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="pp-buttons-container"> - <div dir="ltr" style="text-align: left;" trbidi="on"> - <script> + <div id="paypal_api_config" dir="ltr" trbidi="on"> + <?php $scriptString = <<<script (function(d, s, id){ var js, ref = d.getElementsByTagName(s)[0]; if (!d.getElementById(id)){ @@ -19,7 +20,9 @@ js.src = "https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js"; ref.parentNode.insertBefore(js, ref); } }(document, "script", "paypal-js")); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <a class="action-default" data-paypal-button="true" @@ -32,3 +35,4 @@ target="PPFrame"><?= $block->escapeHtml($block->getSandboxButtonLabel()) ?></a> </div> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("text-align: left;", 'div#paypal_api_config') ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/bml_api_wizard.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/bml_api_wizard.phtml index 72b7ac86ee056..040be8e3f4fa6 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/bml_api_wizard.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/bml_api_wizard.phtml @@ -7,16 +7,21 @@ /** * @see \Magento\Paypal\Block\Adminhtml\System\Config\BmlApiWizard * @var \Magento\Paypal\Block\Adminhtml\System\Config\BmlApiWizard $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="pp-buttons-container"> - <button onclick="javascript:window.open( - '<?= $block->escapeUrl($block->getButtonUrl()) ?>', - 'bmlapiwizard', - 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, ,' + - 'left=100, top=100, width=550, height=550' - ); return false;" - class="scalable" type="button" id="<?= $block->escapeHtml($block->getHtmlId()) ?>"> + <button class="scalable" type="button" id="<?= $block->escapeHtml($block->getHtmlId()) ?>"> <span><span><span><?= $block->escapeHtml($block->getButtonLabel()) ?></span></span></span> </button> </div> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.open( + '" . $block->escapeUrl($block->getButtonUrl()) . "', + 'bmlapiwizard', + 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, ,' + + 'left=100, top=100, width=550, height=550' + );event.preventDefault()", + 'button#' . $block->escapeHtml($block->getHtmlId()) +) ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/rules.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/rules.phtml index ee97d60aa72f8..cd52dd06f9bc1 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/rules.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/rules.phtml @@ -6,14 +6,18 @@ /** * @var \Magento\Paypal\Block\Adminhtml\System\Config\ResolutionRules $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ - ?> -<script> + +<?php $scriptString = <<<script require([ "Magento_Paypal/js/solutions", "domReady!" ], function (Solutions) { - var solutions = new Solutions({config: {solutions: <?= /* @noEscape */ $block->getJson() ?>}}); - }); -</script> +script; + +$scriptString .= 'var solutions = new Solutions({config: {solutions: ' . /* @noEscape */ $block->getJson() . '}}); + });'; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml index f4318b40fef1c..98e59f3a066c3 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Paypal\Block\Adminhtml\Payflowpro\CcForm $block */ +/** + * @var \Magento\Paypal\Block\Adminhtml\Payflowpro\CcForm $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $code = $block->escapeHtml($block->getMethodCode()); $ccType = $block->getInfoData('cc_type'); $ccExpYear = $block->getInfoData('cc_exp_year'); @@ -17,8 +20,11 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); allowtransparency="true" frameborder="0" name="iframeTransparent" - style="display: none; width: 100%; background-color: transparent;" src="<?= $block->escapeUrl($block->getViewFileUrl('blank.html')) ?>"></iframe> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display: none; width: 100%; background-color: transparent;", + "iframe#" . /* @noEscape */ $code . "-transparent-iframe" +) ?> <fieldset id="payment_form_<?= /* @noEscape */ $code ?>" class="admin__fieldset" @@ -31,9 +37,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); "orderSaveUrl":"<?= $block->escapeUrl($block->getOrderUrl()) ?>", "cgiUrl":"<?= $block->escapeUrl($block->getCgiUrl()) ?>", "expireYearLength":"<?= $block->escapeHtml($block->getMethodConfigData('cc_year_length')) ?>", - "nativeAction":"<?= $block->escapeUrl($block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()])) ?>" - }, "validation":[]}' - style="display: none;"> + "nativeAction":"<?= $block->escapeUrl( + $block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()]) + ) ?>" + }, "validation":[]}'> <div class="admin__field _required"> <label for="<?= /* @noEscape */ $code ?>_cc_type" class="admin__field-label"> <span><?= $block->escapeHtml(__('Credit Card Type')) ?></span> @@ -46,9 +53,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-validate='{required:true, "validate-cc-type-select":"#<?= /* @noEscape */ $code ?>_cc_number"}' class="admin__control-select"> <option value=""><?= $block->escapeHtml(__('Please Select')) ?></option> - <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName) : ?> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> <option - value="<?= $block->escapeHtml($typeCode) ?>"<?php if ($typeCode == $ccType) : ?> selected="selected"<?php endif ?>> + value="<?= $block->escapeHtml($typeCode) ?>" + <?php if ($typeCode == $ccType): ?> selected="selected"<?php endif ?>> <?= $block->escapeHtml($typeName) ?> </option> <?php endforeach ?> @@ -86,10 +94,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-container="<?= /* @noEscape */ $code ?>-cc-month" class="admin__control-select admin__control-select-month" data-validate='{required:true, "validate-cc-exp":"#<?= /* @noEscape */ $code ?>_expiration_yr"}'> - <?php foreach ($block->getCcMonths() as $k => $v) : ?> + <?php foreach ($block->getCcMonths() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpMonth) : ?> selected="selected"<?php endif; ?>> + <?php if ($k == $ccExpMonth): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> @@ -98,17 +106,17 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); <select id="<?= /* @noEscape */ $code ?>_expiration_yr" name="payment[cc_exp_year]" class="admin__control-select admin__control-select-year" data-container="<?= /* @noEscape */ $code ?>-cc-year" data-validate='{required:true}'> - <?php foreach ($block->getCcYears() as $k => $v) : ?> + <?php foreach ($block->getCcYears() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpYear) : ?> selected="selected"<?php endif ?>> + <?php if ($k == $ccExpYear): ?> selected="selected"<?php endif ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> </select> </div> </div> - <?php if ($block->hasVerification()) : ?> + <?php if ($block->hasVerification()): ?> <div class="admin__field _required field-cvv" id="<?= /* @noEscape */ $code ?>_cc_type_cvv_div"> <label for="<?= /* @noEscape */ $code ?>_cc_cid" class="admin__field-label"> <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> @@ -120,13 +128,13 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); class="admin__control-text cvv" id="<?= /* @noEscape */ $code ?>_cc_cid" name="payment[cc_cid]" value="" - data-validate='{"required-number":true, "validate-cc-cvn":"#<?= /* @noEscape */ $code ?>_cc_type"}' + data-validate='{"required-number":true, "validate-cc-cvn":"#<?=/* @noEscape */ $code?>_cc_type"}' autocomplete="off"/> </div> </div> <?php endif; ?> - <?php if ($block->isVaultEnabled()) : ?> + <?php if ($block->isVaultEnabled()): ?> <div class="admin__field admin__field-option field-tooltip-content"> <input type="checkbox" id="<?= /* @noEscape */ $code ?>_vault" @@ -142,12 +150,18 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); <?= $block->getChildHtml() ?> </fieldset> - -<script> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display: none;", + "fieldset#payment_form_" . /* @noEscape */ $code +) ?> +<?php $codeNoEscaped = /* @noEscape */ $code; +$scriptString = <<<script /** * Disable card server validation in admin */ require(["Magento_Sales/order/create/form"], function () { - order.addExcludedPaymentMethod('<?= /* @noEscape */ $code ?>'); + order.addExcludedPaymentMethod('{$codeNoEscaped}'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/iframe.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/iframe.phtml index 4edb109d6a4b9..8808bd08985f5 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/iframe.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/iframe.phtml @@ -6,22 +6,28 @@ /** * @var \Magento\Payment\Block\Transparent\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $params = $block->getParams(); +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <html> <head> -<script> -<?php if (isset($params['redirect'])) : ?> - window.location="<?= $block->escapeUrl($params['redirect']) ?>"; -<?php elseif (isset($params['redirect_parent'])) : ?> - window.top.location="<?= $block->escapeUrl($params['redirect_parent']) ?>"; -<?php elseif (isset($params['error_msg'])) : ?> - window.top.alert(<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($params['error_msg']) ?>); -<?php elseif (isset($params['order_success'])) : ?> - window.top.location = "<?= $block->escapeUrl($params['order_success']) ?>"; -<?php else : ?> +<?php +$scriptString = ''; +if (isset($params['redirect'])): + $scriptString .= 'window.location="' . $block->escapeJs($params['redirect']) . '";' . PHP_EOL; +elseif (isset($params['redirect_parent'])): + $scriptString .= 'window.top.location="' . $block->escapeJs($params['redirect_parent']) . '";' . PHP_EOL; +elseif (isset($params['error_msg'])): + $scriptString .= 'window.top.alert(' . /* @noEscape */ $jsonHelper->jsonEncode($params['error_msg']) . ');' . + PHP_EOL; +elseif (isset($params['order_success'])): + $scriptString .= 'window.top.location = "' . $block->escapeJs($params['order_success']) . '";' . PHP_EOL; +else: + $scriptString .= <<<script var require = window.top.require; require(['jquery'], function($) { var cc_number = $("input[name='payment[cc_number]']").val(); @@ -33,8 +39,10 @@ $params = $block->getParams(); $('#edit_form').trigger('realOrder'); }); -<?php endif; ?> -</script> +script; +endif; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </head> <body> </body> diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml index 8e222ca7eb04d..69c7c8179850a 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml @@ -6,12 +6,13 @@ /** * @var \Magento\Paypal\Block\Express\Review $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="paypal-review view"> <div class="block block-order-details-view"> <div class="block-content"> - <?php if ($block->getShippingAddress()) : ?> + <?php if ($block->getShippingAddress()): ?> <div class="box box-order-shipping-method"> <strong class="box-title"> <span><?= $block->escapeHtml(__('Shipping Method')) ?></span> @@ -20,17 +21,20 @@ <form method="post" id="shipping-method-form" action="<?= $block->escapeUrl($block->getShippingMethodSubmitUrl()) ?>" class="form"> - <?php if ($block->canEditShippingMethod()) : ?> - <?php if ($groups = $block->getShippingRateGroups()) : ?> + <?php if ($block->canEditShippingMethod()): ?> + <?php if ($groups = $block->getShippingRateGroups()): ?> <?php $currentRate = $block->getCurrentShippingRate(); ?> <div class="field shipping required"> <select name="shipping_method" id="shipping-method" class="select"> - <?php if (!$currentRate) : ?> - <option value=""><?= $block->escapeHtml(__('Please select a shipping method...')); ?></option> + <?php if (!$currentRate): ?> + <option value=""> + <?= $block->escapeHtml(__('Please select a shipping method...')); ?> + </option> <?php endif; ?> - <?php foreach ($groups as $code => $rates) : ?> - <optgroup label="<?= $block->escapeHtml($block->getCarrierName($code)); ?>"> - <?php foreach ($rates as $rate) : ?> + <?php foreach ($groups as $code => $rates): ?> + <optgroup label="<?= $block->escapeHtml($block->getCarrierName($code)); + ?>"> + <?php foreach ($rates as $rate): ?> <option value="<?= $block->escapeHtml( $block->renderShippingRateValue($rate) @@ -39,7 +43,8 @@ <?= ($currentRate === $rate) ? ' selected="selected"' : ''; ?>> - <?= /* @noEscape */ $block->renderShippingRateOption($rate); ?> + <?= /* @noEscape */ $block->renderShippingRateOption($rate); + ?> </option> <?php endforeach; ?> </optgroup> @@ -56,14 +61,14 @@ </button> </div> </div> - <?php else : ?> + <?php else: ?> <p> <?= $block->escapeHtml(__( 'Sorry, no quotes are available for this order right now.' )); ?> </p> <?php endif; ?> - <?php else : ?> + <?php else: ?> <p> <?= /* @noEscape */ $block->renderShippingRateOption( $block->getCurrentShippingRate() @@ -85,7 +90,7 @@ );?> </address> </div> - <?php if ($block->getCanEditShippingAddress()) : ?> + <?php if ($block->getCanEditShippingAddress()): ?> <div class="box-actions"> <a href="<?= $block->escapeUrl($block->getEditUrl()) ?>" class="action edit"> <span><?= $block->escapeHtml(__('Edit')) ?></span> @@ -102,7 +107,7 @@ <img src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-medium.png" alt="<?= $block->escapeHtml(__('Buy now with PayPal')) ?>"/> </div> - <?php if ($block->getEditUrl()) : ?> + <?php if ($block->getEditUrl()): ?> <div class="box-actions"> <a href="<?= $block->escapeUrl($block->getEditUrl()) ?>" class="action edit"> <span><?= $block->escapeHtml(__('Edit Payment Information')) ?></span> @@ -137,10 +142,11 @@ <span><?= $block->escapeHtml(__('Place Order')) ?></span> </button> </div> - <span class="please-wait load indicator" id="review-please-wait" style="display: none;" + <span class="please-wait load indicator" id="review-please-wait" data-text="<?= $block->escapeHtml(__('Submitting order information...')) ?>"> <span><?= $block->escapeHtml(__('Submitting order information...')) ?></span> </span> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'span#review-please-wait')?> </div> </form> </div> diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml index 839d278ed227c..826628c5cbc63 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml @@ -4,22 +4,25 @@ * See COPYING.txt for license details. */ -/** @var $block \Magento\Paypal\Block\Express\Review */ +/** + * @var $block \Magento\Paypal\Block\Express\Review + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div id="shipping-method-container"> - <?php if ($block->getCanEditShippingMethod() || !$block->getCurrentShippingRate()) : ?> - <?php if ($groups = $block->getShippingRateGroups()) : ?> + <?php if ($block->getCanEditShippingMethod() || !$block->getCurrentShippingRate()): ?> + <?php if ($groups = $block->getShippingRateGroups()): ?> <?php $currentRate = $block->getCurrentShippingRate(); ?> <select name="shipping_method" id="shipping_method" class="required-entry"> - <?php if (!$currentRate) : ?> + <?php if (!$currentRate): ?> <option value=""> <?= $block->escapeHtml(__('Please select a shipping method...')) ?> </option> <?php endif; ?> - <?php foreach ($groups as $code => $rates) : ?> - <optgroup label="<?= $block->escapeHtml($block->getCarrierName($code)) ?>" - style="font-style:normal;"> - <?php foreach ($rates as $rate) : ?> + <?php foreach ($groups as $code => $rates): ?> + <optgroup id="group_<?= /* @noEscape */ $code ?>" + label="<?= $block->escapeHtml($block->getCarrierName($code)) ?>"> + <?php foreach ($rates as $rate): ?> <option value="<?= $block->escapeHtml($block->renderShippingRateValue($rate)) ?>" <?= ($currentRate === $rate) ? ' selected="selected"' : '' ?>> @@ -27,16 +30,20 @@ </option> <?php endforeach; ?> </optgroup> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'font-style:normal;', + 'optgroup#group_' . /* @noEscape */ $code + ) ?> <?php endforeach; ?> </select> - <?php else : ?> + <?php else: ?> <p> <strong> <?= $block->escapeHtml(__('Sorry, no quotes are available for this order right now.')) ?> </strong> </p> <?php endif; ?> - <?php else : ?> + <?php else: ?> <p> <strong> <?= /* @noEscape */ $block->renderShippingRateOption($block->getCurrentShippingRate()) ?> @@ -44,6 +51,10 @@ </p> <?php endif; ?> </div> -<div style="display: none" id="shipping_method_update"> +<div id="shipping_method_update"> <p><?= $block->escapeHtml(__('Please update order data to get shipping methods and rates')) ?></p> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#shipping_method_update' +) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/hss/form.phtml b/app/code/Magento/Paypal/view/frontend/templates/hss/form.phtml index ec6f7b4ad985e..036ebb49d4eff 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/hss/form.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/hss/form.phtml @@ -8,6 +8,7 @@ /** * @var \Magento\Paypal\Block\Payflow\Link\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Paypal\Block\Payflow\Link\Iframe */ ?> @@ -20,8 +21,10 @@ <input type="hidden" name="SECURETOKENID" value="<?= $block->escapeHtml($block->getSecureTokenId()) ?>"/> <input type="hidden" name="MODE" value="<?= /* @noEscape */ $block->isTestMode() ? 'TEST' : 'LIVE' ?>"/> </form> -<script> +<?php $scriptString = <<<script document.getElementById('token_form').submit(); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </body> </html> diff --git a/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml b/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml index c2339f85b7ca5..d8bdf9b183b68 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml @@ -6,11 +6,15 @@ /** * @var \Magento\Paypal\Block\Payment\Info $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" - style="display:none" class="hss items"> +<div id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" class="hss items"> <?= $block->escapeHtml(__( 'You will be required to enter your payment details after you place an order.' )); ?> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/partner/logo.phtml b/app/code/Magento/Paypal/view/frontend/templates/partner/logo.phtml index f0f672492270a..63249f9a52455 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/partner/logo.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/partner/logo.phtml @@ -7,6 +7,7 @@ /** * @var \Magento\Paypal\Block\Logo $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Paypal\Block\Logo */ ?> @@ -14,11 +15,6 @@ <div class="block paypal acceptance"> <div class="block-content"> <a href="#" title="<?= $block->escapeHtml(__('Additional Options')) ?>" - onclick="javascript:window.open( - '<?= $block->escapeUrl($block->getAboutPaypalPageUrl()) ?>', - 'paypal', - 'width=600,height=350,left=0,top=0,location=no,status=yes,scrollbars=yes,resizable=yes' - ); return false;" class="action paypal additional"> <img src="<?= $block->escapeUrl($block->getLogoImageUrl()) ?>" alt="<?= $block->escapeHtml(__('Additional Options')) ?>" @@ -26,3 +22,12 @@ </a> </div> </div> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.open( + '" . $block->escapeJs($block->getAboutPaypalPageUrl()) . "', + 'paypal', + 'width=600,height=350,left=0,top=0,location=no,status=yes,scrollbars=yes,resizable=yes' + ); event.preventDefault();", + 'div.block.paypal.acceptance div.block-content a.action.paypal.additional' +) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/form.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/form.phtml index e643acac297e9..4491b8c09603e 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/form.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/form.phtml @@ -6,6 +6,7 @@ /** * @var \Magento\Paypal\Block\Payflow\Advanced\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <html> @@ -17,8 +18,10 @@ <input type="hidden" name="SECURETOKENID" value="<?= $block->escapeHtml($block->getSecureTokenId()) ?>"/> <input type="hidden" name="MODE" value="<?= /* @noEscape */ $block->isTestMode() ? 'TEST' : 'LIVE' ?>"/> </form> -<script> +<?php $scriptString = <<<script document.getElementById('token_form').submit(); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </body> </html> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml index d5944a6f22f5f..8e11186d43e6c 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml @@ -6,11 +6,16 @@ /** * @var \Magento\Paypal\Block\Payflow\Advanced\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none" +<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" class="fieldset payflowadvanced items redirect"> <div> <?= $block->escapeHtml(__('You will be required to enter your payment details after you place an order.')) ?> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'fieldset#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/form.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/form.phtml index cef3e2f0565ba..839ded13ae680 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/form.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/form.phtml @@ -8,6 +8,7 @@ /** * @var \Magento\Paypal\Block\Payflow\Link\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Paypal\Block\Payflow\Link\Iframe */ ?> @@ -20,8 +21,10 @@ <input type="hidden" name="SECURETOKENID" value="<?= $block->escapeHtml($block->getSecureTokenId()) ?>"/> <input type="hidden" name="MODE" value="<?= /* @noEscape */ $block->isTestMode() ? 'TEST' : 'LIVE' ?>"/> </form> -<script> +<?php $scriptString = <<<script document.getElementById('token_form').submit(); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </body> </html> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/info.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/info.phtml index cbd4a8ba715e7..3d17b24f53e61 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/info.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/info.phtml @@ -6,9 +6,13 @@ /** * @var \Magento\Paypal\Block\Payflow\Link\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div class="payflowlink items" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" - style="display:none"> +<div class="payflowlink items" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>"> <?= $block->escapeHtml(__('You will be required to enter your payment details after you place an order.')) ?> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/redirect.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/redirect.phtml index 75cc2a09e9444..35b678a8853b1 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/redirect.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/redirect.phtml @@ -8,13 +8,15 @@ /** * @var \Magento\Paypal\Block\Payflow\Link\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <html> <head> </head> <body> -<script> +<?php $scriptString= <<<script + (function() { 'use strict'; @@ -29,13 +31,13 @@ } } - var cartUrl = '<?= $block->escapeUrl($block->getUrl('checkout/cart')) ?>', - successUrl = '<?= $block->escapeUrl($block->getUrl('checkout/onepage/success')) ?>', - goToSuccessPage = '<?= $block->escapeUrl($block->getGotoSuccessPage()) ?>', + var cartUrl = '{$block->escapeJs($block->getUrl('checkout/cart'))}', + successUrl = '{$block->escapeJs($block->getUrl('checkout/onepage/success'))}', + goToSuccessPage = '{$block->escapeJs($block->getGotoSuccessPage())}', require = window.top.require, windowContext = window, errorMessage = { - message: '<?= $block->escapeHtml($block->getErrorMsg()) ?>' + message: '{$block->escapeJs($block->getErrorMsg())}' }; if(typeof(require) == "undefined") { @@ -49,8 +51,10 @@ }) } - })(); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </body> </html> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml b/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml index 75ee08111bd7a..85f627ad5509b 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml @@ -6,10 +6,11 @@ /** * @var \Magento\Paypal\Block\Payment\Form\Billing\Agreement $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $code = $block->escapeHtml($block->getMethodCode()); ?> -<div class="field items required" id="payment_form_<?= /* @noEscape */ $code ?>" style="display:none;"> +<div class="field items required" id="payment_form_<?= /* @noEscape */ $code ?>"> <label for="<?= /* @noEscape */ $code ?>_ba_agreement_id" class="label"> <span><?= $block->escapeHtml(__('Billing Agreement')) ?></span> </label> @@ -17,7 +18,7 @@ $code = $block->escapeHtml($block->getMethodCode()); <select id="<?= /* @noEscape */ $code ?>_ba_agreement_id" name="payment[<?= $block->escapeHtml($block->getTransportName()) ?>]" class="select"> <option value=""><?= $block->escapeHtml(__('-- Please Select Billing Agreement--')) ?></option> - <?php foreach ($block->getBillingAgreements() as $id => $referenceId) : ?> + <?php foreach ($block->getBillingAgreements() as $id => $referenceId): ?> <option value="<?= $block->escapeHtml($id) ?>"> <?= $block->escapeHtml($referenceId) ?> </option> @@ -25,3 +26,4 @@ $code = $block->escapeHtml($block->getMethodCode()); </select> </div> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'div#payment_form_' . /* @noEscape */ $code) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payment/mark.phtml b/app/code/Magento/Paypal/view/frontend/templates/payment/mark.phtml index d9fb5fb43bcc7..0b95e3788f91c 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payment/mark.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payment/mark.phtml @@ -7,6 +7,7 @@ /** * Note: This mark is a requirement of PayPal * @var \Magento\Paypal\Block\Express\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Paypal\Block\Express\Form */ $url = $block->escapeUrl($block->getPaymentAcceptanceMarkHref()); @@ -14,18 +15,21 @@ $url = $block->escapeUrl($block->getPaymentAcceptanceMarkHref()); <!-- PayPal Logo --> <img src="<?= $block->escapeUrl($block->getPaymentAcceptanceMarkSrc()) ?>" alt="<?= $block->escapeHtml(__('Acceptance Mark')) ?>" class="paypal icon"/> -<a href="<?= /* @noEscape */ $url ?>" - onclick="javascript:window.open( - '<?= /* @noEscape */ $url ?>', - 'olcwhatispaypal', - 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, ,' + - 'left=0, top=0, width=400, height=350' - ); return false;" - class="action paypal about"> +<a href="<?= /* @noEscape */ $url ?>" class="action paypal about"> <?php if ($block->getPaymentWhatIs()) { echo $block->escapeHtml(__($block->getPaymentWhatIs())); -} else { + } else { echo $block->escapeHtml(__('What is PayPal?')); -} ?> + } ?> </a> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.open( + '" . /* @noEscape */ $block->escapeJs($block->getPaymentAcceptanceMarkHref()) . "', + 'olcwhatispaypal', + 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, ,' + + 'left=0, top=0, width=400, height=350' + ); event.preventDefault();", + 'a.action.paypal.about' +) ?> <!-- PayPal Logo --> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payment/redirect.phtml b/app/code/Magento/Paypal/view/frontend/templates/payment/redirect.phtml index 683153b12db7a..a123f9b9ed7dc 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payment/redirect.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payment/redirect.phtml @@ -6,15 +6,15 @@ /** * @var \Magento\PayPal\Block\Express\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\PayPal\Block\Express\Form */ $code = $block->escapeHtml($block->getBillingAgreementCode()); ?> -<fieldset class="fieldset paypal items redirect" style="display:none;" - id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>"> +<fieldset class="fieldset paypal items redirect" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>"> <div><?= $block->escapeHtml($block->getRedirectMessage()) ?></div> <?php ?> - <?php if ($code) : ?> + <?php if ($code): ?> <input type="checkbox" id="<?= /* @noEscape */ $code ?>" value="1" class="checkbox" name="payment[<?= /* @noEscape */ $code ?>]"> <label for="<?= /* @noEscape */ $code ?>" class="label"> @@ -24,3 +24,7 @@ $code = $block->escapeHtml($block->getBillingAgreementCode()); </label> <?php endif; ?> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'fieldset#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/PaypalGraphQl/Model/PayflowProCcVaultAdditionalDataProvider.php b/app/code/Magento/PaypalGraphQl/Model/PayflowProCcVaultAdditionalDataProvider.php new file mode 100644 index 0000000000000..781cd8d0a9095 --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/PayflowProCcVaultAdditionalDataProvider.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model; + +use Magento\Framework\Stdlib\ArrayManager; +use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderInterface; + +/** + * Get payment additional data for Payflow pro cc vault payment + */ +class PayflowProCcVaultAdditionalDataProvider implements AdditionalDataProviderInterface +{ + const CC_VAULT_CODE = 'payflowpro_cc_vault'; + + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @param ArrayManager $arrayManager + */ + public function __construct( + ArrayManager $arrayManager + ) { + $this->arrayManager = $arrayManager; + } + + /** + * Returns additional data + * + * @param array $args + * @return array + */ + public function getData(array $args): array + { + if (isset($args[self::CC_VAULT_CODE])) { + return $args[self::CC_VAULT_CODE]; + } + return []; + } +} diff --git a/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowPro/SetPaymentMethodOnCart.php b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowPro/SetPaymentMethodOnCart.php new file mode 100644 index 0000000000000..7ca4d41cfd33a --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowPro/SetPaymentMethodOnCart.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Plugin\Cart\PayflowPro; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Paypal\Model\Config; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderPool; +use Magento\Sales\Model\Order\Payment\Repository as PaymentRepository; +use Magento\PaypalGraphQl\Observer\PayflowProSetCcData; + +/** + * Set additionalInformation on payment for PayflowPro method + */ +class SetPaymentMethodOnCart +{ + /** + * @var PaymentRepository + */ + private $paymentRepository; + + /** + * @var AdditionalDataProviderPool + */ + private $additionalDataProviderPool; + + /** + * @param PaymentRepository $paymentRepository + * @param AdditionalDataProviderPool $additionalDataProviderPool + */ + public function __construct( + PaymentRepository $paymentRepository, + AdditionalDataProviderPool $additionalDataProviderPool + ) { + $this->paymentRepository = $paymentRepository; + $this->additionalDataProviderPool = $additionalDataProviderPool; + } + + /** + * Set redirect URL paths on payment additionalInformation + * + * @param \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject + * @param mixed $result + * @param Quote $cart + * @param array $paymentData + * @return void + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute( + \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject, + $result, + Quote $cart, + array $paymentData + ): void { + $paymentData = $this->additionalDataProviderPool->getData(Config::METHOD_PAYFLOWPRO, $paymentData); + $cartCustomerId = (int)$cart->getCustomerId(); + if ($cartCustomerId === 0 && + array_key_exists(PayflowProSetCcData::IS_ACTIVE_PAYMENT_TOKEN_ENABLER, $paymentData)) { + $payment = $cart->getPayment(); + $payment->unsAdditionalInformation(PayflowProSetCcData::IS_ACTIVE_PAYMENT_TOKEN_ENABLER); + $payment->save(); + } + } +} diff --git a/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowProCcVault/SetPaymentMethodOnCart.php b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowProCcVault/SetPaymentMethodOnCart.php new file mode 100644 index 0000000000000..46bad75f0ed19 --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowProCcVault/SetPaymentMethodOnCart.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Plugin\Cart\PayflowProCcVault; + +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderPool; +use Magento\Sales\Model\Order\Payment\Repository as PaymentRepository; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Api\PaymentTokenManagementInterface; + +/** + * Set additionalInformation on payment for PayflowPro vault method + */ +class SetPaymentMethodOnCart +{ + const CC_VAULT_CODE = 'payflowpro_cc_vault'; + + /** + * @var PaymentRepository + */ + private $paymentRepository; + + /** + * @var AdditionalDataProviderPool + */ + private $additionalDataProviderPool; + + /** + * PaymentTokenManagementInterface $paymentTokenManagement + */ + private $paymentTokenManagement; + + /** + * @param PaymentRepository $paymentRepository + * @param AdditionalDataProviderPool $additionalDataProviderPool + * @param PaymentTokenManagementInterface $paymentTokenManagement + */ + public function __construct( + PaymentRepository $paymentRepository, + AdditionalDataProviderPool $additionalDataProviderPool, + PaymentTokenManagementInterface $paymentTokenManagement + ) { + $this->paymentRepository = $paymentRepository; + $this->additionalDataProviderPool = $additionalDataProviderPool; + $this->paymentTokenManagement = $paymentTokenManagement; + } + + /** + * Set public hash and customer id on payment additionalInformation + * + * @param \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject + * @param mixed $result + * @param Quote $cart + * @param array $additionalData + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function afterExecute( + \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject, + $result, + Quote $cart, + array $additionalData + ): void { + $additionalData = $this->additionalDataProviderPool->getData(self::CC_VAULT_CODE, $additionalData); + $customerId = (int) $cart->getCustomer()->getId(); + $payment = $cart->getPayment(); + if (!is_array($additionalData) + || !isset($additionalData[PaymentTokenInterface::PUBLIC_HASH]) + || $customerId === 0 + ) { + return; + } + $tokenPublicHash = $additionalData[PaymentTokenInterface::PUBLIC_HASH]; + if ($tokenPublicHash === null) { + return; + } + $paymentToken = $this->paymentTokenManagement->getByPublicHash($tokenPublicHash, $customerId); + if ($paymentToken === null) { + return; + } + $payment->setAdditionalInformation( + [ + PaymentTokenInterface::CUSTOMER_ID => $customerId, + PaymentTokenInterface::PUBLIC_HASH => $tokenPublicHash + ] + ); + $payment->save(); + } +} diff --git a/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProResponse.php b/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProResponse.php index ce44511c60f3e..b3ddced97aca6 100644 --- a/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProResponse.php +++ b/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProResponse.php @@ -126,9 +126,9 @@ public function resolve( $this->parameters->fromString(urldecode($paypalPayload)); $data = $this->parameters->toArray(); try { - $do = $this->dataObjectFactory->create(['data' => array_change_key_case($data, CASE_LOWER)]); - $this->responseValidator->validate($do, $this->transparent); - $this->transaction->savePaymentInQuote($do, $cart->getId()); + $response = $this->transaction->getResponseObject($data); + $this->responseValidator->validate($response, $this->transparent); + $this->transaction->savePaymentInQuote($response, $cart->getId()); } catch (LocalizedException $exception) { $parameters['error'] = true; $parameters['error_msg'] = $exception->getMessage(); diff --git a/app/code/Magento/PaypalGraphQl/Observer/PayflowProSetCcData.php b/app/code/Magento/PaypalGraphQl/Observer/PayflowProSetCcData.php new file mode 100644 index 0000000000000..55310b1744107 --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Observer/PayflowProSetCcData.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Observer; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Payment\Observer\AbstractDataAssignObserver; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Quote\Model\Quote\Payment; + +/** + * Class PayflowProSetCcData set CcData to quote payment + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class PayflowProSetCcData extends AbstractDataAssignObserver +{ + const XML_PATH_PAYMENT_PAYFLOWPRO_CC_VAULT_ACTIVE = "payment/payflowpro_cc_vault/active"; + const IS_ACTIVE_PAYMENT_TOKEN_ENABLER = "is_active_payment_token_enabler"; + + /** + * Core store config + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Set CcData + * + * @param Observer $observer + * + * @throws GraphQlInputException + */ + public function execute(Observer $observer) + { + $dataObject = $this->readDataArgument($observer); + $additionalData = $dataObject->getData(PaymentInterface::KEY_ADDITIONAL_DATA); + /** + * @var Payment $paymentModel + */ + $paymentModel = $this->readPaymentModelArgument($observer); + $customerId = (int)$paymentModel->getQuote()->getCustomer()->getId(); + + if (!isset($additionalData['cc_details'])) { + return; + } + + if ($this->isPayflowProVaultEnable() && $customerId !== 0) { + if (isset($additionalData[self::IS_ACTIVE_PAYMENT_TOKEN_ENABLER])) { + $paymentModel->setData( + self::IS_ACTIVE_PAYMENT_TOKEN_ENABLER, + $additionalData[self::IS_ACTIVE_PAYMENT_TOKEN_ENABLER] + ); + } + } else { + $paymentModel->setData(self::IS_ACTIVE_PAYMENT_TOKEN_ENABLER, false); + } + + $ccData = $additionalData['cc_details']; + $paymentModel->setCcType($ccData['cc_type']); + $paymentModel->setCcExpYear($ccData['cc_exp_year']); + $paymentModel->setCcExpMonth($ccData['cc_exp_month']); + $paymentModel->setCcLast4($ccData['cc_last_4']); + } + + /** + * Check if payflowpro vault is enable + * + * @return bool + */ + private function isPayflowProVaultEnable() + { + return (bool)$this->scopeConfig->getValue(self::XML_PATH_PAYMENT_PAYFLOWPRO_CC_VAULT_ACTIVE); + } +} diff --git a/app/code/Magento/PaypalGraphQl/composer.json b/app/code/Magento/PaypalGraphQl/composer.json index 8d012be3492dd..285217da64d72 100644 --- a/app/code/Magento/PaypalGraphQl/composer.json +++ b/app/code/Magento/PaypalGraphQl/composer.json @@ -13,10 +13,12 @@ "magento/module-quote-graph-ql": "*", "magento/module-sales": "*", "magento/module-payment": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-vault": "*" }, "suggest": { - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "*", + "magento/module-store-graph-ql": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml b/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml index cd5d6e2062bb9..f5f22050fe50a 100644 --- a/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml @@ -11,6 +11,8 @@ </type> <type name="Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart"> <plugin name="hosted_pro_payment_method" type="Magento\PaypalGraphQl\Model\Plugin\Cart\HostedPro\SetPaymentMethodOnCart"/> + <plugin name="payflowpro_payment_method" type="Magento\PaypalGraphQl\Model\Plugin\Cart\PayflowPro\SetPaymentMethodOnCart"/> + <plugin name="payflowpro_cc_vault_payment_method" type="Magento\PaypalGraphQl\Model\Plugin\Cart\PayflowProCcVault\SetPaymentMethodOnCart"/> </type> <type name="Magento\Paypal\Model\Payflowlink"> <plugin name="payflow_link_update_redirect_urls" type="Magento\PaypalGraphQl\Model\Plugin\Payflowlink"/> @@ -50,6 +52,15 @@ <item name="payflow_advanced" xsi:type="object">Magento\PaypalGraphQl\Model\PayflowLinkAdditionalDataProvider</item> <item name="payflowpro" xsi:type="object">\Magento\PaypalGraphQl\Model\PayflowProAdditionalDataProvider</item> <item name="hosted_pro" xsi:type="object">\Magento\PaypalGraphQl\Model\HostedProAdditionalDataProvider</item> + <item name="payflowpro_cc_vault" xsi:type="object">\Magento\PaypalGraphQl\Model\PayflowProCcVaultAdditionalDataProvider</item> + </argument> + </arguments> + </type> + + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="payment_payflowpro_cc_vault_active" xsi:type="string">payment/payflowpro_cc_vault/active</item> </argument> </arguments> </type> diff --git a/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml b/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml index 41154e5ae06e6..0d2be95d77c92 100644 --- a/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml +++ b/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml @@ -12,4 +12,7 @@ <event name="payment_method_assign_data_payflow_advanced"> <observer name="payflow_advanced_data_assigner" instance="Magento\PaypalGraphQl\Observer\PayflowLinkSetAdditionalData"/> </event> + <event name="payment_method_assign_data_payflowpro"> + <observer name="payflowpro_cc_data_assigner" instance="Magento\PaypalGraphQl\Observer\PayflowProSetCcData" /> + </event> </config> diff --git a/app/code/Magento/PaypalGraphQl/etc/schema.graphqls b/app/code/Magento/PaypalGraphQl/etc/schema.graphqls index b8f14eec70d18..cdc8ee6fda2f3 100644 --- a/app/code/Magento/PaypalGraphQl/etc/schema.graphqls +++ b/app/code/Magento/PaypalGraphQl/etc/schema.graphqls @@ -37,7 +37,7 @@ type PayflowLinkToken @doc(description:"Contains information used to generate Pa paypal_url: String @doc(description:"PayPal URL used for requesting Payflow form") } -type HostedProUrl @doc(desription:"Contains secure URL used for Payments Pro Hosted Solution payment method.") { +type HostedProUrl @doc(description:"Contains secure URL used for Payments Pro Hosted Solution payment method.") { secure_form_url: String @doc(description:"Secure Url generated by PayPal") } @@ -51,6 +51,7 @@ input PaymentMethodInput { payflow_link: PayflowLinkInput @doc(description:"Required input for PayPal Payflow Link and Payments Advanced payments") payflowpro: PayflowProInput @doc(description: "Required input type for PayPal Payflow Pro and Payment Pro payments") hosted_pro: HostedProInput @doc(description:"Required input for PayPal Hosted pro payments") + payflowpro_cc_vault: VaultTokenInput @doc(description:"Required input type for PayPal Payflow Pro vault payments") } input HostedProInput @doc(description:"A set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is https://www.example.com/path/to/page.html, the relative URL is path/to/page.html. Use this input for Payments Pro Hosted Solution payment method.") { @@ -102,6 +103,7 @@ input PayflowProTokenInput @doc(description:"Input required to fetch payment tok input PayflowProInput @doc(description:"Required input for Payflow Pro and Payments Pro payment methods.") { cc_details: CreditCardDetailsInput! @doc(description: "Required input for credit card related information") + is_active_payment_token_enabler: Boolean @doc(description:"States whether details about the customer's credit/debit card should be tokenized for later usage. Required only if Vault is enabled for PayPal Payflow Pro payment integration.") } input CreditCardDetailsInput @doc(description:"Required fields for Payflow Pro and Payments Pro credit card payments") { @@ -141,3 +143,11 @@ input PayflowProResponseInput @doc(description:"Input required to complete payme type PayflowProResponseOutput { cart: Cart! } + +type StoreConfig { + payment_payflowpro_cc_vault_active: String @doc(description: "Payflow Pro vault status.") +} + +input VaultTokenInput @doc(description:"Required input for payment methods with Vault support.") { + public_hash: String! @doc(description: "The public hash of the payment token") +} 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/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php index 036f17fb3c1b2..cf3d92fe985fc 100644 --- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php +++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php @@ -139,9 +139,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) !$this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn() && $this->_checkoutSession->getQuoteId() && - !$this->isRequestFromCheckoutPage($this->request) && // persistent session does not expire on onepage checkout page - $this->isNeedToExpireSession() + !$this->isRequestFromCheckoutPage($this->request) && + $this->getQuote()->getIsPersistent() ) { $this->_eventManager->dispatch('persistent_session_expired'); $this->quoteManager->expire(); @@ -168,18 +168,6 @@ private function isPersistentQuoteOutdated(): bool return false; } - /** - * Condition checker - * - * @return bool - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - private function isNeedToExpireSession(): bool - { - return $this->getQuote()->getIsPersistent() || $this->getQuote()->getCustomerIsGuest(); - } - /** * Getter for Quote with micro optimization * diff --git a/app/code/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserver.php b/app/code/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserver.php index 52a2912c4b170..f0b05cb7850cc 100644 --- a/app/code/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserver.php +++ b/app/code/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserver.php @@ -11,6 +11,9 @@ /** * Persistent Session Observer + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class SynchronizePersistentOnLoginObserver implements ObserverInterface { @@ -63,6 +66,8 @@ public function __construct( } /** + * Synchronize persistent session data with logged in customer + * * @param Observer $observer * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -96,8 +101,9 @@ public function execute(Observer $observer) if (!$sessionModel->getId()) { /** @var \Magento\Persistent\Model\Session $sessionModel */ $sessionModel = $this->_sessionFactory->create(); - $sessionModel->setCustomerId($customer->getId())->save(); + $sessionModel->setCustomerId($customer->getId()); } + $sessionModel->save(); $this->_persistentSession->setSession($sessionModel); } 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/ShippingQuotePersistedForGuestTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml index 7c4e6948386f3..f094c4f07475d 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml @@ -66,7 +66,7 @@ <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="selectCaliforniaRegion"/> <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{US_Address_CA.postcode}}" stepKey="inputPostCode"/> <!--Step 6: Go to Homepage--> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePageAfterChangingShippingAndTaxSection"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePageAfterChangingShippingAndTaxSection"/> <!--Step 7: Go to shopping cart and check "Estimate Shipping and Tax" fields values are saved--> <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" after="goToHomePageAfterChangingShippingAndTaxSection" stepKey="goToShoppingCartAfterChangingShippingAndTaxSection"/> <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTaxAfterChanging" /> 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 dd24c6ae4279d..159b5b6b9e79b 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml @@ -49,7 +49,7 @@ </after> <!-- 1. Go to storefront and click the Create an Account link--> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnHomePage"/> <click selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}" stepKey="clickCreateAnAccountLink" /> <actionGroup ref="StorefrontAssertPersistentRegistrationPageFields" stepKey="assertPersistentRegistrationPageFields"/> @@ -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> @@ -131,7 +131,7 @@ <see selector="{{StorefrontMinicartSection.productCount}}" userInput="2" stepKey="miniCartContainsTwoProductForGuest"/> <!-- 10. Go to My Account section --> - <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="amOnCustomerAccountPage"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="amOnCustomerAccountPage"/> <seeInCurrentUrl url="{{StorefrontCustomerSignInPage.url}}" stepKey="redirectToCustomerAccountLoginPage"/> <seeElement selector="{{StorefrontCustomerSignInFormSection.customerLoginBlock}}" stepKey="checkSystemRequiresToLogIn"/> @@ -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/Test/Unit/Model/QuoteManagerTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php index 5c4a3eb624d3c..0c183084edca2 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php @@ -228,13 +228,9 @@ public function testSetGuest() ->method('removePersistentCookie')->willReturn($this->sessionMock); $this->quoteMock->expects($this->once())->method('isVirtual')->willReturn(false); $this->quoteMock->expects($this->once())->method('getItemsQty')->willReturn(1); - $extensionAttributes = $this->createPartialMock( - CartExtensionInterface::class, - [ - 'setShippingAssignments', - 'getShippingAssignments' - ] - ); + $extensionAttributes = $this->getMockBuilder(CartExtensionInterface::class) + ->addMethods(['getShippingAssignments', 'setShippingAssignments']) + ->getMockForAbstractClass(); $shippingAssignment = $this->createMock(ShippingAssignmentInterface::class); $extensionAttributes->expects($this->once()) ->method('setShippingAssignments') 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/ProductAlert/Model/Email.php b/app/code/Magento/ProductAlert/Model/Email.php index 3351166aa6a12..379ae29ef4649 100644 --- a/app/code/Magento/ProductAlert/Model/Email.php +++ b/app/code/Magento/ProductAlert/Model/Email.php @@ -1,9 +1,10 @@ <?php - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ProductAlert\Model; use Magento\Catalog\Model\Product; @@ -40,7 +41,7 @@ * @api * @since 100.0.2 * @method int getStoreId() - * @method $this setStoreId() + * @method $this setStoreId(int $storeId) */ class Email extends AbstractModel { @@ -206,7 +207,7 @@ public function getType() * * @return $this */ - public function setWebsite(\Magento\Store\Model\Website $website) + public function setWebsite(Website $website) { $this->_website = $website; return $this; @@ -275,7 +276,7 @@ public function clean() * * @return $this */ - public function addPriceProduct(\Magento\Catalog\Model\Product $product) + public function addPriceProduct(Product $product) { $this->_priceProducts[$product->getId()] = $product; return $this; @@ -288,7 +289,7 @@ public function addPriceProduct(\Magento\Catalog\Model\Product $product) * * @return $this */ - public function addStockProduct(\Magento\Catalog\Model\Product $product) + public function addStockProduct(Product $product) { $this->_stockProducts[$product->getId()] = $product; return $this; @@ -342,7 +343,7 @@ public function send() return false; } - $storeId = $this->getStoreId() ?: (int) $this->_customer->getStoreId(); + $storeId = (int) $this->getStoreId() ?: (int) $this->_customer->getStoreId(); $store = $this->getStore($storeId); $this->_appEmulation->startEnvironmentEmulation($storeId); @@ -378,12 +379,13 @@ public function send() 'customerName' => $customerName, 'alertGrid' => $alertGrid, ] - )->setFrom( + )->setFromByScope( $this->_scopeConfig->getValue( self::XML_PATH_EMAIL_IDENTITY, ScopeInterface::SCOPE_STORE, $storeId - ) + ), + $storeId )->addTo( $this->_customer->getEmail(), $customerName diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml index 3032f5208dd59..cc2c933812352 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml @@ -37,8 +37,7 @@ </before> <!--Open simple product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> <argument name="product" value="$$createProduct$$"/> @@ -51,8 +50,7 @@ <actionGroup ref="AssertProductVideoAdminProductPageActionGroup" stepKey="assertProductVideoAdminProductPage" after="addProductVideo"/> <!-- Save the product --> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveFirstProduct"/> - <waitForPageLoad stepKey="waitForFirstProductSaved"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveFirstProduct"/> <!-- Assert product video in storefront product page --> <amOnPage url="$$createProduct.name$$.html" stepKey="goToStorefrontCategoryPage"/> diff --git a/app/code/Magento/ProductVideo/Test/Unit/Model/Product/Attribute/Media/ExternalVideoEntryConverterTest.php b/app/code/Magento/ProductVideo/Test/Unit/Model/Product/Attribute/Media/ExternalVideoEntryConverterTest.php index 3ca043e205e87..a921bff76c8d6 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Model/Product/Attribute/Media/ExternalVideoEntryConverterTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Model/Product/Attribute/Media/ExternalVideoEntryConverterTest.php @@ -1,4 +1,5 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -26,34 +27,26 @@ */ class ExternalVideoEntryConverterTest extends TestCase { - /** - * @var MockObject|ProductAttributeMediaGalleryEntryInterfaceFactory - */ - protected $mediaGalleryEntryFactoryMock; + /** @var MockObject|ProductAttributeMediaGalleryEntryInterfaceFactory */ + private $mediaGalleryEntryFactoryMock; - /** - * @var MockObject|ProductAttributeMediaGalleryEntryInterface - */ - protected $mediaGalleryEntryMock; + /** @var MockObject|ProductAttributeMediaGalleryEntryInterface */ + private $mediaGalleryEntryMock; /** @var MockObject|DataObjectHelper */ - protected $dataObjectHelperMock; + private $dataObjectHelperMock; /** @var MockObject|VideoContentInterfaceFactory */ - protected $videoEntryFactoryMock; + private $videoEntryFactoryMock; /** @var MockObject|VideoContentInterface */ - protected $videoEntryMock; + private $videoEntryMock; - /** - * @var MockObject|ProductAttributeMediaGalleryEntryExtensionFactory - */ - protected $mediaGalleryEntryExtensionFactoryMock; + /** @var MockObject|ProductAttributeMediaGalleryEntryExtensionFactory */ + private $mediaGalleryEntryExtensionFactoryMock; - /** - * @var MockObject|ProductAttributeMediaGalleryEntryExtensionFactory - */ - protected $mediaGalleryEntryExtensionMock; + /** @var MockObject|ProductAttributeMediaGalleryEntryExtension */ + private $mediaGalleryEntryExtensionMock; /** * @var ObjectManager|ExternalVideoEntryConverter @@ -62,33 +55,35 @@ class ExternalVideoEntryConverterTest extends TestCase protected function setUp(): void { - $this->mediaGalleryEntryFactoryMock = - $this->createPartialMock( - ProductAttributeMediaGalleryEntryInterfaceFactory::class, - ['create'] - ); + $this->mediaGalleryEntryFactoryMock = $this->createPartialMock( + ProductAttributeMediaGalleryEntryInterfaceFactory::class, + ['create'] + ); $this->mediaGalleryEntryMock = - $this->createPartialMock(ProductAttributeMediaGalleryEntryInterface::class, [ - 'getId', - 'setId', - 'getMediaType', - 'setMediaType', - 'getLabel', - 'setLabel', - 'getPosition', - 'setPosition', - 'isDisabled', - 'setDisabled', - 'getTypes', - 'setTypes', - 'getFile', - 'setFile', - 'getContent', - 'setContent', - 'getExtensionAttributes', - 'setExtensionAttributes' - ]); + $this->createPartialMock( + ProductAttributeMediaGalleryEntryInterface::class, + [ + 'getId', + 'setId', + 'getMediaType', + 'setMediaType', + 'getLabel', + 'setLabel', + 'getPosition', + 'setPosition', + 'isDisabled', + 'setDisabled', + 'getTypes', + 'setTypes', + 'getFile', + 'setFile', + 'getContent', + 'setContent', + 'getExtensionAttributes', + 'setExtensionAttributes' + ] + ); $this->mediaGalleryEntryFactoryMock->expects($this->any())->method('create')->willReturn( $this->mediaGalleryEntryMock @@ -110,7 +105,8 @@ protected function setUp(): void ); $this->mediaGalleryEntryExtensionMock = $this->getMockBuilder(ProductAttributeMediaGalleryEntryExtension::class) - ->addMethods(['getVideoProvider', 'setVideoContent', 'getVideoContent']) + ->addMethods(['getVideoProvider']) + ->onlyMethods(['setVideoContent', 'getVideoContent']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/ProductVideo/etc/csp_whitelist.xml b/app/code/Magento/ProductVideo/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..ca4536057104d --- /dev/null +++ b/app/code/Magento/ProductVideo/etc/csp_whitelist.xml @@ -0,0 +1,25 @@ +<?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="script-src"> + <values> + <value id="youtube_cdn" type="host">s.ytimg.com</value> + <value id="google_video" type="host">www.googleapis.com</value> + <value id="vimeo" type="host">vimeo.com</value> + <value id="www_vimeo" type="host">www.vimeo.com</value> + </values> + </policy> + <policy id="img-src"> + <values> + <value id="vimeo_cdn" type="host">*.vimeocdn.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml index 1548770d4032f..b75b59eeacce2 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Files.LineLength -// phpcs:disable Magento2.Templates.ThisInTemplate - -/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content */ +/** + * @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $elementNameEscaped = $block->escapeHtmlAttr($block->getElement()->getName()) . '[images]'; $formNameEscaped = $block->escapeHtmlAttr($block->getFormName()); + +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <div class="row"> @@ -28,16 +31,17 @@ $formNameEscaped = $block->escapeHtmlAttr($block->getFormName()); <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content */ $element = $block->getElement(); -$elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'toggleValueElements(this, this.parentNode.parentNode.parentNode)'; +$elementToggleCode = $element->getToggleCode() ? $element->getToggleCode(): + 'toggleValueElements(this, this.parentNode.parentNode.parentNode)'; ?> <div id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>" class="gallery" data-mage-init='{"openVideoModal":{}}' data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" data-images="<?= $block->escapeHtmlAttr($block->getImagesJson()) ?>" - data-types='<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes()) ?>' + data-types='<?= /* @noEscape */ $jsonHelper->jsonEncode($block->getImageTypes()) ?>' > - <?php if (!$block->getElement()->getReadonly()) : ?> + <?php if (!$block->getElement()->getReadonly()): ?> <div class="image image-placeholder"> <?= $block->getUploaderHtml(); ?> <div class="product-image-wrapper"> @@ -48,15 +52,17 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </div> <?= $block->getChildHtml('additional_buttons') ?> <?php endif; ?> - <?php foreach ($block->getImageTypes() as $typeData) : ?> + <?php foreach ($block->getImageTypes() as $typeData): ?> <input name="<?= $block->escapeHtmlAttr($typeData['name']) ?>" data-form-part="<?= /* @noEscape */ $formNameEscaped ?>" class="image-<?= $block->escapeHtmlAttr($typeData['code']) ?>" type="hidden" value="<?= $block->escapeHtmlAttr($typeData['value']) ?>"/> <?php endforeach; ?> - <script id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>-template" data-template="image" type="text/x-magento-template"> - <div class="image item <% if (data.disabled == 1) { %>hidden-for-front<% } %> <% if (data.video_url) { %>video-item<% } %>" + <script id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>-template" data-template="image" + type="text/x-magento-template"> + <div class="image item <% if (data.disabled == 1) { %>hidden-for-front<% } %> + <% if (data.video_url) { %>video-item<% } %>" data-role="image"> <input type="hidden" name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][position]" @@ -164,8 +170,9 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </div> <ul class="item-roles" data-role="roles-labels"> - <?php foreach ($block->getImageTypes() as $typeData) : ?> - <li data-role-code="<?= $block->escapeHtmlAttr($typeData['code']) ?>" class="item-role item-role-<?= $block->escapeHtmlAttr($typeData['code']) ?>"> + <?php foreach ($block->getImageTypes() as $typeData): ?> + <li data-role-code="<?= $block->escapeHtmlAttr($typeData['code']) ?>" + class="item-role item-role-<?= $block->escapeHtmlAttr($typeData['code']) ?>"> <?= $block->escapeHtml($typeData['label']) ?> </li> <?php endforeach; ?> @@ -195,7 +202,8 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to <textarea data-role="image-description" rows="3" class="admin__control-textarea" - name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> + name="<?= /* @noEscape */ $elementNameEscaped + ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> </div> </div> @@ -206,7 +214,7 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to <div class="admin__field-control"> <ul class="multiselect-alt"> <?php - foreach ($block->getMediaAttributes() as $attribute) : + foreach ($block->getMediaAttributes() as $attribute): ?> <li class="item"> <label> @@ -235,7 +243,8 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to <label class="admin__field-label"> <span><?= $block->escapeHtml(__('Image Resolution')) ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) ?>"></div> + <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) + ?>"></div> </div> <div class="admin__field field-image-hide"> @@ -259,7 +268,8 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </fieldset> </div> </script> - <div id="<?= /* @noEscape */ $block->getNewVideoBlockName() ?>" style="display:none"> + <?php $videoBlockId = "new_video_" . $block->getHtmlId() . rand(); ?> + <div id="<?= /* @noEscape */ $videoBlockId ?>"> <?= $block->getFormHtml() ?> <div id="video-player-preview-location" class="video-player-sidebar"> <div class="video-player-container"></div> @@ -277,9 +287,11 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </div> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#' . $videoBlockId + ) ?> <?= $block->getChildHtml('new-video') ?> </div> -<script> - jQuery('body').trigger('contentUpdated'); -</script> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], "jQuery('body').trigger('contentUpdated');", false) ?> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml index e1dcab9e8b2d4..8c40c174c9787 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="row"> <div class="add-video-button-container"> @@ -11,10 +13,14 @@ title="<?= $block->escapeHtmlAttr($addVideoTitle) ?>" type="button" class="action-secondary" - onclick="jQuery('#new-video').modal('openModal'); jQuery('#new_video_form')[0].reset();" data-ui-id="widget-button-1"> <span><?= $block->escapeHtml(__('Add Video')) ?></span> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "jQuery('#new-video').modal('openModal'); jQuery('#new_video_form')[0].reset();", + 'button#add_video_button' + ) ?> </div> </div> <div id="<?= $block->escapeHtmlAttr($htmlId) ?>-container" @@ -62,7 +68,7 @@ <span class="action-manage-images" data-activate-tab="image-management"> <span><?= $block->escapeHtml($imageManagementText) ?></span> </span> -<script> +<?php $scriptString = <<<script require([ 'jquery' ],function($){ @@ -74,4 +80,6 @@ $('#product_info_tabs_image-management').trigger('click'); }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml index 7de3042b56ab5..bf46bd1411e84 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -/* @var Magento\ProductVideo\Block\Adminhtml\Product\Edit\NewVideo $block */ +/** + * @var Magento\ProductVideo\Block\Adminhtml\Product\Edit\NewVideo $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" style="display:none" +<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" data-modal-info='<?= /* @noEscape */ $block->getWidgetOptions() ?>' > <?= $block->getFormHtml() ?> @@ -25,3 +28,7 @@ </div> </div> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#' . $block->escapeJs($block->getNameInLayout()) +) ?> diff --git a/app/code/Magento/Quote/Model/BillingAddressManagement.php b/app/code/Magento/Quote/Model/BillingAddressManagement.php index bc055e71c662e..6f8a44dff464c 100644 --- a/app/code/Magento/Quote/Model/BillingAddressManagement.php +++ b/app/code/Magento/Quote/Model/BillingAddressManagement.php @@ -103,7 +103,7 @@ public function get($cartId) * Get shipping address assignment * * @return \Magento\Quote\Model\ShippingAddressAssignment - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getShippingAddressAssignment() { diff --git a/app/code/Magento/Quote/Model/Cart/AddProductsToCart.php b/app/code/Magento/Quote/Model/Cart/AddProductsToCart.php new file mode 100644 index 0000000000000..2c5c3536d6682 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/AddProductsToCart.php @@ -0,0 +1,225 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder; +use Magento\Quote\Model\Cart\Data\AddProductsToCartOutput; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\Quote\Model\Quote; +use Magento\Framework\Message\MessageInterface; + +/** + * Unified approach to add products to the Shopping Cart. + * Client code must validate, that customer is eligible to call service with provided {cartId} and {cartItems} + */ +class AddProductsToCart +{ + /**#@+ + * Error message codes + */ + private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'; + private const ERROR_INSUFFICIENT_STOCK = 'INSUFFICIENT_STOCK'; + private const ERROR_NOT_SALABLE = 'NOT_SALABLE'; + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * List of error messages and codes. + */ + private const MESSAGE_CODES = [ + 'Could not find a product with SKU' => self::ERROR_PRODUCT_NOT_FOUND, + 'The required options you selected are not available' => self::ERROR_NOT_SALABLE, + 'Product that you are trying to add is not available.' => self::ERROR_NOT_SALABLE, + 'This product is out of stock' => self::ERROR_INSUFFICIENT_STOCK, + 'There are no source items' => self::ERROR_NOT_SALABLE, + 'The fewest you may purchase is' => self::ERROR_INSUFFICIENT_STOCK, + 'The most you may purchase is' => self::ERROR_INSUFFICIENT_STOCK, + 'The requested qty is not available' => self::ERROR_INSUFFICIENT_STOCK, + ]; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var array + */ + private $errors = []; + + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var MaskedQuoteIdToQuoteIdInterface + */ + private $maskedQuoteIdToQuoteId; + + /** + * @var BuyRequestBuilder + */ + private $requestBuilder; + + /** + * @param ProductRepositoryInterface $productRepository + * @param CartRepositoryInterface $cartRepository + * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + * @param BuyRequestBuilder $requestBuilder + */ + public function __construct( + ProductRepositoryInterface $productRepository, + CartRepositoryInterface $cartRepository, + MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, + BuyRequestBuilder $requestBuilder + ) { + $this->productRepository = $productRepository; + $this->cartRepository = $cartRepository; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->requestBuilder = $requestBuilder; + } + + /** + * Add cart items to the cart + * + * @param string $maskedCartId + * @param Data\CartItem[] $cartItems + * @return AddProductsToCartOutput + * @throws NoSuchEntityException Could not find a Cart with provided $maskedCartId + */ + public function execute(string $maskedCartId, array $cartItems): AddProductsToCartOutput + { + $cartId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId); + $cart = $this->cartRepository->get($cartId); + + foreach ($cartItems as $cartItemPosition => $cartItem) { + $this->addItemToCart($cart, $cartItem, $cartItemPosition); + } + + if ($cart->getData('has_error')) { + $errors = $cart->getErrors(); + + /** @var MessageInterface $error */ + foreach ($errors as $error) { + $this->addError($error->getText()); + } + } + + if (count($this->errors) !== 0) { + /* Revert changes introduced by add to cart processes in case of an error */ + $cart->getItemsCollection()->clear(); + } + + return $this->prepareErrorOutput($cart); + } + + /** + * Adds a particular item to the shopping cart + * + * @param CartInterface|Quote $cart + * @param Data\CartItem $cartItem + * @param int $cartItemPosition + */ + private function addItemToCart(CartInterface $cart, Data\CartItem $cartItem, int $cartItemPosition): void + { + $sku = $cartItem->getSku(); + + if ($cartItem->getQuantity() <= 0) { + $this->addError(__('The product quantity should be greater than 0')->render()); + + return; + } + + try { + $product = $this->productRepository->get($sku, false, null, true); + } catch (NoSuchEntityException $e) { + $this->addError( + __('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(), + $cartItemPosition + ); + + return; + } + + try { + $result = $cart->addProduct($product, $this->requestBuilder->build($cartItem)); + $this->cartRepository->save($cart); + } catch (\Throwable $e) { + $this->addError( + __($e->getMessage())->render(), + $cartItemPosition + ); + $cart->setHasError(false); + + return; + } + + if (is_string($result)) { + $errors = array_unique(explode("\n", $result)); + foreach ($errors as $error) { + $this->addError(__($error)->render(), $cartItemPosition); + } + } + } + + /** + * Add order line item error + * + * @param string $message + * @param int $cartItemPosition + * @return void + */ + private function addError(string $message, int $cartItemPosition = 0): void + { + $this->errors[] = new Data\Error( + $message, + $this->getErrorCode($message), + $cartItemPosition + ); + } + + /** + * Get message error code. + * + * TODO: introduce a separate class for getting error code from a message + * + * @param string $message + * @return string + */ + private function getErrorCode(string $message): string + { + foreach (self::MESSAGE_CODES as $codeMessage => $code) { + if (false !== stripos($message, $codeMessage)) { + return $code; + } + } + + /* If no code was matched, return the default one */ + return self::ERROR_UNDEFINED; + } + + /** + * Creates a new output from existing errors + * + * @param CartInterface $cart + * @return AddProductsToCartOutput + */ + private function prepareErrorOutput(CartInterface $cart): AddProductsToCartOutput + { + $output = new AddProductsToCartOutput($cart, $this->errors); + $this->errors = []; + $cart->setHasError(false); + + return $output; + } +} diff --git a/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php new file mode 100644 index 0000000000000..13b19e4f79c9a --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\BuyRequest; + +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * Build buy request for adding products to cart + */ +class BuyRequestBuilder +{ + /** + * @var BuyRequestDataProviderInterface[] + */ + private $providers; + + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + + /** + * @param DataObjectFactory $dataObjectFactory + * @param array $providers + */ + public function __construct( + DataObjectFactory $dataObjectFactory, + array $providers = [] + ) { + $this->dataObjectFactory = $dataObjectFactory; + $this->providers = $providers; + } + + /** + * Build buy request for adding product to cart + * + * @see \Magento\Quote\Model\Quote::addProduct + * @param CartItem $cartItem + * @return DataObject + */ + public function build(CartItem $cartItem): DataObject + { + $requestData = [ + ['qty' => $cartItem->getQuantity()] + ]; + + /** @var BuyRequestDataProviderInterface $provider */ + foreach ($this->providers as $provider) { + $requestData[] = $provider->execute($cartItem); + } + + return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + } +} diff --git a/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestDataProviderInterface.php b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestDataProviderInterface.php new file mode 100644 index 0000000000000..b9c41b18ee163 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestDataProviderInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\BuyRequest; + +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * Provides data for buy request for different types of products + */ +interface BuyRequestDataProviderInterface +{ + /** + * Provide buy request data from add to cart item request + * + * @param CartItem $cartItem + * @return array + */ + public function execute(CartItem $cartItem): array; +} diff --git a/app/code/Magento/Quote/Model/Cart/BuyRequest/CustomizableOptionDataProvider.php b/app/code/Magento/Quote/Model/Cart/BuyRequest/CustomizableOptionDataProvider.php new file mode 100644 index 0000000000000..90f2cbbc5f9e3 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/BuyRequest/CustomizableOptionDataProvider.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\BuyRequest; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * Extract buy request elements require for custom options + */ +class CustomizableOptionDataProvider implements BuyRequestDataProviderInterface +{ + private const OPTION_TYPE = 'custom-option'; + + /** + * @inheritdoc + * + * @throws LocalizedException + */ + public function execute(CartItem $cartItem): array + { + $customizableOptionsData = []; + + foreach ($cartItem->getSelectedOptions() as $optionData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $optionId, $optionValue] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $customizableOptionsData[$optionId][] = $optionValue; + } + } + + foreach ($cartItem->getEnteredOptions() as $option) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($option->getUid())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [$optionType, $optionId] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $customizableOptionsData[$optionId][] = $option->getValue(); + } + } + + return ['options' => $this->flattenOptionValues($customizableOptionsData)]; + } + + /** + * Flatten option values for non-multiselect customizable options + * + * @param array $customizableOptionsData + * @return array + */ + private function flattenOptionValues(array $customizableOptionsData): array + { + foreach ($customizableOptionsData as $optionId => $optionValue) { + if (count($optionValue) === 1) { + $customizableOptionsData[$optionId] = $optionValue[0]; + } + } + + return $customizableOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + if ($optionData[0] !== self::OPTION_TYPE) { + return false; + } + + return true; + } + + /** + * Validates the provided options structure + * + * @param array $optionData + * @throws LocalizedException + */ + private function validateInput(array $optionData): void + { + if (count($optionData) !== 3) { + throw new LocalizedException( + __('Wrong format of the entered option data') + ); + } + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/AddProductsToCartOutput.php b/app/code/Magento/Quote/Model/Cart/Data/AddProductsToCartOutput.php new file mode 100644 index 0000000000000..c12c02c0449f6 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/AddProductsToCartOutput.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +use Magento\Quote\Api\Data\CartInterface; + +/** + * DTO represents output for \Magento\Quote\Model\Cart\AddProductsToCart + */ +class AddProductsToCartOutput +{ + /** + * @var CartInterface + */ + private $cart; + + /** + * @var Error[] + */ + private $errors; + + /** + * @param CartInterface $cart + * @param Error[] $errors + */ + public function __construct(CartInterface $cart, array $errors) + { + $this->cart = $cart; + $this->errors = $errors; + } + + /** + * Get Shopping Cart + * + * @return CartInterface + */ + public function getCart(): CartInterface + { + return $this->cart; + } + + /** + * Get errors happened during adding item to the cart + * + * @return Error[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/CartItem.php b/app/code/Magento/Quote/Model/Cart/Data/CartItem.php new file mode 100644 index 0000000000000..9836247c56694 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/CartItem.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +/** + * DTO represents Cart Item data + */ +class CartItem +{ + /** + * @var string + */ + private $sku; + + /** + * @var float + */ + private $quantity; + + /** + * @var string + */ + private $parentSku; + + /** + * @var SelectedOption[] + */ + private $selectedOptions; + + /** + * @var EnteredOption[] + */ + private $enteredOptions; + + /** + * @param string $sku + * @param float $quantity + * @param string|null $parentSku + * @param array|null $selectedOptions + * @param array|null $enteredOptions + */ + public function __construct( + string $sku, + float $quantity, + string $parentSku = null, + array $selectedOptions = null, + array $enteredOptions = null + ) { + $this->sku = $sku; + $this->quantity = $quantity; + $this->parentSku = $parentSku; + $this->selectedOptions = $selectedOptions; + $this->enteredOptions = $enteredOptions; + } + + /** + * Returns cart item SKU + * + * @return string + */ + public function getSku(): string + { + return $this->sku; + } + + /** + * Returns cart item quantity + * + * @return float + */ + public function getQuantity(): float + { + return $this->quantity; + } + + /** + * Returns parent SKU + * + * @return string|null + */ + public function getParentSku(): ?string + { + return $this->parentSku; + } + + /** + * Returns selected options + * + * @return SelectedOption[]|null + */ + public function getSelectedOptions(): ?array + { + return $this->selectedOptions; + } + + /** + * Returns entered options + * + * @return EnteredOption[]|null + */ + public function getEnteredOptions(): ?array + { + return $this->enteredOptions; + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/CartItemFactory.php b/app/code/Magento/Quote/Model/Cart/Data/CartItemFactory.php new file mode 100644 index 0000000000000..823f03b28229c --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/CartItemFactory.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +use Magento\Framework\Exception\InputException; + +/** + * Creates CartItem DTO + */ +class CartItemFactory +{ + /** + * Creates CartItem DTO + * + * @param array $data + * @return CartItem + * @throws InputException + */ + public function create(array $data): CartItem + { + if (!isset($data['sku'], $data['quantity'])) { + throw new InputException(__('Required fields are not present: sku, quantity')); + } + return new CartItem( + $data['sku'], + $data['quantity'], + $data['parent_sku'] ?? null, + isset($data['selected_options']) ? $this->createSelectedOptions($data['selected_options']) : [], + isset($data['entered_options']) ? $this->createEnteredOptions($data['entered_options']) : [] + ); + } + + /** + * Creates array of Entered Options + * + * @param array $options + * @return EnteredOption[] + */ + private function createEnteredOptions(array $options): array + { + return \array_map( + function (array $option) { + if (!isset($option['uid'], $option['value'])) { + throw new InputException( + __('Required fields are not present EnteredOption.uid, EnteredOption.value') + ); + } + return new EnteredOption($option['uid'], $option['value']); + }, + $options + ); + } + + /** + * Creates array of Selected Options + * + * @param string[] $options + * @return SelectedOption[] + */ + private function createSelectedOptions(array $options): array + { + return \array_map( + function ($option) { + return new SelectedOption($option); + }, + $options + ); + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/EnteredOption.php b/app/code/Magento/Quote/Model/Cart/Data/EnteredOption.php new file mode 100644 index 0000000000000..ba55051d33805 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/EnteredOption.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +/** + * DTO for quote item entered option + */ +class EnteredOption +{ + /** + * @var string + */ + private $uid; + + /** + * @var string + */ + private $value; + + /** + * @param string $uid + * @param string $value + */ + public function __construct(string $uid, string $value) + { + $this->uid = $uid; + $this->value = $value; + } + + /** + * Returns entered option ID + * + * @return string + */ + public function getUid(): string + { + return $this->uid; + } + + /** + * Returns entered option value + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/Error.php b/app/code/Magento/Quote/Model/Cart/Data/Error.php new file mode 100644 index 0000000000000..42b14b06d94aa --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/Error.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +/** + * DTO represents error item + */ +class Error +{ + /** + * @var string + */ + private $message; + + /** + * @var string + */ + private $code; + + /** + * @var int + */ + private $cartItemPosition; + + /** + * @param string $message + * @param string $code + * @param int $cartItemPosition + */ + public function __construct(string $message, string $code, int $cartItemPosition) + { + $this->message = $message; + $this->code = $code; + $this->cartItemPosition = $cartItemPosition; + } + + /** + * Get error message + * + * @return string + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * Get error code + * + * @return string + */ + public function getCode(): string + { + return $this->code; + } + + /** + * Get cart item position + * + * @return int + */ + public function getCartItemPosition(): int + { + return $this->cartItemPosition; + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/SelectedOption.php b/app/code/Magento/Quote/Model/Cart/Data/SelectedOption.php new file mode 100644 index 0000000000000..70edd93cd8ef8 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/SelectedOption.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +/** + * DTO for quote item selected option + */ +class SelectedOption +{ + /** + * @var string + */ + private $id; + + /** + * @param string $id + */ + public function __construct(string $id) + { + $this->id = $id; + } + + /** + * Get selected option ID + * + * @return string + */ + public function getId(): string + { + return $this->id; + } +} diff --git a/app/code/Magento/Quote/Model/ChangeQuoteControl.php b/app/code/Magento/Quote/Model/ChangeQuoteControl.php index b88898a816d66..92e25ca6c7d3a 100644 --- a/app/code/Magento/Quote/Model/ChangeQuoteControl.php +++ b/app/code/Magento/Quote/Model/ChangeQuoteControl.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\Quote\Model; @@ -12,13 +11,10 @@ use Magento\Quote\Api\ChangeQuoteControlInterface; use Magento\Quote\Api\Data\CartInterface; -/** - * {@inheritdoc} - */ class ChangeQuoteControl implements ChangeQuoteControlInterface { /** - * @var UserContextInterface $userContext + * @var UserContextInterface */ private $userContext; @@ -31,25 +27,20 @@ public function __construct(UserContextInterface $userContext) } /** - * {@inheritdoc} + * @inheritdoc */ public function isAllowed(CartInterface $quote): bool { switch ($this->userContext->getUserType()) { case UserContextInterface::USER_TYPE_CUSTOMER: - $isAllowed = ($quote->getCustomerId() == $this->userContext->getUserId()); - break; + return ($quote->getCustomerId() == $this->userContext->getUserId()); case UserContextInterface::USER_TYPE_GUEST: - $isAllowed = ($quote->getCustomerId() === null); - break; + return ($quote->getCustomerId() === null); case UserContextInterface::USER_TYPE_ADMIN: case UserContextInterface::USER_TYPE_INTEGRATION: - $isAllowed = true; - break; - default: - $isAllowed = false; + return true; } - return $isAllowed; + return false; } } diff --git a/app/code/Magento/Quote/Model/CustomerManagement.php b/app/code/Magento/Quote/Model/CustomerManagement.php index 86725bd6211c7..3607cf7f9be63 100644 --- a/app/code/Magento/Quote/Model/CustomerManagement.php +++ b/app/code/Magento/Quote/Model/CustomerManagement.php @@ -3,14 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Quote\Model; -use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; use Magento\Customer\Api\AccountManagementInterface as AccountManagement; use Magento\Customer\Api\AddressRepositoryInterface as CustomerAddressRepository; -use Magento\Quote\Model\Quote as QuoteEntity; +use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Model\AddressFactory; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Validator\Exception as ValidatorException; +use Magento\Framework\Validator\Factory as ValidatorFactory; +use Magento\Quote\Model\Quote as QuoteEntity; /** * Class Customer @@ -33,12 +37,12 @@ class CustomerManagement protected $accountManagement; /** - * @var \Magento\Framework\Validator\Factory + * @var ValidatorFactory */ private $validatorFactory; /** - * @var \Magento\Customer\Model\AddressFactory + * @var AddressFactory */ private $addressFactory; @@ -47,23 +51,23 @@ class CustomerManagement * @param CustomerRepository $customerRepository * @param CustomerAddressRepository $customerAddressRepository * @param AccountManagement $accountManagement - * @param \Magento\Framework\Validator\Factory|null $validatorFactory - * @param \Magento\Customer\Model\AddressFactory|null $addressFactory + * @param ValidatorFactory|null $validatorFactory + * @param AddressFactory|null $addressFactory */ public function __construct( CustomerRepository $customerRepository, CustomerAddressRepository $customerAddressRepository, AccountManagement $accountManagement, - \Magento\Framework\Validator\Factory $validatorFactory = null, - \Magento\Customer\Model\AddressFactory $addressFactory = null + ValidatorFactory $validatorFactory = null, + AddressFactory $addressFactory = null ) { $this->customerRepository = $customerRepository; $this->customerAddressRepository = $customerAddressRepository; $this->accountManagement = $accountManagement; $this->validatorFactory = $validatorFactory ?: ObjectManager::getInstance() - ->get(\Magento\Framework\Validator\Factory::class); + ->get(ValidatorFactory::class); $this->addressFactory = $addressFactory ?: ObjectManager::getInstance() - ->get(\Magento\Customer\Model\AddressFactory::class); + ->get(AddressFactory::class); } /** @@ -82,6 +86,7 @@ public function populateCustomerInfo(QuoteEntity $quote) $quote->getPasswordHash() ); $quote->setCustomer($customer); + $this->fillCustomerAddressId($quote); } if (!$quote->getBillingAddress()->getId() && $customer->getDefaultBilling()) { $quote->getBillingAddress()->importCustomerAddressData( @@ -100,11 +105,36 @@ public function populateCustomerInfo(QuoteEntity $quote) } } + /** + * Filling 'CustomerAddressId' in quote for a newly created customer. + * + * @param QuoteEntity $quote + * @return void + */ + private function fillCustomerAddressId(QuoteEntity $quote): void + { + $customer = $quote->getCustomer(); + + $customer->getDefaultBilling() ? + $quote->getBillingAddress()->setCustomerAddressId($customer->getDefaultBilling()) : + $quote->getBillingAddress()->setCustomerAddressId(0); + + if ($customer->getDefaultShipping() || $customer->getDefaultBilling()) { + if ($quote->getShippingAddress()->getSameAsBilling()) { + $quote->getShippingAddress()->setCustomerAddressId($customer->getDefaultBilling()); + } else { + $quote->getShippingAddress()->setCustomerAddressId($customer->getDefaultShipping()); + } + } else { + $quote->getShippingAddress()->setCustomerAddressId(0); + } + } + /** * Validate Quote Addresses * * @param Quote $quote - * @throws \Magento\Framework\Validator\Exception + * @throws ValidatorException * @return void */ public function validateAddresses(QuoteEntity $quote) @@ -126,7 +156,7 @@ public function validateAddresses(QuoteEntity $quote) $addressModel = $this->addressFactory->create(); $addressModel->updateData($address); if (!$validator->isValid($addressModel)) { - throw new \Magento\Framework\Validator\Exception( + throw new ValidatorException( null, null, $validator->getMessages() diff --git a/app/code/Magento/Quote/Model/GuestCart/GuestCartResolver.php b/app/code/Magento/Quote/Model/GuestCart/GuestCartResolver.php new file mode 100644 index 0000000000000..45d2e60d103c1 --- /dev/null +++ b/app/code/Magento/Quote/Model/GuestCart/GuestCartResolver.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\GuestCart; + +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; +use Magento\Quote\Model\Quote; + +/** + * Return empty cart for guest + */ +class GuestCartResolver +{ + /** + * @var GuestCartManagementInterface + */ + private $guestCartManagement; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel + */ + private $quoteIdMaskResourceModel; + + /** + * @var \Magento\Quote\Api\GuestCartRepositoryInterface + */ + private $guestCartRepository; + + /** + * @param GuestCartManagementInterface $guestCartManagement + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel + * @param \Magento\Quote\Api\GuestCartRepositoryInterface $guestCartRepository + */ + public function __construct( + GuestCartManagementInterface $guestCartManagement, + QuoteIdMaskFactory $quoteIdMaskFactory, + QuoteIdMaskResourceModel $quoteIdMaskResourceModel, + \Magento\Quote\Api\GuestCartRepositoryInterface $guestCartRepository + ) { + $this->guestCartManagement = $guestCartManagement; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; + $this->guestCartRepository = $guestCartRepository; + } + + /** + * Create empty cart for guest + * + * @param string|null $predefinedMaskedQuoteId + * @return Quote + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function resolve(string $predefinedMaskedQuoteId = null): Quote + { + $maskedQuoteId = $this->guestCartManagement->createEmptyCart(); + + if ($predefinedMaskedQuoteId !== null) { + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $this->quoteIdMaskResourceModel->load($quoteIdMask, $maskedQuoteId, 'masked_id'); + + $quoteIdMask->setMaskedId($predefinedMaskedQuoteId); + $this->quoteIdMaskResourceModel->save($quoteIdMask); + $maskedQuoteId = $predefinedMaskedQuoteId; + } + + return $this->guestCartRepository->get($maskedQuoteId); + } +} diff --git a/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php b/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php index 152d575e059c8..5cdcca5349c1b 100644 --- a/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php +++ b/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php @@ -12,6 +12,7 @@ /** * Converts masked quote id to the quote id (entity id) * @api + * @since 101.1.0 */ interface MaskedQuoteIdToQuoteIdInterface { @@ -19,6 +20,7 @@ interface MaskedQuoteIdToQuoteIdInterface * @param string $maskedQuoteId * @return int * @throws NoSuchEntityException + * @since 101.1.0 */ public function execute(string $maskedQuoteId): int; } diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index c7d40c82bbcc2..d2e900138cd06 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -10,6 +10,7 @@ use Magento\Directory\Model\AllowedCountries; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Model\AbstractExtensibleModel; use Magento\Quote\Api\Data\PaymentInterface; use Magento\Quote\Model\Quote\Address; @@ -873,7 +874,7 @@ public function beforeSave() * Loading quote data by customer * * @param \Magento\Customer\Model\Customer|int $customer - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return $this */ public function loadByCustomer($customer) @@ -1104,7 +1105,22 @@ public function getCustomerTaxClassId() //if (!$this->getData('customer_group_id') && !$this->getData('customer_tax_class_id')) { $groupId = $this->getCustomerGroupId(); if ($groupId !== null) { - $taxClassId = $this->groupRepository->getById($this->getCustomerGroupId())->getTaxClassId(); + $taxClassId = null; + try { + $taxClassId = $this->groupRepository->getById($this->getCustomerGroupId())->getTaxClassId(); + } catch (NoSuchEntityException $e) { + /** + * A customer MAY create a quote and AFTER that customer group MAY be deleted. + * That breaks a quote because it still refers no a non-existent customer group. + * In such a case we should load a new customer group id from the current customer + * object and use it to retrieve tax class and update quote. + */ + $groupId = $this->getCustomer()->getGroupId(); + $this->setCustomerGroupId($groupId); + if ($groupId !== null) { + $taxClassId = $this->groupRepository->getById($groupId)->getTaxClassId(); + } + } $this->setCustomerTaxClassId($taxClassId); } diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index 39148f990b714..5476915d9d649 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -238,7 +238,7 @@ class Address extends AbstractAddress implements /** * @var RateFactory - * @since 100.2.0 + * @since 101.0.0 */ protected $_addressRateFactory; @@ -1019,6 +1019,13 @@ public function collectShippingRates() */ public function requestShippingRates(AbstractItem $item = null) { + $storeId = $this->getQuote()->getStoreId() ?: $this->storeManager->getStore()->getId(); + $taxInclude = $this->_scopeConfig->getValue( + 'tax/calculation/price_includes_tax', + ScopeInterface::SCOPE_STORE, + $storeId + ); + /** @var $request RateRequest */ $request = $this->_rateRequestFactory->create(); $request->setAllItems($item ? [$item] : $this->getAllItems()); @@ -1028,9 +1035,11 @@ public function requestShippingRates(AbstractItem $item = null) $request->setDestStreet($this->getStreetFull()); $request->setDestCity($this->getCity()); $request->setDestPostcode($this->getPostcode()); - $request->setPackageValue($item ? $item->getBaseRowTotal() : $this->getBaseSubtotal()); + $baseSubtotal = $taxInclude ? $this->getBaseSubtotalTotalInclTax() : $this->getBaseSubtotal(); + $request->setPackageValue($item ? $item->getBaseRowTotal() : $baseSubtotal); + $baseSubtotalWithDiscount = $baseSubtotal + $this->getBaseDiscountAmount(); $packageWithDiscount = $item ? $item->getBaseRowTotal() - - $item->getBaseDiscountAmount() : $this->getBaseSubtotalWithDiscount(); + $item->getBaseDiscountAmount() : $baseSubtotalWithDiscount; $request->setPackageValueWithDiscount($packageWithDiscount); $request->setPackageWeight($item ? $item->getRowWeight() : $this->getWeight()); $request->setPackageQty($item ? $item->getQty() : $this->getItemQty()); @@ -1038,8 +1047,7 @@ public function requestShippingRates(AbstractItem $item = null) /** * Need for shipping methods that use insurance based on price of physical products */ - $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $this->getBaseSubtotal() - - $this->getBaseVirtualAmount(); + $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $baseSubtotal - $this->getBaseVirtualAmount(); $request->setPackagePhysicalValue($packagePhysicalValue); $request->setFreeMethodWeight($item ? 0 : $this->getFreeMethodWeight()); @@ -1047,12 +1055,10 @@ public function requestShippingRates(AbstractItem $item = null) /** * Store and website identifiers specified from StoreManager */ + $request->setStoreId($storeId); if ($this->getQuote()->getStoreId()) { - $storeId = $this->getQuote()->getStoreId(); - $request->setStoreId($storeId); $request->setWebsiteId($this->storeManager->getStore($storeId)->getWebsiteId()); } else { - $request->setStoreId($this->storeManager->getStore()->getId()); $request->setWebsiteId($this->storeManager->getWebsite()->getId()); } $request->setFreeShipping($this->getFreeShipping()); diff --git a/app/code/Magento/Quote/Model/Quote/Address/Item.php b/app/code/Magento/Quote/Model/Quote/Address/Item.php index ade4f9270b68f..bbf74d5a28935 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Item.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Item.php @@ -199,6 +199,7 @@ public function importQuoteItem(\Magento\Quote\Model\Quote\Item $quoteItem) /** * @inheritdoc + * @since 101.1.1 */ public function getOptionByCode($code) { diff --git a/app/code/Magento/Quote/Model/Quote/Item.php b/app/code/Magento/Quote/Model/Quote/Item.php index 2e4a9c7ded683..22554380ca61e 100644 --- a/app/code/Magento/Quote/Model/Quote/Item.php +++ b/app/code/Magento/Quote/Model/Quote/Item.php @@ -173,7 +173,7 @@ class Item extends \Magento\Quote\Model\Quote\Item\AbstractItem implements \Mage /** * @var \Magento\CatalogInventory\Api\StockRegistryInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $stockRegistry; diff --git a/app/code/Magento/Quote/Model/Quote/Item/Processor.php b/app/code/Magento/Quote/Model/Quote/Item/Processor.php index ef4b853862681..c6bef1cc80bfb 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Processor.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Processor.php @@ -97,7 +97,9 @@ public function prepare(Item $item, DataObject $request, Product $candidate): vo $item->addQty($candidate->getCartQty()); $customPrice = $request->getCustomPrice(); - $item->setPrice($candidate->getFinalPrice()); + if (!$item->getParentItem() || $item->getParentItem()->isChildrenCalculated()) { + $item->setPrice($candidate->getFinalPrice()); + } if (!empty($customPrice)) { $item->setCustomPrice($customPrice); $item->setOriginalCustomPrice($customPrice); diff --git a/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php b/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php index 38bfcbf1d30ca..78aa31d7d9527 100644 --- a/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php +++ b/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php @@ -19,7 +19,7 @@ class ValidationMessage /** * @var \Magento\Framework\Locale\CurrencyInterface - * @deprecated since 101.0.0 + * @deprecated 101.0.3 since 101.0.0 */ private $currency; diff --git a/app/code/Magento/Quote/Model/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php index e7750f5879de5..f0bc12f7b3a36 100644 --- a/app/code/Magento/Quote/Model/QuoteAddressValidator.php +++ b/app/code/Magento/Quote/Model/QuoteAddressValidator.php @@ -31,7 +31,7 @@ class QuoteAddressValidator protected $customerRepository; /** - * @deprecated This class is not a part of HTML presentation layer and should not use sessions. + * @deprecated 101.1.1 This class is not a part of HTML presentation layer and should not use sessions. */ protected $customerSession; diff --git a/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php b/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php index 4d2a8ce877d8c..2a73a648889fb 100644 --- a/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php +++ b/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php @@ -12,6 +12,7 @@ /** * Converts quote id to the masked quote id * @api + * @since 101.1.0 */ interface QuoteIdToMaskedQuoteIdInterface { @@ -19,6 +20,7 @@ interface QuoteIdToMaskedQuoteIdInterface * @param int $quoteId * @return string * @throws NoSuchEntityException + * @since 101.1.0 */ public function execute(int $quoteId): string; } diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index 3a81341e2b02a..b0aef022dcd25 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -24,7 +24,7 @@ use Magento\Store\Model\StoreManagerInterface; /** - * Class QuoteManagement + * Class for managing quote * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -250,6 +250,7 @@ public function createEmptyCart() $quote->setBillingAddress($this->quoteAddressFactory->create()); $quote->setShippingAddress($this->quoteAddressFactory->create()); + $quote->setCustomerIsGuest(1); try { $quote->getShippingAddress()->setCollectShippingRates(true); diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php index ccfd3df5fafa3..0dd2b00a596ea 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository.php +++ b/app/code/Magento/Quote/Model/QuoteRepository.php @@ -43,7 +43,7 @@ class QuoteRepository implements CartRepositoryInterface /** * @var QuoteFactory - * @deprecated + * @deprecated 101.1.2 */ protected $quoteFactory; @@ -54,7 +54,7 @@ class QuoteRepository implements CartRepositoryInterface /** * @var QuoteCollection - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $quoteCollection; @@ -261,7 +261,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) * @param FilterGroup $filterGroup The filter group. * @param QuoteCollection $collection The quote collection. * @return void - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @throws InputException The specified filter group or quote collection does not exist. */ protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCollection $collection) diff --git a/app/code/Magento/Quote/Model/QuoteRepository/Plugin/AccessChangeQuoteControl.php b/app/code/Magento/Quote/Model/QuoteRepository/Plugin/AccessChangeQuoteControl.php index 79b518fc54534..eda0e9638cc0d 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository/Plugin/AccessChangeQuoteControl.php +++ b/app/code/Magento/Quote/Model/QuoteRepository/Plugin/AccessChangeQuoteControl.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Quote\Model\QuoteRepository\Plugin; @@ -32,16 +33,17 @@ public function __construct(ChangeQuoteControlInterface $changeQuoteControl) /** * Checks if change quote's customer id is allowed for current user. * + * A StateException is thrown if Guest's or Customer's customer_id not match user_id or unknown user type + * * @param CartRepositoryInterface $subject * @param CartInterface $quote - * @throws StateException if Guest has customer_id or Customer's customer_id not much with user_id - * or unknown user's type * @return void + * @throws StateException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeSave(CartRepositoryInterface $subject, CartInterface $quote) + public function beforeSave(CartRepositoryInterface $subject, CartInterface $quote): void { - if (! $this->changeQuoteControl->isAllowed($quote)) { + if (!$this->changeQuoteControl->isAllowed($quote)) { throw new StateException(__("Invalid state change requested")); } } diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote.php b/app/code/Magento/Quote/Model/ResourceModel/Quote.php index 48945dacd1738..e6350dd5aeb2b 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote.php @@ -230,7 +230,8 @@ public function subtractProductFromQuotes($product) 'items_qty' => new \Zend_Db_Expr( $connection->quoteIdentifier('q.items_qty') . ' - ' . $connection->quoteIdentifier('qi.qty') ), - 'items_count' => new \Zend_Db_Expr($ifSql) + 'items_count' => new \Zend_Db_Expr($ifSql), + 'updated_at' => 'q.updated_at', ] )->join( ['qi' => $this->getTable('quote_item')], @@ -257,7 +258,7 @@ public function subtractProductFromQuotes($product) * * @param \Magento\Catalog\Model\Product $product * - * @deprecated 101.0.1 + * @deprecated 101.0.3 * @see \Magento\Quote\Model\ResourceModel\Quote::subtractProductFromQuotes * * @return $this @@ -277,21 +278,27 @@ public function markQuotesRecollect($productIds) { $tableQuote = $this->getTable('quote'); $tableItem = $this->getTable('quote_item'); - $subSelect = $this->getConnection()->select()->from( - $tableItem, - ['entity_id' => 'quote_id'] - )->where( - 'product_id IN ( ? )', - $productIds - )->group( - 'quote_id' - ); - - $select = $this->getConnection()->select()->join( - ['t2' => $subSelect], - 't1.entity_id = t2.entity_id', - ['trigger_recollect' => new \Zend_Db_Expr('1')] - ); + $subSelect = $this->getConnection() + ->select() + ->from( + $tableItem, + ['entity_id' => 'quote_id'] + )->where( + 'product_id IN ( ? )', + $productIds + )->group( + 'quote_id' + ); + $select = $this->getConnection() + ->select() + ->join( + ['t2' => $subSelect], + 't1.entity_id = t2.entity_id', + [ + 'trigger_recollect' => new \Zend_Db_Expr('1'), + 'updated_at' => 't1.updated_at', + ] + ); $updateQuery = $select->crossUpdateFromSelect(['t1' => $tableQuote]); $this->getConnection()->query($updateQuery); diff --git a/app/code/Magento/Quote/Model/ShippingMethodManagement.php b/app/code/Magento/Quote/Model/ShippingMethodManagement.php index d9fa37c0185a9..dab4fa98607a0 100644 --- a/app/code/Magento/Quote/Model/ShippingMethodManagement.php +++ b/app/code/Magento/Quote/Model/ShippingMethodManagement.php @@ -286,7 +286,7 @@ public function estimateByAddressId($cartId, $addressId) * @param ExtensibleDataInterface|null $address * @return ShippingMethodInterface[] An array of shipping methods. * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @deprecated 100.2.0 + * @deprecated 100.1.6 */ protected function getEstimatedRates( Quote $quote, @@ -366,7 +366,7 @@ private function extractAddressData($address) * Gets the data object processor * * @return DataObjectProcessor - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getDataObjectProcessor() { 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..d938ad7d638f1 100644 --- a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php +++ b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php @@ -5,7 +5,16 @@ */ namespace Magento\Quote\Observer\Frontend\Quote\Address; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Customer\Helper\Address; +use Magento\Customer\Model\Session; +use Magento\Customer\Model\Vat; +use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Model\Quote; /** * Handle customer VAT number on collect_totals_before event of quote address. @@ -15,22 +24,22 @@ class CollectTotalsObserver implements ObserverInterface { /** - * @var \Magento\Customer\Api\AddressRepositoryInterface + * @var AddressRepositoryInterface */ private $addressRepository; /** - * @var \Magento\Customer\Model\Session + * @var Session */ private $customerSession; /** - * @var \Magento\Customer\Helper\Address + * @var Address */ protected $customerAddressHelper; /** - * @var \Magento\Customer\Model\Vat + * @var Vat */ protected $customerVat; @@ -40,36 +49,36 @@ class CollectTotalsObserver implements ObserverInterface protected $vatValidator; /** - * @var \Magento\Customer\Api\Data\CustomerInterfaceFactory + * @var CustomerInterfaceFactory */ protected $customerDataFactory; /** * Group Management * - * @var \Magento\Customer\Api\GroupManagementInterface + * @var GroupManagementInterface */ protected $groupManagement; /** * Initialize dependencies. * - * @param \Magento\Customer\Helper\Address $customerAddressHelper - * @param \Magento\Customer\Model\Vat $customerVat + * @param Address $customerAddressHelper + * @param Vat $customerVat * @param VatValidator $vatValidator - * @param \Magento\Customer\Api\Data\CustomerInterfaceFactory $customerDataFactory - * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement - * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository - * @param \Magento\Customer\Model\Session $customerSession + * @param CustomerInterfaceFactory $customerDataFactory + * @param GroupManagementInterface $groupManagement + * @param AddressRepositoryInterface $addressRepository + * @param Session $customerSession */ public function __construct( - \Magento\Customer\Helper\Address $customerAddressHelper, - \Magento\Customer\Model\Vat $customerVat, + Address $customerAddressHelper, + Vat $customerVat, VatValidator $vatValidator, - \Magento\Customer\Api\Data\CustomerInterfaceFactory $customerDataFactory, - \Magento\Customer\Api\GroupManagementInterface $groupManagement, - \Magento\Customer\Api\AddressRepositoryInterface $addressRepository, - \Magento\Customer\Model\Session $customerSession + CustomerInterfaceFactory $customerDataFactory, + GroupManagementInterface $groupManagement, + AddressRepositoryInterface $addressRepository, + Session $customerSession ) { $this->customerVat = $customerVat; $this->customerAddressHelper = $customerAddressHelper; @@ -83,25 +92,23 @@ public function __construct( /** * Handle customer VAT number if needed on collect_totals_before event of quote address * - * @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 \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment */ + /** @var ShippingAssignmentInterface $shippingAssignment */ $shippingAssignment = $observer->getShippingAssignment(); - /** @var \Magento\Quote\Model\Quote $quote */ + /** @var Quote $quote */ $quote = $observer->getQuote(); - /** @var \Magento\Quote\Model\Quote\Address $address */ + /** @var Quote\Address $address */ $address = $shippingAssignment->getShipping()->getAddress(); $customer = $quote->getCustomer(); $storeId = $customer->getStoreId(); - if ($customer->getDisableAutoGroupChange() - || false == $this->vatValidator->isEnabled($address, $storeId) - ) { + if ($customer->getDisableAutoGroupChange() || !$this->vatValidator->isEnabled($address, $storeId)) { return; } $customerCountryCode = $address->getCountryId(); @@ -119,9 +126,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( @@ -136,6 +142,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $quote->setCustomerGroupId($groupId); $this->customerSession->setCustomerGroupId($groupId); $customer->setGroupId($groupId); + $customer->setEmail($customer->getEmail() ?: $quote->getCustomerEmail()); $quote->setCustomer($customer); } } diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml new file mode 100755 index 0000000000000..a14be3b533fa8 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerCart" type="CustomerCart"> + <var key="customer_id" entityType="customer" entityKey="id"/> + </entity> + + <entity name="CustomerAddressInformation" type="CustomerAddressInformation"> + <var key="cart_id" entityKey="return" entityType="CustomerCart"/> + <requiredEntity type="shipping_address">ShippingAddressTX</requiredEntity> + <requiredEntity type="billing_address">BillingAddressTX</requiredEntity> + <data key="shipping_method_code">flatrate</data> + <data key="shipping_carrier_code">flatrate</data> + </entity> + + <entity name="CustomerOrderPaymentMethod" type="CustomerPaymentInformation"> + <var key="cart_id" entityKey="return" entityType="CustomerCart"/> + <requiredEntity type="payment_method">PaymentMethodCheckMoneyOrder</requiredEntity> + <requiredEntity type="billing_address">BillingAddressTX</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml new file mode 100644 index 0000000000000..3681245311188 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerCartItem" type="CustomerCartItem"> + <var key="quote_id" entityKey="return" entityType="CustomerCart"/> + <var key="sku" entityKey="sku" entityType="product"/> + <data key="qty">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml new file mode 100644 index 0000000000000..f5555394f8d4d --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + + <operation name="CreateCustomerCartItem" dataType="CustomerCartItem" type="create" auth="adminOauth" url="/V1/carts/mine/items" method="POST"> + <contentType>application/json</contentType> + <object key="cartItem" dataType="CustomerCartItem"> + <field key="quote_id" type="string">string</field> + <field key="sku" type="string">string</field> + <field key="qty">integer</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml new file mode 100644 index 0000000000000..f233954f2cdcf --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCustomerCart" dataType="CustomerCart" type="create" + auth="adminOauth" url="/V1/carts/mine" method="POST" > + <contentType>application/json</contentType> + <field key="customer_id">string</field> + </operation> + + <operation name="AddAddressInfoToCustomerCart" dataType="CustomerAddressInformation" type="create" auth="adminOauth" url="/V1/carts/mine/shipping-information" method="POST"> + <contentType>application/json</contentType> + <field key="cart_id">string</field> + <object key="addressInformation" dataType="CustomerAddressInformation"> + <object key="shipping_address" dataType="shipping_address"> + <field key="city">string</field> + <field key="region">string</field> + <field key="region_code">string</field> + <field key="region_id">integer</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="postcode">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="email">string</field> + <field key="telephone">string</field> + </object> + <object key="billing_address" dataType="billing_address"> + <field key="city">string</field> + <field key="region">string</field> + <field key="region_code">string</field> + <field key="region_id">integer</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="postcode">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="email">string</field> + <field key="telephone">string</field> + </object> + <field key="shipping_method_code">string</field> + <field key="shipping_carrier_code">string</field> + </object> + </operation> + + <operation name="SendCustomerPaymentInformation" dataType="CustomerPaymentInformation" type="update" auth="adminOauth" url="/V1/carts/mine/payment-information" method="POST"> + <contentType>application/json</contentType> + <field key="cart_id">string</field> + <object key="paymentMethod" dataType="payment_method"> + <field key="method">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index 904a07d72035f..80af412439338 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -75,7 +75,9 @@ </createData> <magentoCLI command="config:set customer/online_customers/section_data_lifetime 1" stepKey="setConfigForCartLifetime"/> - <magentoCLI command="cache:flush" stepKey="flushCache" /> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> @@ -85,7 +87,7 @@ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductListing"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductListing"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> @@ -105,13 +107,12 @@ <openNewTab stepKey="openNewTab"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Find the first simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="$$createConfigChildProduct1$$"/> </actionGroup> <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- Disabled child configurable product --> <click selector="{{AdminProductFormSection.enableProductAttributeLabel}}" stepKey="clickDisableProduct"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveProduct"/> @@ -137,12 +138,11 @@ <!-- Disabled via admin panel --> <openNewTab stepKey="openNewTab2"/> <!-- Find the first simple product that we just created using the product grid and go to its page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct2"> <argument name="product" value="$$createSimpleProduct2$$"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> - <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage2"/> <!-- Disabled simple product from grid --> <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid2"> <argument name="product" value="$$createSimpleProduct2$$"/> diff --git a/app/code/Magento/Quote/Test/Unit/Model/ChangeQuoteControlTest.php b/app/code/Magento/Quote/Test/Unit/Model/ChangeQuoteControlTest.php index f302372344c11..a467f3e25d698 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/ChangeQuoteControlTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/ChangeQuoteControlTest.php @@ -16,130 +16,94 @@ /** * Unit test for \Magento\Quote\Model\ChangeQuoteControl - * - * Class \Magento\Quote\Test\Unit\Model\ChangeQuoteControlTest */ class ChangeQuoteControlTest extends TestCase { - /** - * @var ObjectManager - */ - protected $objectManager; - /** * @var ChangeQuoteControl */ protected $model; /** - * @var MockObject + * @var MockObject|UserContextInterface */ protected $userContextMock; /** - * @var MockObject + * @var MockObject|CartInterface */ protected $quoteMock; protected function setUp(): void { - $this->objectManager = new ObjectManager($this); $this->userContextMock = $this->getMockForAbstractClass(UserContextInterface::class); - $this->model = $this->objectManager->getObject( - ChangeQuoteControl::class, - [ - 'userContext' => $this->userContextMock - ] - ); - - $this->quoteMock = $this->getMockForAbstractClass( - CartInterface::class, - [], - '', - false, - true, - true, - ['getCustomerId'] - ); + $this->model = new ChangeQuoteControl($this->userContextMock); + + $this->quoteMock = $this->getMockBuilder(CartInterface::class) + ->disableOriginalConstructor() + ->addMethods(['getCustomerId']) + ->getMockForAbstractClass(); } - /** - * Test if the quote is belonged to customer - */ public function testIsAllowedIfTheQuoteIsBelongedToCustomer() { $quoteCustomerId = 1; - $this->quoteMock->expects($this->any())->method('getCustomerId') + $this->quoteMock->method('getCustomerId') ->willReturn($quoteCustomerId); - $this->userContextMock->expects($this->any())->method('getUserType') + $this->userContextMock->method('getUserType') ->willReturn(UserContextInterface::USER_TYPE_CUSTOMER); - $this->userContextMock->expects($this->any())->method('getUserId') + $this->userContextMock->method('getUserId') ->willReturn($quoteCustomerId); $this->assertTrue($this->model->isAllowed($this->quoteMock)); } - /** - * Test if the quote is not belonged to customer - */ public function testIsAllowedIfTheQuoteIsNotBelongedToCustomer() { $currentCustomerId = 1; $quoteCustomerId = 2; - $this->quoteMock->expects($this->any())->method('getCustomerId') + $this->quoteMock->method('getCustomerId') ->willReturn($quoteCustomerId); - $this->userContextMock->expects($this->any())->method('getUserType') + $this->userContextMock->method('getUserType') ->willReturn(UserContextInterface::USER_TYPE_CUSTOMER); - $this->userContextMock->expects($this->any())->method('getUserId') + $this->userContextMock->method('getUserId') ->willReturn($currentCustomerId); $this->assertFalse($this->model->isAllowed($this->quoteMock)); } - /** - * Test if the quote is belonged to guest and the context is guest - */ public function testIsAllowedIfQuoteIsBelongedToGuestAndContextIsGuest() { $quoteCustomerId = null; - $this->quoteMock->expects($this->any())->method('getCustomerId') + $this->quoteMock->method('getCustomerId') ->willReturn($quoteCustomerId); - $this->userContextMock->expects($this->any())->method('getUserType') + $this->userContextMock->method('getUserType') ->willReturn(UserContextInterface::USER_TYPE_GUEST); $this->assertTrue($this->model->isAllowed($this->quoteMock)); } - /** - * Test if the quote is belonged to customer and the context is guest - */ public function testIsAllowedIfQuoteIsBelongedToCustomerAndContextIsGuest() { $quoteCustomerId = 1; - $this->quoteMock->expects($this->any())->method('getCustomerId') + $this->quoteMock->method('getCustomerId') ->willReturn($quoteCustomerId); - $this->userContextMock->expects($this->any())->method('getUserType') + $this->userContextMock->method('getUserType') ->willReturn(UserContextInterface::USER_TYPE_GUEST); $this->assertFalse($this->model->isAllowed($this->quoteMock)); } - /** - * Test if the context is admin - */ public function testIsAllowedIfContextIsAdmin() { - $this->userContextMock->expects($this->any())->method('getUserType') + $this->userContextMock->method('getUserType') ->willReturn(UserContextInterface::USER_TYPE_ADMIN); $this->assertTrue($this->model->isAllowed($this->quoteMock)); } - /** - * Test if the context is integration - */ public function testIsAllowedIfContextIsIntegration() { - $this->userContextMock->expects($this->any())->method('getUserType') + $this->userContextMock->method('getUserType') ->willReturn(UserContextInterface::USER_TYPE_INTEGRATION); $this->assertTrue($this->model->isAllowed($this->quoteMock)); } diff --git a/app/code/Magento/Quote/Test/Unit/Model/CustomerManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/CustomerManagementTest.php index 956598d17b4d6..26d6aea049915 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/CustomerManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/CustomerManagementTest.php @@ -147,7 +147,7 @@ protected function setUp(): void public function testPopulateCustomerInfo() { - $this->quoteMock->expects($this->once()) + $this->quoteMock->expects($this->atLeastOnce()) ->method('getCustomer') ->willReturn($this->customerMock); $this->customerMock->expects($this->atLeastOnce()) diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php index 2f8a5a344503c..426fa6183fec2 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php @@ -32,22 +32,16 @@ */ class SubtotalTest extends TestCase { - /** - * @var ObjectManager - */ + /** @var ObjectManager */ protected $objectManager; - /** - * @var Subtotal - */ + /** @var Subtotal */ protected $subtotalModel; /** @var MockObject */ protected $stockItemMock; - /** - * @var MockObject - */ + /** @var MockObject */ protected $stockRegistry; protected function setUp(): void @@ -57,14 +51,15 @@ protected function setUp(): void Subtotal::class ); - $this->stockRegistry = $this->createPartialMock( - StockRegistry::class, - ['getStockItem', '__wakeup'] - ); - $this->stockItemMock = $this->createPartialMock( - \Magento\CatalogInventory\Model\Stock\Item::class, - ['getIsInStock', '__wakeup'] - ); + $this->stockRegistry = $this->getMockBuilder(StockRegistry::class) + ->disableOriginalConstructor() + ->addMethods(['__wakeup']) + ->onlyMethods(['getStockItem']) + ->getMock(); + $this->stockItemMock = $this->getMockBuilder(\Magento\CatalogInventory\Model\Stock\Item::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIsInStock', '__wakeup']) + ->getMock(); } /** @@ -110,10 +105,11 @@ public function testCollect($price, $originalPrice, $itemHasParent, $expectedPri ] ); /** @var Address|MockObject $address */ - $address = $this->createPartialMock( - Address::class, - ['setTotalQty', 'getTotalQty', 'removeItem', 'getQuote'] - ); + $address = $this->getMockBuilder(Address::class) + ->disableOriginalConstructor() + ->onlyMethods(['removeItem', 'getQuote']) + ->addMethods(['setTotalQty', 'getTotalQty']) + ->getMock(); /** @var Product|MockObject $product */ $product = $this->createMock(Product::class); @@ -161,10 +157,10 @@ public function testCollect($price, $originalPrice, $itemHasParent, $expectedPri $shippingAssignmentMock->expects($this->exactly(2))->method('getShipping')->willReturn($shipping); $shippingAssignmentMock->expects($this->once())->method('getItems')->willReturn([$quoteItem]); - $total = $this->createPartialMock( - Total::class, - ['setBaseVirtualAmount', 'setVirtualAmount'] - ); + $total = $this->getMockBuilder(Total::class) + ->disableOriginalConstructor() + ->addMethods(['setVirtualAmount', 'setBaseVirtualAmount']) + ->getMock(); $total->expects($this->once())->method('setBaseVirtualAmount')->willReturnSelf(); $total->expects($this->once())->method('setVirtualAmount')->willReturnSelf(); @@ -185,7 +181,11 @@ public function testFetch() ]; $quoteMock = $this->createMock(Quote::class); - $totalMock = $this->createPartialMock(Total::class, ['getSubtotal']); + $totalMock = $this->getMockBuilder(Total::class) + ->addMethods(['getSubtotal']) + ->disableOriginalConstructor() + ->getMock(); + $totalMock->expects($this->once())->method('getSubtotal')->willReturn(100); $this->assertEquals($expectedResult, $this->subtotalModel->fetch($quoteMock, $totalMock)); @@ -229,13 +229,11 @@ public function testCollectWithInvalidItems() $address->expects($this->once()) ->method('removeItem') ->with($addressItemId); - $addressItem = $this->createPartialMock( - AddressItem::class, - [ - 'getId', - 'getQuoteItemId' - ] - ); + $addressItem = $this->getMockBuilder(AddressItem::class) + ->disableOriginalConstructor() + ->onlyMethods(['getId']) + ->addMethods(['getQuoteItemId']) + ->getMock(); $addressItem->setAddress($address); $addressItem->method('getId') ->willReturn($addressItemId); diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php index a8fd794c08757..d4f6778a2ccb8 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php @@ -352,10 +352,40 @@ public function testRequestShippingRates() $currentCurrencyCode = 'UAH'; + $this->quote->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); + + $this->storeManager->expects($this->at(0)) + ->method('getStore') + ->with($storeId) + ->willReturn($this->store); + $this->store->expects($this->any()) + ->method('getWebsiteId') + ->willReturn($webSiteId); + + $this->scopeConfig->expects($this->exactly(1)) + ->method('getValue') + ->with( + 'tax/calculation/price_includes_tax', + ScopeInterface::SCOPE_STORE, + $storeId + ) + ->willReturn(1); + /** @var RateRequest */ $request = $this->getMockBuilder(RateRequest::class) ->disableOriginalConstructor() - ->setMethods(['setStoreId', 'setWebsiteId', 'setBaseCurrency', 'setPackageCurrency']) + ->setMethods( + [ + 'setStoreId', + 'setWebsiteId', + 'setBaseCurrency', + 'setPackageCurrency', + 'getBaseSubtotalTotalInclTax', + 'getBaseSubtotal' + ] + ) ->getMock(); /** @var Collection */ @@ -434,13 +464,6 @@ public function testRequestShippingRates() $this->storeManager->method('getStore') ->willReturn($this->store); - $this->storeManager->expects($this->once()) - ->method('getWebsite') - ->willReturn($this->website); - - $this->store->method('getId') - ->willReturn($storeId); - $this->store->method('getBaseCurrency') ->willReturn($baseCurrency); @@ -452,10 +475,6 @@ public function testRequestShippingRates() ->method('getCurrentCurrencyCode') ->willReturn($currentCurrencyCode); - $this->website->expects($this->once()) - ->method('getId') - ->willReturn($webSiteId); - $this->addressRateFactory->expects($this->once()) ->method('create') ->willReturn($rate); diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php index cbcb7dd0adc3c..3025a72410671 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php @@ -77,7 +77,16 @@ protected function setUp(): void $this->itemMock = $this->getMockBuilder(Item::class) ->addMethods(['setOriginalCustomPrice']) - ->onlyMethods(['getId', 'setOptions', 'setProduct', 'addQty', 'setCustomPrice', 'setData', 'setPrice']) + ->onlyMethods([ + 'getId', + 'setOptions', + 'setProduct', + 'addQty', + 'setCustomPrice', + 'setData', + 'setPrice', + 'getParentItem' + ]) ->disableOriginalConstructor() ->getMock(); $this->quoteItemFactoryMock->expects($this->any()) @@ -438,4 +447,41 @@ public function testPrepareWithResetCountAndNotStickAndSameItemId() $this->processor->prepare($this->itemMock, $this->objectMock, $this->productMock); } + + /** + * @param bool $isChildrenCalculated + * @dataProvider prepareChildProductDataProvider + */ + public function testPrepareChildProduct(bool $isChildrenCalculated): void + { + $finalPrice = 10; + $this->objectMock->method('getResetCount') + ->willReturn(false); + $this->productMock->method('getFinalPrice') + ->willReturn($finalPrice); + $this->itemMock->expects($isChildrenCalculated ? $this->once() : $this->never()) + ->method('setPrice') + ->with($finalPrice) + ->willReturnSelf(); + $parentItem = $this->createConfiguredMock( + \Magento\Quote\Model\Quote\Item::class, + [ + 'isChildrenCalculated' => $isChildrenCalculated + ] + ); + $this->itemMock->method('getParentItem') + ->willReturn($parentItem); + $this->processor->prepare($this->itemMock, $this->objectMock, $this->productMock); + } + + /** + * @return array + */ + public function prepareChildProductDataProvider(): array + { + return [ + [false], + [true] + ]; + } } diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/Plugin/AccessChangeQuoteControlTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/Plugin/AccessChangeQuoteControlTest.php index 85098d2f23448..199ddfd9b9120 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/Plugin/AccessChangeQuoteControlTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/Plugin/AccessChangeQuoteControlTest.php @@ -8,6 +8,7 @@ namespace Magento\Quote\Test\Unit\Model\QuoteRepository\Plugin; use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\Exception\StateException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Model\ChangeQuoteControl; use Magento\Quote\Model\Quote; @@ -52,7 +53,7 @@ protected function setUp(): void $this->quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() - ->setMethods(['getCustomerId']) + ->addMethods(['getCustomerId']) ->getMock(); $this->quoteRepositoryMock = $this->getMockBuilder(QuoteRepository::class) @@ -63,17 +64,10 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); - $objectManagerHelper = new ObjectManager($this); - $this->accessChangeQuoteControl = $objectManagerHelper->getObject( - AccessChangeQuoteControl::class, - ['changeQuoteControl' => $this->changeQuoteControlMock] - ); + $this->accessChangeQuoteControl = new AccessChangeQuoteControl($this->changeQuoteControlMock); } - /** - * User with role Customer and customer_id matches context user_id. - */ - public function testBeforeSaveForCustomer() + public function testBeforeSaveForCustomerWithCustomerIdMatchinQuoteUserIdIsAllowed() { $this->quoteMock->method('getCustomerId') ->willReturn(1); @@ -84,17 +78,12 @@ public function testBeforeSaveForCustomer() $this->changeQuoteControlMock->method('isAllowed') ->willReturn(true); - $result = $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); - - $this->assertNull($result); + $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); } - /** - * The user_id and customer_id from the quote are different. - */ - public function testBeforeSaveException() + public function testBeforeSaveThrowsExceptionForCustomerWithCustomerIdNotMatchingQuoteUserId() { - $this->expectException('Magento\Framework\Exception\StateException'); + $this->expectException(StateException::class); $this->expectExceptionMessage('Invalid state change requested'); $this->quoteMock->method('getCustomerId') ->willReturn(2); @@ -108,10 +97,7 @@ public function testBeforeSaveException() $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); } - /** - * User with role Admin and customer_id not much with user_id. - */ - public function testBeforeSaveForAdmin() + public function testBeforeSaveForAdminUserRoleIsAllowed() { $this->quoteMock->method('getCustomerId') ->willReturn(2); @@ -122,15 +108,10 @@ public function testBeforeSaveForAdmin() $this->changeQuoteControlMock->method('isAllowed') ->willReturn(true); - $result = $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); - - $this->assertNull($result); + $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); } - /** - * User with role Guest and customer_id === null. - */ - public function testBeforeSaveForGuest() + public function testBeforeSaveForGuestIsAllowed() { $this->quoteMock->method('getCustomerId') ->willReturn(null); @@ -141,17 +122,12 @@ public function testBeforeSaveForGuest() $this->changeQuoteControlMock->method('isAllowed') ->willReturn(true); - $result = $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); - - $this->assertNull($result); + $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); } - /** - * User with role Guest and customer_id !== null. - */ - public function testBeforeSaveForGuestException() + public function testBeforeSaveThrowsExceptionForGuestDoesNotEquals() { - $this->expectException('Magento\Framework\Exception\StateException'); + $this->expectException(StateException::class); $this->expectExceptionMessage('Invalid state change requested'); $this->quoteMock->method('getCustomerId') ->willReturn(1); @@ -165,12 +141,9 @@ public function testBeforeSaveForGuestException() $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); } - /** - * User with unknown role. - */ - public function testBeforeSaveForUnknownUserTypeException() + public function testBeforeSaveThrowsExceptionForUnknownUserType() { - $this->expectException('Magento\Framework\Exception\StateException'); + $this->expectException(StateException::class); $this->expectExceptionMessage('Invalid state change requested'); $this->quoteMock->method('getCustomerId') ->willReturn(2); diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php index d9b797c454d4e..422a6cbcb7bbe 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php @@ -30,7 +30,9 @@ use Magento\Framework\DataObject\Copy; use Magento\Framework\DataObject\Factory; use Magento\Framework\Event\Manager; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Model\Context; +use Magento\Framework\Phrase; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Model\Quote; @@ -640,6 +642,48 @@ public function testGetCustomerTaxClassId() $this->assertEquals($taxClassId, $result); } + /** + * Test case when non-existent customer group is stored into the quote. + * In such a case we should get a NoSuchEntityException exception and try + * to get a valid customer group from the current customer object. + */ + public function testGetCustomerTaxClassIdForNonExistentCustomerGroup() + { + $customerId = 1; + $nonExistentGroupId = 100; + $groupId = 1; + $taxClassId = 1; + $groupMock = $this->getMockForAbstractClass(GroupInterface::class, [], '', false); + $this->groupRepositoryMock->expects($this->at(0)) + ->method('getById') + ->with($nonExistentGroupId) + ->willThrowException(new NoSuchEntityException(new Phrase('Entity Id does not exist'))); + $customerMock = $this->getMockForAbstractClass( + CustomerInterface::class, + [], + '', + false + ); + $customerMock->expects($this->once()) + ->method('getGroupId') + ->willReturn($groupId); + $this->customerRepositoryMock->expects($this->once()) + ->method('getById') + ->with($customerId) + ->willReturn($customerMock); + $this->groupRepositoryMock->expects($this->at(1)) + ->method('getById') + ->with($groupId) + ->willReturn($groupMock); + $groupMock->expects($this->once()) + ->method('getTaxClassId') + ->willReturn($taxClassId); + $this->quote->setData('customer_id', $customerId); + $this->quote->setData('customer_group_id', $nonExistentGroupId); + $result = $this->quote->getCustomerTaxClassId(); + $this->assertEquals($taxClassId, $result); + } + public function testGetAllAddresses() { $id = 1; 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/Quote/etc/graphql/di.xml b/app/code/Magento/Quote/etc/graphql/di.xml new file mode 100644 index 0000000000000..0e688d42ecb32 --- /dev/null +++ b/app/code/Magento/Quote/etc/graphql/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="customizable_option" xsi:type="object">Magento\Quote\Model\Cart\BuyRequest\CustomizableOptionDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/QuoteBundleOptions/Model/Cart/BuyRequest/BundleDataProvider.php b/app/code/Magento/QuoteBundleOptions/Model/Cart/BuyRequest/BundleDataProvider.php new file mode 100644 index 0000000000000..575784c86ace1 --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/Model/Cart/BuyRequest/BundleDataProvider.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteBundleOptions\Model\Cart\BuyRequest; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * Data provider for bundle product buy requests + */ +class BundleDataProvider implements BuyRequestDataProviderInterface +{ + private const OPTION_TYPE = 'bundle'; + + /** + * @inheritdoc + * + * @throws LocalizedException + */ + public function execute(CartItem $cartItem): array + { + $bundleOptionsData = []; + + foreach ($cartItem->getSelectedOptions() as $optionData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $optionId, $optionValueId, $optionQuantity] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } + } + //for bundle options with custom quantity + foreach ($cartItem->getEnteredOptions() as $option) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($option->getUid())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $optionId, $optionValueId] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $optionQuantity = $option->getValue(); + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } + } + + return $bundleOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + if ($optionData[0] !== self::OPTION_TYPE) { + return false; + } + + return true; + } + + /** + * Validates the provided options structure + * + * @param array $optionData + * @throws LocalizedException + */ + private function validateInput(array $optionData): void + { + if (count($optionData) !== 4) { + $errorMessage = __('Wrong format of the entered option data'); + throw new LocalizedException($errorMessage); + } + } +} diff --git a/app/code/Magento/QuoteBundleOptions/README.md b/app/code/Magento/QuoteBundleOptions/README.md new file mode 100644 index 0000000000000..3207eeaf2b683 --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/README.md @@ -0,0 +1,3 @@ +# QuoteBundleOptions + +**QuoteBundleOptions** provides data provider for creating buy request for bundle products. diff --git a/app/code/Magento/QuoteBundleOptions/composer.json b/app/code/Magento/QuoteBundleOptions/composer.json new file mode 100644 index 0000000000000..a2651272018a8 --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-quote-bundle-options", + "description": "Magento module provides data provider for creating buy request for bundle products", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-quote": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\QuoteBundleOptions\\": "" + } + } +} diff --git a/app/code/Magento/QuoteBundleOptions/etc/graphql/di.xml b/app/code/Magento/QuoteBundleOptions/etc/graphql/di.xml new file mode 100644 index 0000000000000..e15493e092e3b --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/etc/graphql/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="bundle" xsi:type="object">Magento\QuoteBundleOptions\Model\Cart\BuyRequest\BundleDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/QuoteBundleOptions/etc/module.xml b/app/code/Magento/QuoteBundleOptions/etc/module.xml new file mode 100644 index 0000000000000..4dc531b561115 --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/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_QuoteBundleOptions"/> +</config> diff --git a/app/code/Magento/QuoteBundleOptions/registration.php b/app/code/Magento/QuoteBundleOptions/registration.php new file mode 100644 index 0000000000000..cf4c92fd929d9 --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_QuoteBundleOptions', __DIR__); diff --git a/app/code/Magento/QuoteConfigurableOptions/Model/Cart/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/QuoteConfigurableOptions/Model/Cart/BuyRequest/SuperAttributeDataProvider.php new file mode 100644 index 0000000000000..d58b574352bd8 --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/Model/Cart/BuyRequest/SuperAttributeDataProvider.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteConfigurableOptions\Model\Cart\BuyRequest; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * DataProvider for building super attribute options in buy requests + */ +class SuperAttributeDataProvider implements BuyRequestDataProviderInterface +{ + private const OPTION_TYPE = 'configurable'; + + /** + * @inheritdoc + * + * @throws LocalizedException + */ + public function execute(CartItem $cartItem): array + { + $configurableProductData = []; + foreach ($cartItem->getSelectedOptions() as $optionData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $attributeId, $valueIndex] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $configurableProductData[$attributeId] = $valueIndex; + } + } + + return ['super_attribute' => $configurableProductData]; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + if ($optionData[0] !== self::OPTION_TYPE) { + return false; + } + + return true; + } + + /** + * Validates the provided options structure + * + * @param array $optionData + * @throws LocalizedException + */ + private function validateInput(array $optionData): void + { + if (count($optionData) !== 3) { + throw new LocalizedException( + __('Wrong format of the entered option data') + ); + } + } +} diff --git a/app/code/Magento/QuoteConfigurableOptions/README.md b/app/code/Magento/QuoteConfigurableOptions/README.md new file mode 100644 index 0000000000000..db47e2c37c3ff --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/README.md @@ -0,0 +1,3 @@ +# QuoteConfigurableOptions + +**QuoteConfigurableOptions** provides data provider for creating buy request for configurable products. diff --git a/app/code/Magento/QuoteConfigurableOptions/composer.json b/app/code/Magento/QuoteConfigurableOptions/composer.json new file mode 100644 index 0000000000000..51d6933d5c6d6 --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-quote-configurable-options", + "description": "Magento module provides data provider for creating buy request for configurable products", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-quote": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\QuoteConfigurableOptions\\": "" + } + } +} diff --git a/app/code/Magento/QuoteConfigurableOptions/etc/graphql/di.xml b/app/code/Magento/QuoteConfigurableOptions/etc/graphql/di.xml new file mode 100644 index 0000000000000..c4fe6357a5689 --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/etc/graphql/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="super_attribute" xsi:type="object">Magento\QuoteConfigurableOptions\Model\Cart\BuyRequest\SuperAttributeDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/QuoteConfigurableOptions/etc/module.xml b/app/code/Magento/QuoteConfigurableOptions/etc/module.xml new file mode 100644 index 0000000000000..e32489c1b2109 --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/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_QuoteConfigurableOptions"/> +</config> diff --git a/app/code/Magento/QuoteConfigurableOptions/registration.php b/app/code/Magento/QuoteConfigurableOptions/registration.php new file mode 100644 index 0000000000000..0b55a18a81fce --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_QuoteConfigurableOptions', __DIR__); diff --git a/app/code/Magento/QuoteDownloadableLinks/Model/Cart/BuyRequest/DownloadableLinkDataProvider.php b/app/code/Magento/QuoteDownloadableLinks/Model/Cart/BuyRequest/DownloadableLinkDataProvider.php new file mode 100644 index 0000000000000..e412c7df573c7 --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/Model/Cart/BuyRequest/DownloadableLinkDataProvider.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteDownloadableLinks\Model\Cart\BuyRequest; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * DataProvider for building downloadable product links in buy requests + */ +class DownloadableLinkDataProvider implements BuyRequestDataProviderInterface +{ + private const OPTION_TYPE = 'downloadable'; + + /** + * @inheritdoc + * + * @throws LocalizedException + */ + public function execute(CartItem $cartItem): array + { + $linksData = []; + + foreach ($cartItem->getSelectedOptions() as $optionData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $linkId] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $linksData[] = $linkId; + } + } + + return ['links' => $linksData]; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + if ($optionData[0] !== self::OPTION_TYPE) { + return false; + } + + return true; + } + + /** + * Validates the provided options structure + * + * @param array $optionData + * @throws LocalizedException + */ + private function validateInput(array $optionData): void + { + if (count($optionData) !== 2) { + throw new LocalizedException( + __('Wrong format of the entered option data') + ); + } + } +} diff --git a/app/code/Magento/QuoteDownloadableLinks/README.md b/app/code/Magento/QuoteDownloadableLinks/README.md new file mode 100644 index 0000000000000..68efffcea6fb8 --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/README.md @@ -0,0 +1,3 @@ +# QuoteDownloadableLinks + +**QuoteDownloadableLinks** provides data provider for creating buy request for links of downloadable products. diff --git a/app/code/Magento/QuoteDownloadableLinks/composer.json b/app/code/Magento/QuoteDownloadableLinks/composer.json new file mode 100644 index 0000000000000..ad120dea96263 --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-quote-downloadable-links", + "description": "Magento module provides data provider for creating buy request for links of downloadable products", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-quote": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\QuoteDownloadableLinks\\": "" + } + } +} diff --git a/app/code/Magento/QuoteDownloadableLinks/etc/graphql/di.xml b/app/code/Magento/QuoteDownloadableLinks/etc/graphql/di.xml new file mode 100644 index 0000000000000..a932d199983a3 --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/etc/graphql/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="downloadable" xsi:type="object">Magento\QuoteDownloadableLinks\Model\Cart\BuyRequest\DownloadableLinkDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/QuoteDownloadableLinks/etc/module.xml b/app/code/Magento/QuoteDownloadableLinks/etc/module.xml new file mode 100644 index 0000000000000..a0cc652ab9188 --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/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_QuoteDownloadableLinks"/> +</config> diff --git a/app/code/Magento/QuoteDownloadableLinks/registration.php b/app/code/Magento/QuoteDownloadableLinks/registration.php new file mode 100644 index 0000000000000..8b766e7fde06c --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_QuoteDownloadableLinks', __DIR__); diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php index 4dbcfad31e84c..51303df345827 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php @@ -16,7 +16,7 @@ use Magento\Quote\Model\ShippingAddressManagementInterface; /** - * Assign shipping address to cart + * Assigning shipping address to cart */ class AssignShippingAddressToCart { @@ -49,7 +49,14 @@ public function execute( try { $this->shippingAddressManagement->assign($cart->getId(), $shippingAddress); } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + if ($cart->getIsVirtual()) { + throw new GraphQlNoSuchEntityException( + __('Shipping address is not allowed on cart: cart contains no items for shipment.'), + $e + ); + } else { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage()), $e); } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index f73daa715c1df..e959c19a7cbe4 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -51,7 +51,10 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s $shippingAddressInput = current($shippingAddressesInput) ?? []; $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; - if (!$customerAddressId && !isset($shippingAddressInput['address']['save_in_address_book'])) { + if (!$customerAddressId + && isset($shippingAddressInput['address']) + && !isset($shippingAddressInput['address']['save_in_address_book']) + ) { $shippingAddressInput['address']['save_in_address_book'] = true; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php index b2526bdc04e98..654a4bb558632 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php @@ -42,12 +42,12 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s } $shippingMethodInput = current($shippingMethodsInput); - if (!isset($shippingMethodInput['carrier_code']) || empty($shippingMethodInput['carrier_code'])) { + if (empty($shippingMethodInput['carrier_code'])) { throw new GraphQlInputException(__('Required parameter "carrier_code" is missing.')); } $carrierCode = $shippingMethodInput['carrier_code']; - if (!isset($shippingMethodInput['method_code']) || empty($shippingMethodInput['method_code'])) { + if (empty($shippingMethodInput['method_code'])) { throw new GraphQlInputException(__('Required parameter "method_code" is missing.')); } $methodCode = $shippingMethodInput['method_code']; diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php new file mode 100644 index 0000000000000..c2e94b215956e --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php @@ -0,0 +1,157 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\GiftMessage\Api\Data\MessageInterface; +use Magento\GiftMessage\Api\Data\MessageInterfaceFactory; +use Magento\GiftMessage\Api\ItemRepositoryInterface; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; +use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem; + +/** + * Class contain update cart items methods + */ +class UpdateCartItems +{ + /** + * @var CartItemRepositoryInterface + */ + private $cartItemRepository; + + /** + * @var UpdateCartItem + */ + private $updateCartItem; + + /** + * @var ItemRepositoryInterface + */ + private $itemRepository; + + /** + * @var GiftMessageHelper + */ + private $giftMessageHelper; + + /** + * @var MessageInterfaceFactory + */ + private $giftMessageFactory; + + /** + * @param CartItemRepositoryInterface $cartItemRepository + * @param UpdateCartItem $updateCartItem + * @param ItemRepositoryInterface $itemRepository + * @param GiftMessageHelper $giftMessageHelper + * @param MessageInterfaceFactory $giftMessageFactory + */ + public function __construct( + CartItemRepositoryInterface $cartItemRepository, + UpdateCartItem $updateCartItem, + ItemRepositoryInterface $itemRepository, + GiftMessageHelper $giftMessageHelper, + MessageInterfaceFactory $giftMessageFactory + ) { + $this->cartItemRepository = $cartItemRepository; + $this->updateCartItem = $updateCartItem; + $this->itemRepository = $itemRepository; + $this->giftMessageHelper = $giftMessageHelper; + $this->giftMessageFactory = $giftMessageFactory; + } + + /** + * Process cart items + * + * @param Quote $cart + * @param array $items + * + * @throws GraphQlInputException + * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function processCartItems(Quote $cart, array $items): void + { + foreach ($items as $item) { + if (empty($item['cart_item_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.')); + } + + $itemId = (int)$item['cart_item_id']; + $customizableOptions = $item['customizable_options'] ?? []; + $cartItem = $cart->getItemById($itemId); + + if ($cartItem && $cartItem->getParentItemId()) { + throw new GraphQlInputException(__('Child items may not be updated.')); + } + + if (count($customizableOptions) === 0 && !isset($item['quantity'])) { + throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); + } + + $quantity = (float)$item['quantity']; + + if ($quantity <= 0.0) { + $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); + } else { + $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions); + } + + if (!empty($item['gift_message'])) { + try { + if (!$this->giftMessageHelper->isMessagesAllowed('items', $cartItem)) { + continue; + } + if (!$this->giftMessageHelper->isMessagesAllowed('item', $cartItem)) { + continue; + } + + /** @var MessageInterface $giftItemMessage */ + $giftItemMessage = $this->itemRepository->get($cart->getEntityId(), $itemId); + + if (empty($giftItemMessage)) { + /** @var MessageInterface $giftMessage */ + $giftMessage = $this->giftMessageFactory->create(); + $this->updateGiftMessageForItem($cart, $giftMessage, $item, $itemId); + continue; + } + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__('Gift Message cannot be updated.')); + } + + $this->updateGiftMessageForItem($cart, $giftItemMessage, $item, $itemId); + } + } + } + + /** + * Update Gift Message for Quote item + * + * @param Quote $cart + * @param MessageInterface $giftItemMessage + * @param array $item + * @param int $itemId + * + * @throws GraphQlInputException + */ + private function updateGiftMessageForItem(Quote $cart, MessageInterface $giftItemMessage, array $item, int $itemId) + { + try { + $giftItemMessage->setRecipient($item['gift_message']['to']); + $giftItemMessage->setSender($item['gift_message']['from']); + $giftItemMessage->setMessage($item['gift_message']['message']); + $this->itemRepository->save($cart->getEntityId(), $giftItemMessage, $itemId); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__('Gift Message cannot be updated')); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php new file mode 100644 index 0000000000000..d5e554f096ec1 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\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\Quote\Model\Cart\AddProductsToCart as AddProductsToCartService; +use Magento\Quote\Model\Cart\Data\AddProductsToCartOutput; +use Magento\Quote\Model\Cart\Data\CartItemFactory; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\Quote\Model\Cart\Data\Error; + +/** + * Resolver for addProductsToCart mutation + * + * @inheritdoc + */ +class AddProductsToCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var AddProductsToCartService + */ + private $addProductsToCartService; + + /** + * @param GetCartForUser $getCartForUser + * @param AddProductsToCartService $addProductsToCart + */ + public function __construct( + GetCartForUser $getCartForUser, + AddProductsToCartService $addProductsToCart + ) { + $this->getCartForUser = $getCartForUser; + $this->addProductsToCartService = $addProductsToCart; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (empty($args['cartId'])) { + throw new GraphQlInputException(__('Required parameter "cartId" is missing')); + } + if (empty($args['cartItems']) || !is_array($args['cartItems']) + ) { + throw new GraphQlInputException(__('Required parameter "cartItems" is missing')); + } + + $maskedCartId = $args['cartId']; + $cartItemsData = $args['cartItems']; + $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); + + // Shopping Cart validation + $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); + + $cartItems = []; + foreach ($cartItemsData as $cartItemData) { + $cartItems[] = (new CartItemFactory())->create($cartItemData); + } + + /** @var AddProductsToCartOutput $addProductsToCartOutput */ + $addProductsToCartOutput = $this->addProductsToCartService->execute($maskedCartId, $cartItems); + + return [ + 'cart' => [ + 'model' => $addProductsToCartOutput->getCart(), + ], + 'user_errors' => array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + 'path' => [$error->getCartItemPosition()] + ]; + }, + $addProductsToCartOutput->getErrors() + ) + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php index 0be95eccc39e5..e8aa8d612c670 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php @@ -7,17 +7,12 @@ namespace Magento\QuoteGraphQl\Model\Resolver; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForCustomer; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; -use Magento\Quote\Api\CartManagementInterface; -use Magento\Quote\Model\QuoteIdMaskFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; +use Magento\Quote\Model\Cart\CustomerCartResolver; /** * Get cart for the customer @@ -25,48 +20,19 @@ class CustomerCart implements ResolverInterface { /** - * @var CreateEmptyCartForCustomer + * @var CustomerCartResolver */ - private $createEmptyCartForCustomer; + private $customerCartResolver; /** - * @var CartManagementInterface - */ - private $cartManagement; - - /** - * @var QuoteIdMaskFactory - */ - private $quoteIdMaskFactory; - - /** - * @var QuoteIdMaskResourceModel - */ - private $quoteIdMaskResourceModel; - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedQuoteId; - - /** - * @param CreateEmptyCartForCustomer $createEmptyCartForCustomer - * @param CartManagementInterface $cartManagement - * @param QuoteIdMaskFactory $quoteIdMaskFactory - * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel - * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + * CustomerCart constructor. + * + * @param CustomerCartResolver $customerCartResolver */ public function __construct( - CreateEmptyCartForCustomer $createEmptyCartForCustomer, - CartManagementInterface $cartManagement, - QuoteIdMaskFactory $quoteIdMaskFactory, - QuoteIdMaskResourceModel $quoteIdMaskResourceModel, - QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + CustomerCartResolver $customerCartResolver ) { - $this->createEmptyCartForCustomer = $createEmptyCartForCustomer; - $this->cartManagement = $cartManagement; - $this->quoteIdMaskFactory = $quoteIdMaskFactory; - $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; - $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; + $this->customerCartResolver = $customerCartResolver; } /** @@ -76,22 +42,17 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value { $currentUserId = $context->getUserId(); - /** @var ContextInterface $context */ + /** + * @var ContextInterface $context + */ if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The request is allowed for logged in customer')); } - try { - $cart = $this->cartManagement->getCartForCustomer($currentUserId); - } catch (NoSuchEntityException $e) { - $this->createEmptyCartForCustomer->execute($currentUserId, null); - $cart = $this->cartManagement->getCartForCustomer($currentUserId); - } - $maskedId = $this->quoteIdToMaskedQuoteId->execute((int) $cart->getId()); - if (empty($maskedId)) { - $quoteIdMask = $this->quoteIdMaskFactory->create(); - $quoteIdMask->setQuoteId((int) $cart->getId()); - $this->quoteIdMaskResourceModel->save($quoteIdMask); + try { + $cart = $this->customerCartResolver->resolve($currentUserId); + } catch (\Exception $e) { + $cart = null; } return [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php index dd4ce8fe7f7a6..a2ac94a0f28cc 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php @@ -22,7 +22,7 @@ /** * Resolver for setting payment method and placing order * - * @deprecated Should use setPaymentMethodOnCart and placeOrder mutations in single request. + * @deprecated 100.3.4 Should use setPaymentMethodOnCart and placeOrder mutations in single request. * @see \Magento\QuoteGraphQl\Model\Resolver\SetPaymentMethodOnCart * @see \Magento\QuoteGraphQl\Model\Resolver\PlaceOrder */ @@ -71,14 +71,15 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + if (empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } - $maskedCartId = $args['input']['cart_id']; - if (!isset($args['input']['payment_method']['code']) || empty($args['input']['payment_method']['code'])) { + if (empty($args['input']['payment_method']['code'])) { throw new GraphQlInputException(__('Required parameter "code" for "payment_method" is missing.')); } + + $maskedCartId = $args['input']['cart_id']; $paymentData = $args['input']['payment_method']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php index fa90f08e4b553..005baaad0e1e5 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -14,53 +14,43 @@ use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Quote\Api\CartItemRepositoryInterface; use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Quote\Model\Quote; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; -use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\UpdateCartItems as UpdateCartItemsProvider; /** * @inheritdoc */ class UpdateCartItems implements ResolverInterface { - /** - * @var UpdateCartItem - */ - private $updateCartItem; - /** * @var GetCartForUser */ private $getCartForUser; /** - * @var CartItemRepositoryInterface + * @var CartRepositoryInterface */ - private $cartItemRepository; + private $cartRepository; /** - * @var CartRepositoryInterface + * @var UpdateCartItemsProvider */ - private $cartRepository; + private $updateCartItems; /** - * @param GetCartForUser $getCartForUser - * @param CartItemRepositoryInterface $cartItemRepository - * @param UpdateCartItem $updateCartItem + * @param GetCartForUser $getCartForUser * @param CartRepositoryInterface $cartRepository + * @param UpdateCartItemsProvider $updateCartItems */ public function __construct( GetCartForUser $getCartForUser, - CartItemRepositoryInterface $cartItemRepository, - UpdateCartItem $updateCartItem, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + UpdateCartItemsProvider $updateCartItems ) { $this->getCartForUser = $getCartForUser; - $this->cartItemRepository = $cartItemRepository; - $this->updateCartItem = $updateCartItem; $this->cartRepository = $cartRepository; + $this->updateCartItems = $updateCartItems; } /** @@ -71,6 +61,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); } + $maskedCartId = $args['input']['cart_id']; if (empty($args['input']['cart_items']) @@ -78,13 +69,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value ) { throw new GraphQlInputException(__('Required parameter "cart_items" is missing.')); } - $cartItems = $args['input']['cart_items']; + $cartItems = $args['input']['cart_items']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); try { - $this->processCartItems($cart, $cartItems); + $this->updateCartItems->processCartItems($cart, $cartItems); $this->cartRepository->save($cart); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); @@ -98,39 +89,4 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value ], ]; } - - /** - * Process cart items - * - * @param Quote $cart - * @param array $items - * @throws GraphQlInputException - * @throws LocalizedException - */ - private function processCartItems(Quote $cart, array $items): void - { - foreach ($items as $item) { - if (empty($item['cart_item_id'])) { - throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.')); - } - $itemId = (int)$item['cart_item_id']; - $customizableOptions = $item['customizable_options'] ?? []; - - $cartItem = $cart->getItemById($itemId); - if ($cartItem && $cartItem->getParentItemId()) { - throw new GraphQlInputException(__('Child items may not be updated.')); - } - - if (count($customizableOptions) === 0 && !isset($item['quantity'])) { - throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); - } - $quantity = (float)$item['quantity']; - - if ($quantity <= 0.0) { - $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); - } else { - $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions); - } - } - } } diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json index 0652d39b5f426..25f089cf75a62 100644 --- a/app/code/Magento/QuoteGraphQl/composer.json +++ b/app/code/Magento/QuoteGraphQl/composer.json @@ -13,7 +13,8 @@ "magento/module-customer-graph-ql": "*", "magento/module-sales": "*", "magento/module-directory": "*", - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "*", + "magento/module-gift-message": "*" }, "suggest": { "magento/module-graph-ql-cache": "*" diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 955ee1cc2429a..4e0e7ce5732be 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -22,6 +22,7 @@ type Mutation { setPaymentMethodAndPlaceOrder(input: SetPaymentMethodAndPlaceOrderInput): PlaceOrderOutput @deprecated(reason: "Should use setPaymentMethodOnCart and placeOrder mutations in single request.") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentAndPlaceOrder") mergeCarts(source_cart_id: String!, destination_cart_id: String!): Cart! @doc(description:"Merges the source cart into the destination cart") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\MergeCarts") placeOrder(input: PlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\PlaceOrder") + addProductsToCart(cartId: String!, cartItems: [CartItemInput!]!): AddProductsToCartOutput @doc(description:"Add any type of product to the cart") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddProductsToCart") } input createEmptyCartInput { @@ -51,6 +52,9 @@ input VirtualProductCartItemInput { input CartItemInput { sku: String! quantity: Float! + parent_sku: String @doc(description: "For child products, the SKU of its parent product") + selected_options: [ID!] @doc(description: "The selected options for the base product, such as color or size") + entered_options: [EnteredOptionInput!] @doc(description: "An array of entered options for the base product, such as personalization text") } input CustomizableOptionInput { @@ -368,3 +372,21 @@ type Order { order_number: String! order_id: String @deprecated(reason: "The order_id field is deprecated, use order_number instead.") } + +type CartUserInputError @doc(description:"An error encountered while adding an item to the the cart.") { + message: String! @doc(description: "A localized error message") + code: CartUserInputErrorType! @doc(description: "Cart-specific error code") +} + +type AddProductsToCartOutput { + cart: Cart! @doc(description: "The cart after products have been added") + user_errors: [CartUserInputError!]! @doc(description: "An error encountered while adding an item to the cart.") +} + +enum CartUserInputErrorType { + PRODUCT_NOT_FOUND + NOT_SALABLE + INSUFFICIENT_STOCK + UNDEFINED +} + 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/Reports/Block/Adminhtml/Wishlist.php b/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php index dd42874b55795..257eb481e1923 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php @@ -9,7 +9,7 @@ /** * Adminhtml wishlist report page content block * - * @deprecated + * @deprecated 100.3.3 * @author Magento Core Team <core@magentocommerce.com> */ class Wishlist extends \Magento\Backend\Block\Template diff --git a/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php b/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php index 12959f083d376..1e3eb12331bde 100644 --- a/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php +++ b/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php @@ -8,7 +8,7 @@ /** * Reports Recently Viewed Products Widget * - * @deprecated + * @deprecated 100.3.3 * @author Magento Core Team <core@magentocommerce.com> */ class Item extends \Magento\Catalog\Block\Product\AbstractProduct implements \Magento\Widget\Block\BlockInterface diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index 0a74c23fad991..44571550459c2 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -818,7 +818,7 @@ public function addSumAvgTotals($storeId = 0) * @param string $baseSubtotalCanceled * @param string $baseDiscountCanceled * @return string - * @deprecated + * @deprecated 100.3.2 * @see getTotalsExpressionWithDiscountRefunded */ protected function getTotalsExpression( diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php index d194526858cde..ed3e4e8c4446d 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php @@ -100,6 +100,7 @@ public function addFieldToFilter($field, $condition = null) /** * @inheritDoc + * @since 100.3.2 */ public function getSelectCountSql() { diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php index c1c6fb2eaed88..b69ea94aac9bb 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php @@ -58,6 +58,7 @@ public function __construct( * @param array $storeIds * @param bool $withAdmin * @return $this + * @since 100.3.1 */ public function addStoreFilter(array $storeIds, $withAdmin = true) { diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php index d219aefe81d45..16df2d30db40d 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Reports\Model\ResourceModel\Quote\Item; @@ -17,6 +18,8 @@ */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { + private const PREPARED_FLAG_NAME = 'reports_collection_prepared'; + /** * Join fields * @@ -99,6 +102,11 @@ protected function _construct() public function prepareActiveCartItems() { $quoteItemsSelect = $this->getSelect(); + + if ($this->getFlag(self::PREPARED_FLAG_NAME)) { + return $quoteItemsSelect; + } + $quoteItemsSelect->reset() ->from(['main_table' => $this->getTable('quote_item')], '') ->columns('main_table.product_id') @@ -114,6 +122,7 @@ public function prepareActiveCartItems() )->group( 'main_table.product_id' ); + $this->setFlag(self::PREPARED_FLAG_NAME, true); return $quoteItemsSelect; } diff --git a/app/code/Magento/Reports/Model/ResourceModel/Review/Customer/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Review/Customer/Collection.php index f37bd6c6a7bd2..1ee47f3cd7bbb 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Review/Customer/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Review/Customer/Collection.php @@ -113,6 +113,7 @@ protected function _joinCustomers() * * Additional processing of 'customer_name' field is required, as it is a concat field, which can not be aliased. * @see _joinCustomers + * @since 100.2.2 */ public function addFieldToFilter($field, $condition = null) { diff --git a/app/code/Magento/Reports/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Review/Product/Collection.php index 6f7738a8273bb..69221af3322f0 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Review/Product/Collection.php @@ -106,6 +106,7 @@ public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) * @param array|null $condition * @param string $joinType * @return $this|\Magento\Catalog\Model\ResourceModel\Product\Collection + * @since 100.3.5 */ public function addAttributeToFilter($attribute, $condition = null, $joinType = 'inner') { diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml index a96e74c8ad166..beb1471bd6c4d 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml @@ -45,7 +45,7 @@ <argument name="attribute" value="colorProductAttribute"/> <argument name="option" value="colorProductAttribute1"/> </actionGroup> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitFirstOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitFirstOrder" /> <!--Add second configurable product to order--> <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToSecondOrderWithExistingCustomer"> @@ -56,7 +56,7 @@ <argument name="attribute" value="colorProductAttribute"/> <argument name="option" value="colorProductAttribute2"/> </actionGroup> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitSecondOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitSecondOrder" /> <!-- Get date --> <generateDate stepKey="generateStartDate" date="-1 minute" format="m/d/Y"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml index e572febec5a5c..6e9e8e800e076 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml @@ -47,10 +47,9 @@ <argument name="customer" value="$$createCustomer$$"/> </actionGroup> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceAction"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seePageNameNewInvoicePage"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeOrderShipmentUrl"/> <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php index ea8fcfbb77132..6e7d5bdce16f5 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php @@ -15,6 +15,7 @@ use Magento\Framework\Event\ManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Model\ResourceModel\Quote; +use Magento\Reports\Model\ResourceModel\Quote\Collection; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -42,7 +43,7 @@ protected function setUp(): void public function testGetSelectCountSql() { /** @var MockObject $collection */ - $collection = $this->getMockBuilder(\Magento\Reports\Model\ResourceModel\Quote\Collection::class) + $collection = $this->getMockBuilder(Collection::class) ->setMethods(['getSelect']) ->disableOriginalConstructor() ->getMock(); @@ -62,12 +63,12 @@ public function testPrepareActiveCartItems() $constructArgs = $this->objectManager ->getConstructArguments(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class); $collection = $this->getMockBuilder(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class) - ->setMethods(['getSelect', 'getTable']) + ->setMethods(['getSelect', 'getTable', 'getFlag', 'setFlag']) ->disableOriginalConstructor() ->setConstructorArgs($constructArgs) ->getMock(); - $collection->expects($this->once())->method('getSelect')->willReturn($this->selectMock); + $collection->expects($this->exactly(2))->method('getSelect')->willReturn($this->selectMock); $this->selectMock->expects($this->once())->method('reset')->willReturnSelf(); $this->selectMock->expects($this->once())->method('from')->willReturnSelf(); $this->selectMock->expects($this->atLeastOnce())->method('columns')->willReturnSelf(); @@ -75,7 +76,12 @@ public function testPrepareActiveCartItems() $this->selectMock->expects($this->once())->method('where')->willReturnSelf(); $this->selectMock->expects($this->once())->method('group')->willReturnSelf(); $collection->expects($this->exactly(2))->method('getTable')->willReturn('table'); + $collection->expects($this->once())->method('setFlag') + ->with('reports_collection_prepared')->willReturnSelf(); $collection->prepareActiveCartItems(); + $collection->method('getFlag') + ->with('reports_collection_prepared')->willReturn(true); + $this->assertEquals($this->selectMock, $collection->prepareActiveCartItems()); } public function testLoadWithFilter() diff --git a/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml b/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml index 81453a5a17ad2..4f6e3c4a9a02b 100644 --- a/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml +++ b/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml @@ -5,19 +5,23 @@ */ ?> <?php -/** @var $block \Magento\Reports\Block\Adminhtml\Grid */ +/** + * @var $block \Magento\Reports\Block\Adminhtml\Grid + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->getCollection()) : ?> - <?php if ($block->canDisplayContainer()) : ?> +<?php if ($block->getCollection()): ?> + <?php if ($block->canDisplayContainer()): ?> <div id="<?= $block->escapeHtmlAttr($block->getId()) ?>"> - <?php else : ?> + <?php else: ?> <?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> <?php endif; ?> - <?php if ($block->getStoreSwitcherVisibility() || $block->getDateFilterVisibility()) : ?> + <?php if ($block->getStoreSwitcherVisibility() || $block->getDateFilterVisibility()): ?> <div class="admin__data-grid-header admin__data-grid-toolbar"> <div class="admin__data-grid-header-row"> - <?php if ($block->getDateFilterVisibility()) : ?> - <div class="admin__filter-actions" data-role="filter-form" id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_range')) ?>"> + <?php if ($block->getDateFilterVisibility()): ?> + <div class="admin__filter-actions" data-role="filter-form" + id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_range')) ?>"> <span class="field-row"> <label for="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from')) ?>" class="admin__control-support-text"> @@ -28,7 +32,8 @@ id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from')) ?>" name="report_from" value="<?= $block->escapeHtmlAttr($block->getFilter('report_from')) ?>"> - <span id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from_advice')) ?>"></span> + <span id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from_advice'))?>"> + </span> </span> <span class="field-row"> @@ -41,7 +46,8 @@ id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_to')) ?>" name="report_to" value="<?= $block->escapeHtmlAttr($block->getFilter('report_to')) ?>"/> - <span id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_to_advice')) ?>"></span> + <span id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_to_advice')) ?>"> + </span> </span> <span class="field-row admin__control-filter"> @@ -49,34 +55,43 @@ class="admin__control-support-text"> <span><?= $block->escapeHtml(__('Show By')) ?>:</span> </label> - <select name="report_period" id="<?= $block->escapeHtmlAttr($block->getSuffixId('report_period')) ?>" class="admin__control-select"> - <?php foreach ($block->getPeriods() as $_value => $_label) : ?> - <option value="<?= $block->escapeHtmlAttr($_value) ?>" <?php if ($block->getFilter('report_period') == $_value) : ?> selected<?php endif; ?>><?= $block->escapeHtml($_label) ?></option> + <select name="report_period" + id="<?= $block->escapeHtmlAttr($block->getSuffixId('report_period')) ?>" + class="admin__control-select"> + <?php foreach ($block->getPeriods() as $_value => $_label): ?> + <option value="<?= $block->escapeHtmlAttr($_value) ?>" + <?php if ($block->getFilter('report_period') == $_value): + ?> selected<?php endif; ?>><?= $block->escapeHtml($_label) ?> + </option> <?php endforeach; ?> </select> <?= $block->getRefreshButtonHtml() ?> </span> - <script> + <?php $scriptString = <<<script + require([ "jquery", "mage/calendar" ], function($){ - $("#<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_range'))) ?>").dateRange({ - dateFormat:"<?= $block->escapeJs($block->escapeHtml($block->getDateFormat())) ?>", - buttonText:"<?= $block->escapeJs($block->escapeHtml(__('Select Date'))) ?>", + $("#{$block->escapeJs($block->getSuffixId('period_date_range'))}").dateRange({ + dateFormat:"{$block->escapeJs($block->getDateFormat())}", + buttonText:"{$block->escapeJs(__('Select Date'))}", from:{ - id:"<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from'))) ?>" + id:"{$block->escapeJs($block->getSuffixId('period_date_from'))}" }, to:{ - id:"<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to'))) ?>" + id:"{$block->escapeJs($block->getSuffixId('period_date_to'))}" } }); }); - </script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> <?php endif; ?> - <?php if ($block->getChildBlock('grid.export')) : ?> + <?php if ($block->getChildBlock('grid.export')): ?> <?= $block->getChildHtml('grid.export') ?> <?php endif; ?> </div> @@ -88,8 +103,13 @@ </table> </div> </div> - <?php if ($block->canDisplayContainer()) : ?> - <script> + <?php if ($block->canDisplayContainer()): ?> + <?php $useAjax = ''; + if ($block->getUseAjax()): + $useAjax = $block->escapeJs($block->getUseAjax()); + endif; + $scriptString = <<<script + require([ "jquery", "validation", @@ -98,16 +118,24 @@ ], function(jQuery){ //<![CDATA[ - <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?> = new varienGrid('<?= $block->escapeJs($block->escapeHtml($block->getId())) ?>', '<?= $block->escapeJs($block->escapeUrl($block->getGridUrl())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNamePage())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNameSort())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNameDir())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNameFilter())) ?>'); - <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>.useAjax = '<?php if ($block->getUseAjax()) : - echo $block->escapeJs($block->escapeHtml($block->getUseAjax())); - endif; ?>'; - <?php if ($block->getDateFilterVisibility()) : ?> - <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>.doFilterCallback = validateFilterDate; - var period_date_from = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from'))) ?>'); - var period_date_to = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to'))) ?>'); - period_date_from.adviceContainer = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from_advice'))) ?>'); - period_date_to.adviceContainer = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to_advice'))) ?>'); + {$block->escapeJs($block->getJsObjectName())} = new varienGrid('{$block->escapeJs($block->getId())}', + '{$block->escapeJs($block->getGridUrl())}', '{$block->escapeJs($block->getVarNamePage())}', + '{$block->escapeJs($block->getVarNameSort())}', '{$block->escapeJs($block->getVarNameDir())}', + '{$block->escapeJs($block->getVarNameFilter())}'); + {$block->escapeJs($block->getJsObjectName())}.useAjax = '{$useAjax}'; + +script; + ?> + <?php if ($block->getDateFilterVisibility()): ?> + <?php $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())}.doFilterCallback = validateFilterDate; + var period_date_from = $('{$block->escapeJs($block->getSuffixId('period_date_from'))}'); + var period_date_to = $('{$block->escapeJs($block->getSuffixId('period_date_to'))}'); + period_date_from.adviceContainer = + $('{$block->escapeJs($block->getSuffixId('period_date_from_advice'))}'); + period_date_to.adviceContainer = + $('{$block->escapeJs($block->getSuffixId('period_date_to_advice'))}'); var validateFilterDate = function() { if (period_date_from && period_date_to) { @@ -121,8 +149,13 @@ return true; } } + +script; + ?> <?php endif;?> - <?php if ($block->getStoreSwitcherVisibility()) : ?> + <?php if ($block->getStoreSwitcherVisibility()): ?> + <?php $scriptString .= <<<script + /* Overwrite function from switcher.phtml widget*/ switchStore = function(obj) { if (obj.options[obj.selectedIndex].getAttribute('website') == 'true') { @@ -136,9 +169,12 @@ if (obj.switchParams) { storeParam += obj.switchParams; } - var formParam = new Array('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from'))) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to'))) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('report_period'))) ?>'); + var formParam = new Array('{$block->escapeJs($block->getSuffixId('period_date_from'))}', + '{$block->escapeJs($block->getSuffixId('period_date_to'))}', + '{$block->escapeJs($block->getSuffixId('report_period'))}'); var paramURL = ''; - var switchURL = '<?= $block->escapeUrl($block->getAbsoluteGridUrl(['_current' => false])) ?>'.replace(/(store|group|website)\/\d+\//, ''); + var switchURL = '{$block->escapeJs($block->getAbsoluteGridUrl(['_current' => false]))}' + .replace(/(store|group|website)\/\d+\//, ''); for (var i = 0; i < formParam.length; i++) { if ($(formParam[i]).value && $(formParam[i]).name) { @@ -147,10 +183,18 @@ } setLocation(switchURL + storeParam + '?' + paramURL); } + +script; + ?> <?php endif; ?> + <?php $scriptString .= <<<script + //]]> }); - </script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml b/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml index 85145454428e2..1d3471a877387 100644 --- a/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml +++ b/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="reports-content"> @@ -11,7 +13,8 @@ <?= $block->getGridHtml() ?> -<script> +<?php $scriptString = <<<script + require([ 'jquery', 'mage/backend/validation', @@ -21,7 +24,7 @@ require([ //<![CDATA[ jQuery('#filter_form').mage('validation', {errorClass: 'mage-error'}); function filterFormSubmit() { - var filters = $$('#filter_form input', '#filter_form select'), + var filters = \$$('#filter_form input', '#filter_form select'), elements = []; for (var i in filters) { @@ -31,7 +34,7 @@ require([ } if (jQuery('#filter_form').valid()) { - setLocation('<?= $block->escapeJs($block->escapeUrl($block->getFilterUrl())) ?>filter/'+ + setLocation('{$block->escapeJs($block->getFilterUrl())}filter/'+ Base64.encode(Form.serializeElements(elements))+'/' ); } @@ -39,4 +42,7 @@ require([ //]]> window.filterFormSubmit = filterFormSubmit; }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> 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/Review/Block/Adminhtml/Edit/Tab/Reviews.php b/app/code/Magento/Review/Block/Adminhtml/Edit/Tab/Reviews.php index 15d41fad0a595..bf3c0e5b82ccb 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit/Tab/Reviews.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit/Tab/Reviews.php @@ -13,6 +13,7 @@ * Review tab in adminhtml area. * * @api + * @since 100.4.0 */ class Reviews extends Grid { @@ -20,6 +21,7 @@ class Reviews extends Grid * Hide grid mass action elements. * * @return Reviews + * @since 100.4.0 */ protected function _prepareMassaction() { @@ -30,6 +32,7 @@ protected function _prepareMassaction() * Determine ajax url for grid refresh * * @return string + * @since 100.4.0 */ public function getGridUrl() { diff --git a/app/code/Magento/Review/Block/Customer/ListCustomer.php b/app/code/Magento/Review/Block/Customer/ListCustomer.php index eb67af5780ddb..282421401b674 100644 --- a/app/code/Magento/Review/Block/Customer/ListCustomer.php +++ b/app/code/Magento/Review/Block/Customer/ListCustomer.php @@ -7,12 +7,15 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Review\Helper\Data as ReviewHelper; /** * Customer Reviews list block * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ListCustomer extends \Magento\Customer\Block\Account\Dashboard { @@ -44,6 +47,7 @@ class ListCustomer extends \Magento\Customer\Block\Account\Dashboard * @param \Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory $collectionFactory * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer * @param array $data + * @param ReviewHelper|null $reviewHelper */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -53,9 +57,11 @@ public function __construct( AccountManagementInterface $customerAccountManagement, \Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory $collectionFactory, \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, - array $data = [] + array $data = [], + ?ReviewHelper $reviewHelper = null ) { $this->_collectionFactory = $collectionFactory; + $data['reviewHelper'] = $reviewHelper ?? ObjectManager::getInstance()->get(ReviewHelper::class); parent::__construct( $context, $customerSession, diff --git a/app/code/Magento/Review/Block/Customer/View.php b/app/code/Magento/Review/Block/Customer/View.php index da5aff1f4d2f8..bb322f17b6ce9 100644 --- a/app/code/Magento/Review/Block/Customer/View.php +++ b/app/code/Magento/Review/Block/Customer/View.php @@ -161,7 +161,7 @@ public function getRating() /** * Get rating summary * - * @deprecated + * @deprecated 100.3.3 * @return array */ public function getRatingSummary() diff --git a/app/code/Magento/Review/Block/View.php b/app/code/Magento/Review/Block/View.php index 82a5f37f9b6bf..fcfa11faa169d 100644 --- a/app/code/Magento/Review/Block/View.php +++ b/app/code/Magento/Review/Block/View.php @@ -119,7 +119,7 @@ public function getRating() /** * Retrieve rating summary for current product * - * @deprecated + * @deprecated 100.3.3 * @return string */ public function getRatingSummary() diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating.php b/app/code/Magento/Review/Controller/Adminhtml/Rating.php index 02649661154af..672c3ed327941 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating.php @@ -41,7 +41,7 @@ public function __construct( } /** - * @deprecated Misspelled method + * @deprecated 100.3.0 Misspelled method * @see initEntityId */ protected function initEnityId() diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php index ab264ef1b6179..1fb7e7df2461f 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php @@ -553,6 +553,7 @@ protected function _afterLoad() * Not add store ids to items * * @return $this + * @since 100.2.8 */ protected function prepareStoreId() { diff --git a/app/code/Magento/Review/Model/Review.php b/app/code/Magento/Review/Model/Review.php index 0c581f570ef0c..f2e0997ea8878 100644 --- a/app/code/Magento/Review/Model/Review.php +++ b/app/code/Magento/Review/Model/Review.php @@ -101,7 +101,7 @@ class Review extends \Magento\Framework\Model\AbstractModel implements IdentityI /** * Review model summary * - * @deprecated Summary factory injected as separate property + * @deprecated 100.3.3 Summary factory injected as separate property * @var \Magento\Review\Model\Review\Summary */ protected $_reviewSummary; @@ -216,7 +216,7 @@ public function aggregate() /** * Get entity summary * - * @deprecated + * @deprecated 100.3.3 * @param Product $product * @param int $storeId * @return void @@ -306,7 +306,7 @@ public function afterDeleteCommit() /** * Append review summary data object to product collection * - * @deprecated + * @deprecated 100.3.3 * @param ProductCollection $collection * @return $this * @throws \Magento\Framework\Exception\NoSuchEntityException diff --git a/app/code/Magento/Review/Model/Review/Config.php b/app/code/Magento/Review/Model/Review/Config.php new file mode 100644 index 0000000000000..a3082503b1391 --- /dev/null +++ b/app/code/Magento/Review/Model/Review/Config.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Model\Review; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Provides reviews configuration + */ +class Config +{ + const XML_PATH_REVIEW_ACTIVE = 'catalog/review/active'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check whether the reviews are enabled or not + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_REVIEW_ACTIVE, + ScopeInterface::SCOPE_STORES + ); + } +} diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml index 8a2f441e5c4e8..4fc316b000c17 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml @@ -28,11 +28,15 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Review/view/adminhtml/templates/add.phtml b/app/code/Magento/Review/view/adminhtml/templates/add.phtml index 83017eec57013..ec017fa36a33c 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/add.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/add.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> <?= $block->getBackButtonHtml() ?> @@ -14,7 +16,8 @@ <div class="hidden" id="formContainer"> <?= $block->getFormHtml() ?> </div> -<script> +<?php $scriptString = <<<script + require([ "jquery", "mage/mage", @@ -25,4 +28,7 @@ require([ $('#edit_form').mage('form').mage('validation'); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml b/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml index bf0cab4c621f5..8bacccef869e2 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml @@ -4,26 +4,37 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Adminhtml\Rating\Detailed $block */ +/** + * @var \Magento\Review\Block\Adminhtml\Rating\Detailed $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->getRating() && $block->getRating()->getSize()) : ?> - <?php foreach ($block->getRating() as $_rating) : ?> +<?php if ($block->getRating() && $block->getRating()->getSize()): ?> + <?php foreach ($block->getRating() as $_rating): ?> <div class="admin__field admin__field-rating"> <label class="admin__field-label"><span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span></label> <?php $_iterator = 1; ?> <?php $_options = ($_rating->getRatingOptions()) ? $_rating->getRatingOptions() : $_rating->getOptions() ?> <div class="admin__field-control" data-widget="ratingControl"> - <?php foreach (array_reverse($_options) as $_option) : ?> - <input type="radio" name="ratings[<?= $block->escapeHtmlAttr($_rating->getVoteId() ? $_rating->getVoteId() : $_rating->getId()) ?>]" id="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>" value="<?= $block->escapeHtmlAttr($_option->getId()) ?>" <?php if ($block->isSelected($_option, $_rating)) : ?>checked="checked"<?php endif; ?> /> - <label for="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>">★</label> + <?php foreach (array_reverse($_options) as $_option): ?> + <input type="radio" + name="ratings[<?= $block->escapeHtmlAttr($_rating->getVoteId() ? $_rating->getVoteId() : + $_rating->getId()) ?>]" + id="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) + ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>" + value="<?= $block->escapeHtmlAttr($_option->getId()) ?>" + <?php if ($block->isSelected($_option, $_rating)): ?>checked="checked"<?php endif; ?> /> + <label for="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) + ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>">★</label> <?php $_iterator++ ?> <?php endforeach; ?> </div> </div> <?php endforeach; ?> <input type="hidden" name="validate_rating" class="validate-rating" value="" /> -<script> + <?php $scriptString = <<<script + require([ "jquery", "mage/mage", @@ -33,7 +44,10 @@ require([ $('[data-widget=ratingControl]').ratingControl(); }); -</script> -<?php else : ?> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<?php else: ?> <?= $block->escapeHtml(__("Rating isn't Available")) ?> <?php endif; ?> diff --git a/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml b/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml index 1f27db795f8c9..22ddf532b6926 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml @@ -4,12 +4,20 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Adminhtml\Rating\Summary $block */ +/** + * @var \Magento\Review\Block\Adminhtml\Rating\Summary $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->getRatingSummary()->getCount()) : ?> +<?php if ($block->getRatingSummary()->getCount()): ?> <div class="rating-box"> - <div class="rating" style="width:<?= /* @noEscape */ ceil($block->getRatingSummary()->getSum() / ($block->getRatingSummary()->getCount())) ?>%;"></div> + <div class="rating"></div> </div> -<?php else : ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . /* @noEscape */ ceil($block->getRatingSummary()->getSum() / + ($block->getRatingSummary()->getCount())) . "%;", + 'div.rating-box div.rating' + ) ?> +<?php else: ?> <?= $block->escapeHtml(__("Rating isn't Available")) ?> <?php endif; ?> diff --git a/app/code/Magento/Review/view/frontend/templates/customer/list.phtml b/app/code/Magento/Review/view/frontend/templates/customer/list.phtml index 11ea987b74cec..6dd7aa575e9df 100644 --- a/app/code/Magento/Review/view/frontend/templates/customer/list.phtml +++ b/app/code/Magento/Review/view/frontend/templates/customer/list.phtml @@ -4,9 +4,15 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Customer\ListCustomer $block */ +/** + * @var \Magento\Review\Block\Customer\ListCustomer $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Review\Helper\Data $reviewHelper */ +$reviewHelper = $block->getData('reviewHelper'); ?> -<?php if ($block->getReviews() && count($block->getReviews())) : ?> +<?php if ($block->getReviews() && count($block->getReviews())): ?> <div class="table-wrapper reviews"> <table class="data table table-reviews" id="my-reviews-table"> <caption class="table-caption"><?= $block->escapeHtml(__('Product Reviews')) ?></caption> @@ -20,26 +26,39 @@ </tr> </thead> <tbody> - <?php foreach ($block->getReviews() as $review) : ?> + <?php foreach ($block->getReviews() as $review): ?> <tr> - <td data-th="<?= $block->escapeHtml(__('Created')) ?>" class="col date"><?= $block->escapeHtml($block->dateFormat($review->getReviewCreatedAt())) ?></td> + <td data-th="<?= $block->escapeHtml(__('Created')) ?>" + class="col date"><?= $block->escapeHtml($block->dateFormat($review->getReviewCreatedAt())) ?> + </td> <td data-th="<?= $block->escapeHtml(__('Product Name')) ?>" class="col item"> <strong class="product-name"> - <a href="<?= $block->escapeUrl($block->getProductUrl($review)) ?>"><?= $block->escapeHtml($review->getName()) ?></a> + <a href="<?= $block->escapeUrl($block->getProductUrl($review)) ?>"> + <?= $block->escapeHtml($review->getName()) ?> + </a> </strong> </td> <td data-th="<?= $block->escapeHtml(__('Rating')) ?>" class="col summary"> - <?php if ($review->getSum()) : ?> + <?php if ($review->getSum()): ?> <div class="rating-summary"> <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> - <div class="rating-result" title="<?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%"> - <span style="width:<?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%;"><span><?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%</span></span> + <div class="rating-result" + title="<?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%"> + <span> + <span> + <?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>% + </span> + </span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) . "%;", + 'div.rating-summary div.rating-result>span:first-child' + ) ?> <?php endif; ?> </td> <td data-th="<?= $block->escapeHtmlAttr(__('Review')) ?>" class="col description"> - <?= $this->helper(\Magento\Review\Helper\Data::class)->getDetailHtml($review->getDetail()) ?> + <?= $reviewHelper->getDetailHtml($review->getDetail()) ?> </td> <td data-th="<?= $block->escapeHtmlAttr(__('Actions')) ?>" class="col actions"> <a href="<?= $block->escapeUrl($block->getReviewUrl($review)) ?>" class="action more"> @@ -51,12 +70,12 @@ </tbody> </table> </div> - <?php if ($block->getToolbarHtml()) : ?> + <?php if ($block->getToolbarHtml()): ?> <div class="toolbar products-reviews-toolbar bottom"> <?= $block->getToolbarHtml() ?> </div> <?php endif; ?> -<?php else : ?> +<?php else: ?> <div class="message info empty"><span><?= $block->escapeHtml(__('You have submitted no reviews.')) ?></span></div> <?php endif; ?> <div class="actions-toolbar"> diff --git a/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml b/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml index 5cd81a2f17cbc..cf7d53e818c36 100644 --- a/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml +++ b/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml @@ -4,26 +4,41 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Customer\Recent $block */ +/** + * @var \Magento\Review\Block\Customer\Recent $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->getReviews() && count($block->getReviews())) : ?> +<?php if ($block->getReviews() && count($block->getReviews())): ?> <div class="block block-reviews-dashboard"> <div class="block-title"> <strong><?= $block->escapeHtml(__('My Recent Reviews')) ?></strong> - <a class="action view" href="<?= $block->escapeUrl($block->getAllReviewsUrl()) ?>"><span><?= $block->escapeHtml(__('View All')) ?></span></a> + <a class="action view" href="<?= $block->escapeUrl($block->getAllReviewsUrl()) ?>"> + <span><?= $block->escapeHtml(__('View All')) ?></span> + </a> </div> <div class="block-content"> <ol class="items"> - <?php foreach ($block->getReviews() as $_review) : ?> + <?php foreach ($block->getReviews() as $_review): ?> <li class="item"> - <strong class="product-name"><a href="<?= $block->escapeUrl($block->getReviewUrl($_review->getReviewId())) ?>"><?= $block->escapeHtml($_review->getName()) ?></a></strong> - <?php if ($_review->getSum()) : ?> + <strong class="product-name"> + <a href="<?= $block->escapeUrl($block->getReviewUrl($_review->getReviewId())) ?>"> + <?= $block->escapeHtml($_review->getName()) ?> + </a> + </strong> + <?php if ($_review->getSum()): ?> <?php $rating = $_review->getSum() / $_review->getCount() ?> <div class="rating-summary"> <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating) ?>%"> - <span style="width:<?= $block->escapeHtmlAttr($rating) ?>%"><span><?= $block->escapeHtml($rating) ?>%</span></span> + <span> + <span><?= $block->escapeHtml($rating) ?>%</span> + </span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:". $block->escapeHtmlAttr($rating) . "%", + 'div.rating-result>span:first-child' + ) ?> </div> <?php endif; ?> </li> diff --git a/app/code/Magento/Review/view/frontend/templates/customer/view.phtml b/app/code/Magento/Review/view/frontend/templates/customer/view.phtml index f92282848b1b7..862a9a466414f 100644 --- a/app/code/Magento/Review/view/frontend/templates/customer/view.phtml +++ b/app/code/Magento/Review/view/frontend/templates/customer/view.phtml @@ -4,11 +4,14 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Customer\View $block */ +/** + * @var \Magento\Review\Block\Customer\View $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $product = $block->getProductData(); ?> -<?php if ($product->getId()) : ?> +<?php if ($product->getId()): ?> <div class="customer-review view"> <div class="product-details"> <div class="product-media"> @@ -19,7 +22,7 @@ $product = $block->getProductData(); </div> <div class="product-info"> <h2 class="product-name"><?= $block->escapeHtml($product->getName()) ?></h2> - <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> + <?php if ($block->getRating() && $block->getRating()->getSize()): ?> <span class="rating-average-label"><?= $block->escapeHtml(__('Average Customer Rating:')) ?></span> <?= $block->getReviewsSummaryHtml($product) ?> <?php endif; ?> @@ -27,21 +30,27 @@ $product = $block->getProductData(); </div> <div class="review-details"> - <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> + <?php if ($block->getRating() && $block->getRating()->getSize()): ?> <div class="title"> <strong><?= $block->escapeHtml(__('Your Review')) ?></strong> </div> <div class="customer-review-rating"> - <?php foreach ($block->getRating() as $_rating) : ?> - <?php if ($_rating->getPercent()) : ?> + <?php foreach ($block->getRating() as $_rating): ?> + <?php if ($_rating->getPercent()): ?> <?php $rating = ceil($_rating->getPercent()) ?> <div class="rating-summary item"> - <span class="rating-label"><span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span></span> + <span class="rating-label"> + <span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span> + </span> <div class="rating-result" title="<?= /* @noEscape */ $rating ?>%"> - <span style="width:<?= /* @noEscape */ $rating ?>%"> + <span> <span><?= /* @noEscape */ $rating ?>%</span> </span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . /* @noEscape */ $rating . "%", + 'div.rating-result>span:first-child' + ) ?> </div> <?php endif; ?> <?php endforeach; ?> @@ -49,15 +58,20 @@ $product = $block->getProductData(); <?php endif; ?> <div class="review-title"><?= $block->escapeHtml($block->getReviewData()->getTitle()) ?></div> - <div class="review-content"><?= /* @noEscape */ nl2br($block->escapeHtml($block->getReviewData()->getDetail())) ?></div> + <div class="review-content"> + <?= /* @noEscape */ nl2br($block->escapeHtml($block->getReviewData()->getDetail())) ?> + </div> <div class="review-date"> - <?= $block->escapeHtml(__('Submitted on %1', '<time class="date">' . $block->dateFormat($block->getReviewData()->getCreatedAt()) . '</time>'), ['time']) ?> + <?= $block->escapeHtml(__('Submitted on %1', '<time class="date">' . + $block->dateFormat($block->getReviewData()->getCreatedAt()) . '</time>'), ['time']) ?> </div> </div> </div> <div class="actions-toolbar"> <div class="secondary"> - <a class="action back" href="<?= $block->escapeUrl($block->getBackUrl()) ?>"><span><?= $block->escapeHtml(__('Back to My Reviews')) ?></span></a> + <a class="action back" href="<?= $block->escapeUrl($block->getBackUrl()) ?>"> + <span><?= $block->escapeHtml(__('Back to My Reviews')) ?></span> + </a> </div> </div> <?php endif; ?> diff --git a/app/code/Magento/Review/view/frontend/templates/detailed.phtml b/app/code/Magento/Review/view/frontend/templates/detailed.phtml index 7b3b0e2dd6d02..1bd8138f9cdac 100644 --- a/app/code/Magento/Review/view/frontend/templates/detailed.phtml +++ b/app/code/Magento/Review/view/frontend/templates/detailed.phtml @@ -4,21 +4,28 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Rating\Entity\Detailed $block */ +/** + * @var \Magento\Review\Block\Rating\Entity\Detailed $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if (!empty($collection) && $collection->getSize()) : ?> +<?php if (!empty($collection) && $collection->getSize()): ?> <div class="table-wrapper"> <table class="data table ratings review summary"> <caption class="table-caption"><?= $block->escapeHtml(__('Ratings Review Summary')) ?></caption> <tbody> - <?php foreach ($collection as $_rating) : ?> - <?php if ($_rating->getSummary()) : ?> + <?php foreach ($collection as $_rating): ?> + <?php if ($_rating->getSummary()): ?> <tr> <th class="label" scope="row"><?= $block->escapeHtml(__($_rating->getRatingCode())) ?></th> <td class="value"> <div class="rating box"> - <div class="rating" style="width:<?= /* @noEscape */ ceil($_rating->getSummary()) ?>%;"></div> + <div class="rating"/> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . /* @noEscape */ ceil($_rating->getSummary()) . "%;", + 'div.rating.box div.rating' + ) ?> </td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml b/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml index b042b5e92cbac..93afe4a815f61 100644 --- a/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml +++ b/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml @@ -4,36 +4,49 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Product\ReviewRenderer $block */ +/** + * @var \Magento\Review\Block\Product\ReviewRenderer $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $url = $block->getReviewsUrl() . '#reviews'; $urlForm = $block->getReviewsUrl() . '#review-form'; ?> -<?php if ($block->isReviewEnabled() && $block->getReviewsCount()) : ?> +<?php if ($block->isReviewEnabled() && $block->getReviewsCount()): ?> <?php $rating = $block->getRatingSummary(); ?> - <div class="product-reviews-summary<?= !$rating ? ' no-rating' : '' ?>" itemprop="aggregateRating" itemscope itemtype="http://schema.org/AggregateRating"> - <?php if ($rating) :?> + <div class="product-reviews-summary<?= !$rating ? ' no-rating' : '' ?>" itemprop="aggregateRating" itemscope + itemtype="http://schema.org/AggregateRating"> + <?php if ($rating):?> <div class="rating-summary"> <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating); ?>%"> - <span style="width:<?= $block->escapeHtmlAttr($rating); ?>%"> + <span> <span> - <span itemprop="ratingValue"><?= $block->escapeHtml($rating); ?></span>% of <span itemprop="bestRating">100</span> + <span itemprop="ratingValue"><?= $block->escapeHtml($rating); ?> + </span>% of <span itemprop="bestRating">100</span> </span> </span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . $block->escapeHtmlAttr($rating) . "%", + 'div.rating-summary div.rating-result>span:first-child' + ) ?> <?php endif;?> <div class="reviews-actions"> <a class="action view" href="<?= $block->escapeUrl($url) ?>"> <span itemprop="reviewCount"><?= $block->escapeHtml($block->getReviewsCount()) ?></span>  - <span><?= ($block->getReviewsCount() == 1) ? $block->escapeHtml(__('Review')) : $block->escapeHtml(__('Reviews')) ?></span> + <span><?= ($block->getReviewsCount() == 1) ? $block->escapeHtml(__('Review')) : + $block->escapeHtml(__('Reviews')) ?> + </span> + </a> + <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> + <?= $block->escapeHtml(__('Add Your Review')) ?> </a> - <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"><?= $block->escapeHtml(__('Add Your Review')) ?></a> </div> </div> -<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()) : ?> +<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()): ?> <div class="product-reviews-summary empty"> <div class="reviews-actions"> <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> diff --git a/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml b/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml index 7ea84c952eaaa..20d695195c920 100644 --- a/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml +++ b/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml @@ -5,26 +5,38 @@ */ /** @var \Magento\Review\Block\Product\ReviewRenderer $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $url = $block->getReviewsUrl() . '#reviews'; $urlForm = $block->getReviewsUrl() . '#review-form'; ?> -<?php if ($block->isReviewEnabled() && $block->getReviewsCount()) : ?> +<?php if ($block->isReviewEnabled() && $block->getReviewsCount()): ?> <?php $rating = $block->getRatingSummary(); ?> <div class="product-reviews-summary short<?= !$rating ? ' no-rating' : '' ?>"> - <?php if ($rating) :?> + <?php if ($rating):?> <div class="rating-summary"> <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> - <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating) ?>%"> - <span style="width:<?= $block->escapeHtmlAttr($rating) ?>%"><span><?= $block->escapeHtml($rating) ?>%</span></span> + <div class="rating-result" + id="rating-result_<?= /* @noEscape */ $block->getProduct()->getId() ?>" + title="<?= $block->escapeHtmlAttr($rating) ?>%"> + <span><span><?= $block->escapeHtml($rating) ?>%</span></span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'width:' . $block->escapeHtmlAttr($rating) . '%', + '#rating-result_' . $block->getProduct()->getId() . ' span' + ) ?> </div> <?php endif;?> <div class="reviews-actions"> - <a class="action view" href="<?= $block->escapeUrl($url) ?>"><?= $block->escapeHtml($block->getReviewsCount()) ?> <span><?= ($block->getReviewsCount() == 1) ? $block->escapeHtml(__('Review')) : $block->escapeHtml(__('Reviews')) ?></span></a> + <a class="action view" + href="<?= $block->escapeUrl($url) ?>"><?= $block->escapeHtml($block->getReviewsCount()) ?> +  <span><?= ($block->getReviewsCount() == 1) ? + $block->escapeHtml(__('Review')) : $block->escapeHtml(__('Reviews')) ?> + </span> + </a> </div> </div> -<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()) : ?> +<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()): ?> <div class="product-reviews-summary short empty"> <div class="reviews-actions"> <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> diff --git a/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml b/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml index d00c310069573..e631f5bc19580 100644 --- a/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml +++ b/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml @@ -5,13 +5,14 @@ */ /** @var Magento\Review\Block\Product\View\ListView $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $_items = $block->getReviewsCollection()->getItems(); $format = $block->getDateFormat() ?: \IntlDateFormatter::SHORT; ?> -<?php if (count($_items)) : ?> +<?php if (count($_items)): ?> <div class="block review-list" id="customer-reviews"> - <?php if (!$block->getHideTitle()) : ?> + <?php if (!$block->getHideTitle()): ?> <div class="block-title"> <strong><?= $block->escapeHtml(__('Customer Reviews')) ?></strong> </div> @@ -21,21 +22,31 @@ $format = $block->getDateFormat() ?: \IntlDateFormatter::SHORT; <?= $block->getChildHtml('toolbar') ?> </div> <ol class="items review-items"> - <?php foreach ($_items as $_review) : ?> + <?php foreach ($_items as $_review): ?> <li class="item review-item" itemscope itemprop="review" itemtype="http://schema.org/Review"> <div class="review-title" itemprop="name"><?= $block->escapeHtml($_review->getTitle()) ?></div> - <?php if (count($_review->getRatingVotes())) : ?> + <?php if (count($_review->getRatingVotes())): ?> <div class="review-ratings"> - <?php foreach ($_review->getRatingVotes() as $_vote) : ?> - <div class="rating-summary item" itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating"> - <span class="label rating-label"><span><?= $block->escapeHtml($_vote->getRatingCode()) ?></span></span> - <div class="rating-result" title="<?= $block->escapeHtmlAttr($_vote->getPercent()) ?>%"> + <?php foreach ($_review->getRatingVotes() as $_vote): ?> + <div class="rating-summary item" + itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating"> + <span class="label rating-label"> + <span><?= $block->escapeHtml($_vote->getRatingCode()) ?></span> + </span> + <div class="rating-result" + id="review_<?= /* @noEscape */ $_review->getReviewId() + ?>_vote_<?= /* @noEscape */ $_vote->getVoteId() ?>" + title="<?= $block->escapeHtmlAttr($_vote->getPercent()) ?>%"> <meta itemprop="worstRating" content = "1"/> <meta itemprop="bestRating" content = "100"/> - <span style="width:<?= $block->escapeHtmlAttr($_vote->getPercent()) ?>%"> + <span> <span itemprop="ratingValue"><?= $block->escapeHtml($_vote->getPercent()) ?>%</span> </span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'width:' . $_vote->getPercent() . '%', + 'div#review_' . $_review->getReviewId() . '_vote_' . $_vote->getVoteId() . ' span' + ) ?> </div> <?php endforeach; ?> </div> @@ -46,11 +57,18 @@ $format = $block->getDateFormat() ?: \IntlDateFormatter::SHORT; <div class="review-details"> <p class="review-author"> <span class="review-details-label"><?= $block->escapeHtml(__('Review by')) ?></span> - <strong class="review-details-value" itemprop="author"><?= $block->escapeHtml($_review->getNickname()) ?></strong> + <strong class="review-details-value" + itemprop="author"><?= $block->escapeHtml($_review->getNickname()) ?></strong> </p> <p class="review-date"> <span class="review-details-label"><?= $block->escapeHtml(__('Posted on')) ?></span> - <time class="review-details-value" itemprop="datePublished" datetime="<?= $block->escapeHtmlAttr($block->formatDate($_review->getCreatedAt(), $format)) ?>"><?= $block->escapeHtml($block->formatDate($_review->getCreatedAt(), $format)) ?></time> + <time class="review-details-value" + itemprop="datePublished" + datetime="<?= $block->escapeHtmlAttr($block->formatDate( + $_review->getCreatedAt(), + $format + )) ?>"><?= $block->escapeHtml($block->formatDate($_review->getCreatedAt(), $format)) ?> + </time> </p> </div> </li> diff --git a/app/code/Magento/Review/view/frontend/templates/review.phtml b/app/code/Magento/Review/view/frontend/templates/review.phtml index ea4b4bc42a1ed..04782080fc775 100644 --- a/app/code/Magento/Review/view/frontend/templates/review.phtml +++ b/app/code/Magento/Review/view/frontend/templates/review.phtml @@ -13,7 +13,7 @@ { "*": { "Magento_Review/js/process-reviews": { - "productReviewUrl": "<?= $block->escapeJs($block->escapeUrl($block->getProductReviewUrl())) ?>", + "productReviewUrl": "<?= $block->escapeJs($block->getProductReviewUrl()) ?>", "reviewsTabSelector": "#tab-label-reviews" } } diff --git a/app/code/Magento/Review/view/frontend/templates/view.phtml b/app/code/Magento/Review/view/frontend/templates/view.phtml index 1c3d1942dd2e7..b51353b7df685 100644 --- a/app/code/Magento/Review/view/frontend/templates/view.phtml +++ b/app/code/Magento/Review/view/frontend/templates/view.phtml @@ -4,44 +4,57 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\View $block */ +/** + * @var \Magento\Review\Block\View $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->getProductData()->getId()) : ?> +<?php if ($block->getProductData()->getId()): ?> <div class="product-review"> <div class="page-title-wrapper"> <h1><?= $block->escapeHtml(__('Review Details')) ?></h1> </div> <div class="product-img-box"> <a href="<?= $block->escapeUrl($block->getProductData()->getProductUrl()) ?>"> - <?= $block->getImage($block->getProductData(), 'product_base_image', ['class' => 'product-image'])->toHtml() ?> + <?= $block->getImage($block->getProductData(), 'product_base_image', ['class' => 'product-image'])->toHtml() + ?> </a> - <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> + <?php if ($block->getRating() && $block->getRating()->getSize()): ?> <p><?= $block->escapeHtml(__('Average Customer Rating')) ?>:</p> <?= $block->getReviewsSummaryHtml($block->getProductData()) ?> <?php endif; ?> </div> <div class="details"> <h3 class="product-name"><?= $block->escapeHtml($block->getProductData()->getName()) ?></h3> - <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> + <?php if ($block->getRating() && $block->getRating()->getSize()): ?> <h4><?= $block->escapeHtml(__('Product Rating:')) ?></h4> <div class="table-wrapper"> <table class="data-table review-summary-table"> <caption class="table-caption"><?= $block->escapeHtml(__('Product Rating')) ?></caption> - <?php foreach ($block->getRating() as $_rating) : ?> - <?php if ($_rating->getPercent()) : ?> + <?php foreach ($block->getRating() as $_rating): ?> + <?php if ($_rating->getPercent()): ?> <tr> <td class="label"><?= $block->escapeHtml(__($_rating->getRatingCode())) ?></td> <td class="value"> <div class="rating-box"> - <div class="rating" style="width:<?= /* @noEscape */ ceil($_rating->getPercent()) ?>%;"></div> - </div></td> + <div class="rating"/> + </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . /* @noEscape */ ceil($_rating->getPercent()) . "%;", + 'div.rating-box div.rating' + ) ?> + </td> </tr> <?php endif; ?> <?php endforeach; ?> </table> </div> <?php endif; ?> - <p class="date"><?= $block->escapeHtml(__('Product Review (submitted on %1):', $block->dateFormat($block->getReviewData()->getCreatedAt()))) ?></p> + <p class="date"> + <?= $block->escapeHtml( + __('Product Review (submitted on %1):', $block->dateFormat($block->getReviewData()->getCreatedAt())) + ) ?> + </p> <p><?= /* @noEscape */ nl2br($block->escapeHtml($block->getReviewData()->getDetail())) ?></p> </div> <div class="actions"> diff --git a/app/code/Magento/ReviewGraphQl/Mapper/ReviewDataMapper.php b/app/code/Magento/ReviewGraphQl/Mapper/ReviewDataMapper.php new file mode 100644 index 0000000000000..6a06fbfc4102c --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Mapper/ReviewDataMapper.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Mapper; + +use Magento\Catalog\Model\Product; +use Magento\Review\Model\Review; + +/** + * Converts the review data from review object to an associative array + */ +class ReviewDataMapper +{ + /** + * Mapping the review data + * + * @param Review $review + * + * @return array + */ + public function map(Review $review): array + { + return [ + 'summary' => $review->getData('title'), + 'text' => $review->getData('detail'), + 'nickname' => $review->getData('nickname'), + 'created_at' => $review->getData('created_at'), + 'sku' => $review->getSku(), + 'model' => $review + ]; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/DataProvider/AggregatedReviewsDataProvider.php b/app/code/Magento/ReviewGraphQl/Model/DataProvider/AggregatedReviewsDataProvider.php new file mode 100644 index 0000000000000..5412c670b4800 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/DataProvider/AggregatedReviewsDataProvider.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\DataProvider; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Review\Model\ResourceModel\Review\Collection as ReviewCollection; +use Magento\Review\Model\ResourceModel\Review\Product\Collection as ProductCollection; +use Magento\ReviewGraphQl\Mapper\ReviewDataMapper; + +/** + * Provides aggregated reviews result + * + * The following class prepares the GraphQl endpoints' result for Customer and Product reviews + */ +class AggregatedReviewsDataProvider +{ + /** + * @var ReviewDataMapper + */ + private $reviewDataMapper; + + /** + * @param ReviewDataMapper $reviewDataMapper + */ + public function __construct(ReviewDataMapper $reviewDataMapper) + { + $this->reviewDataMapper = $reviewDataMapper; + } + + /** + * Get reviews result + * + * @param ProductCollection|ReviewCollection $reviewsCollection + * + * @return array + */ + public function getData($reviewsCollection): array + { + if ($reviewsCollection->getPageSize()) { + $maxPages = ceil($reviewsCollection->getSize() / $reviewsCollection->getPageSize()); + } else { + $maxPages = 0; + } + + $currentPage = $reviewsCollection->getCurPage(); + if ($reviewsCollection->getCurPage() > $maxPages && $reviewsCollection->getSize() > 0) { + $currentPage = new GraphQlInputException( + __( + 'currentPage value %1 specified is greater than the number of pages available.', + [$maxPages] + ) + ); + } + + $items = []; + foreach ($reviewsCollection->getItems() as $item) { + $items[] = $this->reviewDataMapper->map($item); + } + + return [ + 'total_count' => $reviewsCollection->getSize(), + 'items' => $items, + 'page_info' => [ + 'page_size' => $reviewsCollection->getPageSize(), + 'current_page' => $currentPage, + 'total_pages' => $maxPages + ] + ]; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/DataProvider/CustomerReviewsDataProvider.php b/app/code/Magento/ReviewGraphQl/Model/DataProvider/CustomerReviewsDataProvider.php new file mode 100644 index 0000000000000..42adc8009c010 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/DataProvider/CustomerReviewsDataProvider.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\DataProvider; + +use Magento\Review\Model\ResourceModel\Review\Collection as ReviewsCollection; +use Magento\Review\Model\ResourceModel\Review\CollectionFactory as ReviewsCollectionFactory; +use Magento\Review\Model\Review; + +/** + * Provides customer reviews + */ +class CustomerReviewsDataProvider +{ + /** + * @var ReviewsCollectionFactory + */ + private $collectionFactory; + + /** + * @param ReviewsCollectionFactory $collectionFactory + */ + public function __construct( + ReviewsCollectionFactory $collectionFactory + ) { + $this->collectionFactory = $collectionFactory; + } + + /** + * Get customer reviews + * + * @param int $customerId + * @param int $currentPage + * @param int $pageSize + * + * @return ReviewsCollection + */ + public function getData(int $customerId, int $currentPage, int $pageSize): ReviewsCollection + { + /** @var ReviewsCollection $reviewsCollection */ + $reviewsCollection = $this->collectionFactory->create(); + $reviewsCollection + ->addCustomerFilter($customerId) + ->setPageSize($pageSize) + ->setCurPage($currentPage) + ->setDateOrder(); + $reviewsCollection->getSelect()->join( + ['cpe' => $reviewsCollection->getTable('catalog_product_entity')], + 'cpe.entity_id = main_table.entity_pk_value', + ['sku'] + ); + $reviewsCollection->addRateVotes(); + + return $reviewsCollection; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/DataProvider/ProductReviewsDataProvider.php b/app/code/Magento/ReviewGraphQl/Model/DataProvider/ProductReviewsDataProvider.php new file mode 100644 index 0000000000000..635605f9091ed --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/DataProvider/ProductReviewsDataProvider.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\DataProvider; + +use Magento\Review\Model\ResourceModel\Review\Collection; +use Magento\Review\Model\ResourceModel\Review\CollectionFactory; +use Magento\Review\Model\Review; + +/** + * Provides product reviews + */ +class ProductReviewsDataProvider +{ + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param CollectionFactory $collectionFactory + */ + public function __construct( + CollectionFactory $collectionFactory + ) { + $this->collectionFactory = $collectionFactory; + } + + /** + * Get product reviews + * + * @param int $productId + * @param int $currentPage + * @param int $pageSize + * + * @return Collection + */ + public function getData(int $productId, int $currentPage, int $pageSize): Collection + { + /** @var Collection $reviewsCollection */ + $reviewsCollection = $this->collectionFactory->create() + ->addStatusFilter(Review::STATUS_APPROVED) + ->addEntityFilter(Review::ENTITY_PRODUCT_CODE, $productId) + ->setPageSize($pageSize) + ->setCurPage($currentPage) + ->setDateOrder(); + $reviewsCollection->getSelect()->join( + ['cpe' => $reviewsCollection->getTable('catalog_product_entity')], + 'cpe.entity_id = main_table.entity_pk_value', + ['sku'] + ); + $reviewsCollection->addRateVotes(); + + return $reviewsCollection; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/DataProvider/ReviewRatingsDataProvider.php b/app/code/Magento/ReviewGraphQl/Model/DataProvider/ReviewRatingsDataProvider.php new file mode 100644 index 0000000000000..82e0f73b1c774 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/DataProvider/ReviewRatingsDataProvider.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\DataProvider; + +use Magento\Review\Model\ResourceModel\Rating\Option\Vote\Collection as VoteCollection; +use Magento\Review\Model\ResourceModel\Rating\Option\Vote\CollectionFactory as VoteCollectionFactory; + +/** + * Provides rating votes + */ +class ReviewRatingsDataProvider +{ + /** + * @var VoteCollectionFactory + */ + private $voteCollectionFactory; + + /** + * @param VoteCollectionFactory $voteCollectionFactory + */ + public function __construct(VoteCollectionFactory $voteCollectionFactory) + { + $this->voteCollectionFactory = $voteCollectionFactory; + } + + /** + * Providing rating votes + * + * @param int $reviewId + * + * @return array + */ + public function getData(int $reviewId): array + { + /** @var VoteCollection $ratingVotes */ + $ratingVotes = $this->voteCollectionFactory->create(); + $ratingVotes->setReviewFilter($reviewId); + $ratingVotes->addRatingInfo(); + + $data = []; + + foreach ($ratingVotes->getItems() as $ratingVote) { + $data[] = [ + 'name' => $ratingVote->getData('rating_code'), + 'value' => $ratingVote->getData('value') + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/CreateProductReview.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/CreateProductReview.php new file mode 100644 index 0000000000000..9b0171c3b700a --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/CreateProductReview.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Review\Helper\Data as ReviewHelper; +use Magento\Review\Model\Review\Config as ReviewsConfig; +use Magento\ReviewGraphQl\Mapper\ReviewDataMapper; +use Magento\ReviewGraphQl\Model\Review\AddReviewToProduct; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Create product review resolver + */ +class CreateProductReview implements ResolverInterface +{ + /** + * @var ReviewHelper + */ + private $reviewHelper; + + /** + * @var AddReviewToProduct + */ + private $addReviewToProduct; + + /** + * @var ReviewDataMapper + */ + private $reviewDataMapper; + + /** + * @var ReviewsConfig + */ + private $reviewsConfig; + + /** + * @param AddReviewToProduct $addReviewToProduct + * @param ReviewDataMapper $reviewDataMapper + * @param ReviewHelper $reviewHelper + * @param ReviewsConfig $reviewsConfig + */ + public function __construct( + AddReviewToProduct $addReviewToProduct, + ReviewDataMapper $reviewDataMapper, + ReviewHelper $reviewHelper, + ReviewsConfig $reviewsConfig + ) { + + $this->addReviewToProduct = $addReviewToProduct; + $this->reviewDataMapper = $reviewDataMapper; + $this->reviewHelper = $reviewHelper; + $this->reviewsConfig = $reviewsConfig; + } + + /** + * Resolve product review ratings + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array[]|Value|mixed + * + * @throws GraphQlAuthorizationException + * @throws GraphQlNoSuchEntityException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $this->reviewsConfig->isEnabled()) { + throw new GraphQlAuthorizationException(__('Creating product reviews are not currently available.')); + } + + $input = $args['input']; + $customerId = null; + + if (false !== $context->getExtensionAttributes()->getIsCustomer()) { + $customerId = (int) $context->getUserId(); + } + + if (!$customerId && !$this->reviewHelper->getIsGuestAllowToWrite()) { + throw new GraphQlAuthorizationException(__('Guest customers aren\'t allowed to add product reviews.')); + } + + $sku = $input['sku']; + $ratings = $input['ratings']; + $data = [ + 'nickname' => $input['nickname'], + 'title' => $input['summary'], + 'detail' => $input['text'], + ]; + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $review = $this->addReviewToProduct->execute($data, $ratings, $sku, $customerId, (int) $store->getId()); + + return ['review' => $this->reviewDataMapper->map($review)]; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Customer/Reviews.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Customer/Reviews.php new file mode 100644 index 0000000000000..8c0bca63f8efc --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Customer/Reviews.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Customer; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\ReviewGraphQl\Model\DataProvider\AggregatedReviewsDataProvider; +use Magento\ReviewGraphQl\Model\DataProvider\CustomerReviewsDataProvider; + +/** + * Customer reviews resolver, used by GraphQL endpoints to retrieve customer's reviews + */ +class Reviews implements ResolverInterface +{ + /** + * @var CustomerReviewsDataProvider + */ + private $customerReviewsDataProvider; + + /** + * @var AggregatedReviewsDataProvider + */ + private $aggregatedReviewsDataProvider; + + /** + * @param CustomerReviewsDataProvider $customerReviewsDataProvider + * @param AggregatedReviewsDataProvider $aggregatedReviewsDataProvider + */ + public function __construct( + CustomerReviewsDataProvider $customerReviewsDataProvider, + AggregatedReviewsDataProvider $aggregatedReviewsDataProvider + ) { + $this->customerReviewsDataProvider = $customerReviewsDataProvider; + $this->aggregatedReviewsDataProvider = $aggregatedReviewsDataProvider; + } + + /** + * Resolves the customer reviews + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * + * @throws GraphQlInputException + * @throws GraphQlAuthorizationException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + + if ($args['currentPage'] < 1) { + throw new GraphQlInputException(__('currentPage value must be greater than 0.')); + } + + if ($args['pageSize'] < 1) { + throw new GraphQlInputException(__('pageSize value must be greater than 0.')); + } + + $reviewsCollection = $this->customerReviewsDataProvider->getData( + (int) $context->getUserId(), + $args['currentPage'], + $args['pageSize'] + ); + + return $this->aggregatedReviewsDataProvider->getData($reviewsCollection); + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/RatingSummary.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/RatingSummary.php new file mode 100644 index 0000000000000..eed5034c59daa --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/RatingSummary.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Product; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Review\Model\Review\Config as ReviewsConfig; +use Magento\Review\Model\Review\SummaryFactory; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Average rating for the product + */ +class RatingSummary implements ResolverInterface +{ + /** + * @var SummaryFactory + */ + private $summaryFactory; + + /** + * @var ReviewsConfig + */ + private $reviewsConfig; + + /** + * @param SummaryFactory $summaryFactory + * @param ReviewsConfig $reviewsConfig + */ + public function __construct( + SummaryFactory $summaryFactory, + ReviewsConfig $reviewsConfig + ) { + $this->summaryFactory = $summaryFactory; + $this->reviewsConfig = $reviewsConfig; + } + + /** + * Resolves the product rating summary + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return float + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): float { + if (false === $this->reviewsConfig->isEnabled()) { + return 0; + } + + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + /** @var Product $product */ + $product = $value['model']; + + try { + $summary = $this->summaryFactory->create()->setStoreId($store->getId())->load($product->getId()); + + return floatval($summary->getData('rating_summary')); + } catch (Exception $e) { + throw new GraphQlInputException(__('Couldn\'t get the product rating summary.')); + } + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/AverageRating.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/AverageRating.php new file mode 100644 index 0000000000000..2e0d428b47873 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/AverageRating.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Product\Review; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Review\Model\RatingFactory; +use Magento\Review\Model\Review; + +/** + * Review average rating resolver + */ +class AverageRating implements ResolverInterface +{ + /** + * @var RatingFactory + */ + private $ratingFactory; + + /** + * @param RatingFactory $ratingFactory + */ + public function __construct( + RatingFactory $ratingFactory + ) { + $this->ratingFactory = $ratingFactory; + } + + /** + * Resolves review average rating + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return float|Value|mixed + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + + /** @var Review $review */ + $review = $value['model']; + $summary = $this->ratingFactory->create()->getReviewSummary($review->getId()); + $averageRating = $summary->getSum() ?: 0; + + if ($averageRating > 0) { + $averageRating = (float) number_format( + (int) $summary->getSum() / (int) $summary->getCount(), + 2 + ); + } + + return $averageRating; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/RatingBreakdown.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/RatingBreakdown.php new file mode 100644 index 0000000000000..a51bd0420dda9 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/RatingBreakdown.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Product\Review; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Review\Model\Review; +use Magento\ReviewGraphQl\Model\DataProvider\ReviewRatingsDataProvider; + +/** + * Review rating resolver + */ +class RatingBreakdown implements ResolverInterface +{ + /** + * @var ReviewRatingsDataProvider + */ + private $reviewRatingsDataProvider; + + /** + * @param ReviewRatingsDataProvider $reviewRatingsDataProvider + */ + public function __construct( + ReviewRatingsDataProvider $reviewRatingsDataProvider + ) { + $this->reviewRatingsDataProvider = $reviewRatingsDataProvider; + } + + /** + * Resolves the rating breakdown + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + + /** @var Review $review */ + $review = $value['model']; + + return $this->reviewRatingsDataProvider->getData((int) $review->getId()); + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/ReviewCount.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/ReviewCount.php new file mode 100644 index 0000000000000..dfa62adf0266e --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/ReviewCount.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Product; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Catalog\Model\Product; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Review\Model\Review; +use Magento\Review\Model\Review\Config as ReviewsConfig; + +/** + * Product total review count + */ +class ReviewCount implements ResolverInterface +{ + /** + * @var Review + */ + private $review; + + /** + * @var ReviewsConfig + */ + private $reviewsConfig; + + /** + * @param Review $review + * @param ReviewsConfig $reviewsConfig + */ + public function __construct(Review $review, ReviewsConfig $reviewsConfig) + { + $this->review = $review; + $this->reviewsConfig = $reviewsConfig; + } + + /** + * Resolves the product total reviews + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return int|Value|mixed + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $this->reviewsConfig->isEnabled()) { + return 0; + } + + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + + /** @var Product $product */ + $product = $value['model']; + + return (int) $this->review->getTotalReviews($product->getId(), true); + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Reviews.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Reviews.php new file mode 100644 index 0000000000000..72eea5e6b3bd2 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Reviews.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Product; + +use Magento\Catalog\Model\Product; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Review\Model\Review\Config as ReviewsConfig; +use Magento\ReviewGraphQl\Model\DataProvider\AggregatedReviewsDataProvider; +use Magento\ReviewGraphQl\Model\DataProvider\ProductReviewsDataProvider; + +/** + * Product reviews resolver, used by GraphQL endpoints to retrieve product's reviews + */ +class Reviews implements ResolverInterface +{ + /** + * @var ProductReviewsDataProvider + */ + private $productReviewsDataProvider; + + /** + * @var AggregatedReviewsDataProvider + */ + private $aggregatedReviewsDataProvider; + + /** + * @var ReviewsConfig + */ + private $reviewsConfig; + + /** + * @param ProductReviewsDataProvider $productReviewsDataProvider + * @param AggregatedReviewsDataProvider $aggregatedReviewsDataProvider + * @param ReviewsConfig $reviewsConfig + */ + public function __construct( + ProductReviewsDataProvider $productReviewsDataProvider, + AggregatedReviewsDataProvider $aggregatedReviewsDataProvider, + ReviewsConfig $reviewsConfig + ) { + $this->productReviewsDataProvider = $productReviewsDataProvider; + $this->aggregatedReviewsDataProvider = $aggregatedReviewsDataProvider; + $this->reviewsConfig = $reviewsConfig; + } + + /** + * Resolves the product reviews + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $this->reviewsConfig->isEnabled()) { + return ['items' => []]; + } + + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + + if ($args['currentPage'] < 1) { + throw new GraphQlInputException(__('currentPage value must be greater than 0.')); + } + + if ($args['pageSize'] < 1) { + throw new GraphQlInputException(__('pageSize value must be greater than 0.')); + } + + /** @var Product $product */ + $product = $value['model']; + $reviewsCollection = $this->productReviewsDataProvider->getData( + (int) $product->getId(), + $args['currentPage'], + $args['pageSize'] + ); + + return $this->aggregatedReviewsDataProvider->getData($reviewsCollection); + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingValueMetadata.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingValueMetadata.php new file mode 100644 index 0000000000000..e7e6574e7e7ae --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingValueMetadata.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Product rating value resolver + */ +class ProductReviewRatingValueMetadata implements ResolverInterface +{ + /** + * Resolve product review rating values + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return array|Value|mixed + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['values'])) { + throw new GraphQlInputException(__('Value must contain "values" property.')); + } + + $ratingOptions = $value['values']; + $data = []; + + foreach ($ratingOptions as $item) { + $data[] = ['value' => $item->getData('value'), 'value_id' => base64_encode($item->getData('option_id'))]; + } + + return $data; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingsMetadata.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingsMetadata.php new file mode 100644 index 0000000000000..2cf536255baf7 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingsMetadata.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Review\Model\ResourceModel\Rating\Collection as RatingCollection; +use Magento\Review\Model\ResourceModel\Rating\CollectionFactory; +use Magento\Review\Model\Review; +use Magento\Review\Model\Review\Config as ReviewsConfig; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Resolve data review rating metadata + */ +class ProductReviewRatingsMetadata implements ResolverInterface +{ + /** + * @var CollectionFactory + */ + private $ratingCollectionFactory; + + /** + * @var ReviewsConfig + */ + private $reviewsConfig; + + /** + * @param CollectionFactory $ratingCollectionFactory + * @param ReviewsConfig $reviewsConfig + */ + public function __construct(CollectionFactory $ratingCollectionFactory, ReviewsConfig $reviewsConfig) + { + $this->ratingCollectionFactory = $ratingCollectionFactory; + $this->reviewsConfig = $reviewsConfig; + } + + /** + * Resolve product review ratings + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array[]|Value|mixed + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $this->reviewsConfig->isEnabled()) { + return ['items' => []]; + } + + $items = []; + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + /** @var RatingCollection $ratingCollection */ + $ratingCollection = $this->ratingCollectionFactory->create(); + $ratingCollection->addEntityFilter(Review::ENTITY_PRODUCT_CODE) + ->setStoreFilter($store->getId()) + ->setActiveFilter(true) + ->setPositionOrder() + ->addOptionToItems(); + + foreach ($ratingCollection->getItems() as $item) { + $items[] = [ + 'id' => base64_encode($item->getData('rating_id')), + 'name' => $item->getData('rating_code'), + 'values' => $item->getData('options') + ]; + } + + return ['items' => $items]; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Review/AddReviewToProduct.php b/app/code/Magento/ReviewGraphQl/Model/Review/AddReviewToProduct.php new file mode 100644 index 0000000000000..1b744e717a782 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Review/AddReviewToProduct.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Review; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Review\Model\Rating; +use Magento\Review\Model\RatingFactory; +use Magento\Review\Model\ResourceModel\Rating\Option\Vote\Collection as OptionVoteCollection; +use Magento\Review\Model\ResourceModel\Rating\Option\Vote\CollectionFactory as OptionVoteCollectionFactory; +use Magento\Review\Model\Review; +use Magento\Review\Model\ReviewFactory; + +/** + * Adding a review to specific product + */ +class AddReviewToProduct +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var RatingFactory + */ + private $ratingFactory; + + /** + * @var ReviewFactory + */ + private $reviewFactory; + + /** + * @var OptionVoteCollectionFactory + */ + private $ratingOptionCollectionFactory; + + /** + * @param ProductRepositoryInterface $productRepository + * @param ReviewFactory $reviewFactory + * @param RatingFactory $ratingFactory + * @param OptionVoteCollectionFactory $ratingOptionCollectionFactory + */ + public function __construct( + ProductRepositoryInterface $productRepository, + ReviewFactory $reviewFactory, + RatingFactory $ratingFactory, + OptionVoteCollectionFactory $ratingOptionCollectionFactory + ) { + $this->productRepository = $productRepository; + $this->reviewFactory = $reviewFactory; + $this->ratingFactory = $ratingFactory; + $this->ratingOptionCollectionFactory = $ratingOptionCollectionFactory; + } + + /** + * Add review to product + * + * @param array $data + * @param array $ratings + * @param string $sku + * @param int|null $customerId + * @param int $storeId + * + * @return Review + * + * @throws GraphQlNoSuchEntityException + */ + public function execute(array $data, array $ratings, string $sku, ?int $customerId, int $storeId): Review + { + $review = $this->reviewFactory->create()->setData($data); + $review->unsetData('review_id'); + $productId = $this->getProductIdBySku($sku); + $review->setEntityId($review->getEntityIdByCode(Review::ENTITY_PRODUCT_CODE)) + ->setEntityPkValue($productId) + ->setStatusId(Review::STATUS_PENDING) + ->setCustomerId($customerId) + ->setStoreId($storeId) + ->setStores([$storeId]) + ->save(); + $this->addReviewRatingVotes($ratings, (int) $review->getId(), $customerId, $productId); + $review->aggregate(); + $votesCollection = $this->getReviewRatingVotes((int) $review->getId(), $storeId); + $review->setData('rating_votes', $votesCollection); + $review->setData('sku', $sku); + + return $review; + } + + /** + * Get Product ID + * + * @param string $sku + * + * @return int|null + * + * @throws GraphQlNoSuchEntityException + */ + private function getProductIdBySku(string $sku): ?int + { + try { + $product = $this->productRepository->get($sku, false, null, true); + + return (int) $product->getId(); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__('Could not find a product with SKU "%sku"', ['sku' => $sku])); + } + } + + /** + * Add review rating votes + * + * @param array $ratings + * @param int $reviewId + * @param int|null $customerId + * @param int $productId + * + * @return void + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + private function addReviewRatingVotes(array $ratings, int $reviewId, ?int $customerId, int $productId): void + { + foreach ($ratings as $option) { + $ratingId = $option['id']; + $optionId = $option['value_id']; + /** @var Rating $ratingModel */ + $ratingModel = $this->ratingFactory->create(); + $ratingModel->setRatingId(base64_decode($ratingId)) + ->setReviewId($reviewId) + ->setCustomerId($customerId) + ->addOptionVote(base64_decode($optionId), $productId); + } + } + + /** + * Get review rating votes + * + * @param int $reviewId + * @param int $storeId + * + * @return OptionVoteCollection + */ + private function getReviewRatingVotes(int $reviewId, int $storeId): OptionVoteCollection + { + /** @var OptionVoteCollection $votesCollection */ + $votesCollection = $this->ratingOptionCollectionFactory->create(); + $votesCollection->setReviewFilter($reviewId)->setStoreFilter($storeId)->addRatingInfo($storeId); + + return $votesCollection; + } +} diff --git a/app/code/Magento/ReviewGraphQl/README.md b/app/code/Magento/ReviewGraphQl/README.md new file mode 100644 index 0000000000000..bf9563b87c9b2 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/README.md @@ -0,0 +1,3 @@ +# ReviewGraphQl + +**ReviewGraphQl** provides endpoints for getting and creating the Product reviews by guest and logged in customers. diff --git a/app/code/Magento/ReviewGraphQl/composer.json b/app/code/Magento/ReviewGraphQl/composer.json new file mode 100644 index 0000000000000..819ddefd76213 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-review-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-catalog": "*", + "magento/module-review": "*", + "magento/module-store": "*", + "magento/framework": "*" + }, + "suggest": { + "magento/module-graph-ql": "*", + "magento/module-graph-ql-cache": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\ReviewGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/ReviewGraphQl/etc/module.xml b/app/code/Magento/ReviewGraphQl/etc/module.xml new file mode 100644 index 0000000000000..c098ee5094760 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/etc/module.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_ReviewGraphQl" > + <sequence> + <module name="Magento_GraphQl"/> + <module name="Magento_Review"/> + <module name="Magento_Store"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/ReviewGraphQl/etc/schema.graphqls b/app/code/Magento/ReviewGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..14b4fc60e8b09 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/etc/schema.graphqls @@ -0,0 +1,78 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +interface ProductInterface { + rating_summary: Float! @doc(description: "The average of all the ratings given to the product.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Product\\RatingSummary") + review_count: Int! @doc(description: "The total count of all the reviews given to the product.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Product\\ReviewCount") + reviews( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return."), + ): ProductReviews! @doc(description: "The list of products reviews.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Product\\Reviews") +} + +type ProductReviews { + items: [ProductReview]! @doc(description: "An array of product reviews.") + page_info: SearchResultPageInfo! @doc(description: "Metadata for pagination rendering.") +} + +type ProductReview @doc(description: "Details of a product review") { + product: ProductInterface! @doc(description: "Contains details about the reviewed product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") + summary: String! @doc(description: "The summary (title) of the review") + text: String! @doc(description: "The review text.") + nickname: String! @doc(description: "The customer's nickname. Defaults to the customer name, if logged in") + created_at: String! @doc(description: "Date indicating when the review was created.") + average_rating: Float! @doc(description: "The average rating for product review.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Product\\Review\\AverageRating") + ratings_breakdown: [ProductReviewRating!]! @doc(description: "An array of ratings by rating category, such as quality, price, and value") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Product\\Review\\RatingBreakdown") +} + +type ProductReviewRating { + name: String! @doc(description: "The label assigned to an aspect of a product that is being rated, such as quality or price") + value: String! @doc(description: "The rating value given by customer. By default, possible values range from 1 to 5.") +} + +type Query { + productReviewRatingsMetadata: ProductReviewRatingsMetadata! @doc(description: "Retrieves metadata required by clients to render the Reviews section.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\ProductReviewRatingsMetadata") +} + +type ProductReviewRatingsMetadata { + items: [ProductReviewRatingMetadata!]! @doc(description: "List of product reviews sorted by position") +} + +type ProductReviewRatingMetadata { + id: String! @doc(description: "Base64 encoded rating ID.") + name: String! @doc(description: "The label assigned to an aspect of a product that is being rated, such as quality or price") + values: [ProductReviewRatingValueMetadata!]! @doc(description: "List of product review ratings sorted by position.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\ProductReviewRatingValueMetadata") +} + +type ProductReviewRatingValueMetadata { + value_id: String! @doc(description: "Base 64 encoded rating value id.") + value: String! @doc(description: "e.g Good, Perfect, 3, 4, 5") +} + +type Customer { + reviews( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return."), + ): ProductReviews! @doc(description: "Contains the customer's product reviews") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Customer\\Reviews") +} + +type Mutation { + createProductReview(input: CreateProductReviewInput!): CreateProductReviewOutput! @doc(description: "Creates a product review for the specified SKU") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\CreateProductReview") +} + +type CreateProductReviewOutput { + review: ProductReview! @doc(description: "Contains the completed product review") +} + +input CreateProductReviewInput { + sku: String! @doc(description: "The SKU of the reviewed product") + nickname: String! @doc(description: "The customer's nickname. Defaults to the customer name, if logged in") + summary: String! @doc(description: "The summary (title) of the review") + text: String! @doc(description: "The review text.") + ratings: [ProductReviewRatingInput!]! @doc(description: "Ratings details by category. e.g price: 5, quality: 4 etc") +} + +input ProductReviewRatingInput { + id: String! @doc(description: "Base64 encoded rating ID.") + value_id: String! @doc(description: "Base 64 encoded rating value id.") +} diff --git a/app/code/Magento/ReviewGraphQl/registration.php b/app/code/Magento/ReviewGraphQl/registration.php new file mode 100644 index 0000000000000..8fb6535902edf --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_ReviewGraphQl', __DIR__); diff --git a/app/code/Magento/Robots/Block/Data.php b/app/code/Magento/Robots/Block/Data.php index 460225d3ed71c..9a28f91de19d9 100644 --- a/app/code/Magento/Robots/Block/Data.php +++ b/app/code/Magento/Robots/Block/Data.php @@ -19,7 +19,7 @@ * Prepares base content for robots.txt and implements Page Cache functionality. * * @api - * @since 100.2.0 + * @since 100.1.0 */ class Data extends AbstractBlock implements IdentityInterface { @@ -60,7 +60,7 @@ public function __construct( * Retrieve base content for robots.txt file * * @return string - * @since 100.2.0 + * @since 100.1.0 */ protected function _toHtml() { @@ -71,7 +71,7 @@ protected function _toHtml() * Get unique page cache identities * * @return array - * @since 100.2.0 + * @since 100.1.0 */ public function getIdentities() { diff --git a/app/code/Magento/Robots/Model/Config/Value.php b/app/code/Magento/Robots/Model/Config/Value.php index 16a5a486e1078..ab955dadbe33d 100644 --- a/app/code/Magento/Robots/Model/Config/Value.php +++ b/app/code/Magento/Robots/Model/Config/Value.php @@ -23,7 +23,7 @@ * Required to implement Page Cache functionality. * * @api - * @since 100.2.0 + * @since 100.1.0 */ class Value extends ConfigValue implements IdentityInterface { @@ -35,7 +35,7 @@ class Value extends ConfigValue implements IdentityInterface /** * @inheritdoc * - * @since 100.2.0 + * @since 100.1.0 */ protected $_cacheTag = [self::CACHE_TAG]; @@ -86,7 +86,7 @@ public function __construct( * Get unique page cache identities * * @return array - * @since 100.2.0 + * @since 100.1.0 */ public function getIdentities() { diff --git a/app/code/Magento/Rss/Model/Rss.php b/app/code/Magento/Rss/Model/Rss.php index e37ee263b8301..eb24db28b9572 100644 --- a/app/code/Magento/Rss/Model/Rss.php +++ b/app/code/Magento/Rss/Model/Rss.php @@ -8,10 +8,13 @@ namespace Magento\Rss\Model; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\App\FeedFactoryInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Rss\DataProviderInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\RuntimeException; use Magento\Framework\Serialize\SerializerInterface; -use Magento\Framework\App\FeedFactoryInterface; /** * Provides functionality to work with RSS feeds @@ -27,12 +30,12 @@ class Rss protected $dataProvider; /** - * @var \Magento\Framework\App\CacheInterface + * @var CacheInterface */ protected $cache; /** - * @var \Magento\Framework\App\FeedFactoryInterface + * @var FeedFactoryInterface */ private $feedFactory; @@ -44,12 +47,12 @@ class Rss /** * Rss constructor * - * @param \Magento\Framework\App\CacheInterface $cache + * @param CacheInterface $cache * @param SerializerInterface|null $serializer * @param FeedFactoryInterface|null $feedFactory */ public function __construct( - \Magento\Framework\App\CacheInterface $cache, + CacheInterface $cache, SerializerInterface $serializer = null, FeedFactoryInterface $feedFactory = null ) { @@ -59,6 +62,8 @@ public function __construct( } /** + * Returns feeds + * * @return array */ public function getFeeds() @@ -66,47 +71,48 @@ public function getFeeds() if ($this->dataProvider === null) { return []; } - $cache = false; - if ($this->dataProvider->getCacheKey() && $this->dataProvider->getCacheLifetime()) { - $cache = $this->cache->load($this->dataProvider->getCacheKey()); - } + $cacheKey = $this->dataProvider->getCacheKey(); + $cacheLifeTime = $this->dataProvider->getCacheLifetime(); + $cache = $cacheKey && $cacheLifeTime ? $this->cache->load($cacheKey) : false; if ($cache) { return $this->serializer->unserialize($cache); } - $data = $this->dataProvider->getRssData(); + // serializing data to make sure all Phrase objects converted to a string + $serializedData = $this->serializer->serialize($this->dataProvider->getRssData()); - if ($this->dataProvider->getCacheKey() && $this->dataProvider->getCacheLifetime()) { - $this->cache->save( - $this->serializer->serialize($data), - $this->dataProvider->getCacheKey(), - ['rss'], - $this->dataProvider->getCacheLifetime() - ); + if ($cacheKey && $cacheLifeTime) { + $this->cache->save($serializedData, $cacheKey, ['rss'], $cacheLifeTime); } - return $data; + return $this->serializer->unserialize($serializedData); } /** + * Sets data provider + * * @param DataProviderInterface $dataProvider * @return $this */ public function setDataProvider(DataProviderInterface $dataProvider) { $this->dataProvider = $dataProvider; + return $this; } /** + * Returns rss xml + * * @return string - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\RuntimeException + * @throws InputException + * @throws RuntimeException */ public function createRssXml() { $feed = $this->feedFactory->create($this->getFeeds(), FeedFactoryInterface::FORMAT_RSS); + return $feed->getFormattedContent(); } } diff --git a/app/code/Magento/Rss/Test/Mftf/Page/StorefrontRssPage.xml b/app/code/Magento/Rss/Test/Mftf/Page/StorefrontRssPage.xml new file mode 100644 index 0000000000000..d0559af3e5370 --- /dev/null +++ b/app/code/Magento/Rss/Test/Mftf/Page/StorefrontRssPage.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="StorefrontRssPage" url="/rss/" area="storefront" module="Magento_Rss"> + <section name="StorefrontRssListSection"/> + </page> +</pages> diff --git a/app/code/Magento/Rss/Test/Mftf/Section/StorefrontRssListSection.xml b/app/code/Magento/Rss/Test/Mftf/Section/StorefrontRssListSection.xml new file mode 100644 index 0000000000000..839e937887ec1 --- /dev/null +++ b/app/code/Magento/Rss/Test/Mftf/Section/StorefrontRssListSection.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="StorefrontRssListSection"> + <element name="rssTable" type="block" selector="table.rss"/> + <element name="rssLink" type="text" selector="table.rss tr:nth-of-type(2) > td.action a"/> + </section> +</sections> diff --git a/app/code/Magento/Rss/Test/Mftf/Test/RssListTest.xml b/app/code/Magento/Rss/Test/Mftf/Test/RssListTest.xml new file mode 100644 index 0000000000000..a9f8e96c0bc1c --- /dev/null +++ b/app/code/Magento/Rss/Test/Mftf/Test/RssListTest.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="RssListTest"> + <annotations> + <group value="Rss"/> + <stories value="RSS Feed available to view"/> + <title value="RSS Feed"/> + <description value="View selected RSS feed by link."/> + <testCaseId value="MC-36686"/> + <severity value="AVERAGE"/> + </annotations> + <before> + <createData entity="SimpleProductWithNewFromDate" stepKey="createProduct"/> + <magentoCLI command="config:set rss/config/active 1" stepKey="enableRss"/> + <magentoCLI command="config:set rss/catalog/new 1" stepKey="enableRssForCatalogNewProducts"/> + <magentoCLI command="cache:clean" stepKey="cleanCache"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set rss/config/active 0" stepKey="disableRss"/> + <magentoCLI command="config:set rss/catalog/new 0" stepKey="disableRssForCatalogNewProducts"/> + <magentoCLI command="cache:clean" stepKey="cleanCache"/> + </after> + + <amOnPage url="{{StorefrontRssPage.url}}" stepKey="goToRssPage"/> + <seeElement selector="{{StorefrontRssListSection.rssTable}}" stepKey="seeRssList"/> + <click selector="{{StorefrontRssListSection.rssLink}}" stepKey="clickRssLink"/> + <seeInCurrentUrl url="rss/feed/index/type/new_products/" stepKey="seeInUrl"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="New Products from Main Website Store" stepKey="seeText" /> + + </test> +</tests> diff --git a/app/code/Magento/Rss/Test/Unit/Model/RssTest.php b/app/code/Magento/Rss/Test/Unit/Model/RssTest.php index f2694fc81dab4..4e245f8b440be 100644 --- a/app/code/Magento/Rss/Test/Unit/Model/RssTest.php +++ b/app/code/Magento/Rss/Test/Unit/Model/RssTest.php @@ -17,12 +17,17 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Rss\Model\Rss. + */ class RssTest extends TestCase { + private const STUB_SERIALIZED_DATA = 'serializedData'; + /** * @var Rss */ - protected $rss; + private $rss; /** * @var array @@ -62,11 +67,6 @@ class RssTest extends TestCase </channel> </rss>'; - /** - * @var ObjectManagerHelper - */ - protected $objectManagerHelper; - /** * @var CacheInterface|MockObject */ @@ -87,6 +87,9 @@ class RssTest extends TestCase */ private $serializerMock; + /** + * @inheritDoc + */ protected function setUp(): void { $this->cacheMock = $this->getMockForAbstractClass(CacheInterface::class); @@ -94,8 +97,8 @@ protected function setUp(): void $this->feedFactoryMock = $this->getMockForAbstractClass(FeedFactoryInterface::class); $this->feedMock = $this->getMockForAbstractClass(FeedInterface::class); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->rss = $this->objectManagerHelper->getObject( + $objectManagerHelper = new ObjectManagerHelper($this); + $this->rss = $objectManagerHelper->getObject( Rss::class, [ 'cache' => $this->cacheMock, @@ -105,12 +108,23 @@ protected function setUp(): void ); } - public function testGetFeeds() + /** + * Get feeds test + * + * @return void + */ + public function testGetFeeds(): void { $dataProvider = $this->getMockForAbstractClass(DataProviderInterface::class); - $dataProvider->expects($this->any())->method('getCacheKey')->willReturn('cache_key'); - $dataProvider->expects($this->any())->method('getCacheLifetime')->willReturn(100); - $dataProvider->expects($this->any())->method('getRssData')->willReturn($this->feedData); + $dataProvider->expects($this->atLeastOnce()) + ->method('getCacheKey') + ->willReturn('cache_key'); + $dataProvider->expects($this->atLeastOnce()) + ->method('getCacheLifetime') + ->willReturn(100); + $dataProvider->expects($this->once()) + ->method('getRssData') + ->willReturn($this->feedData); $this->rss->setDataProvider($dataProvider); @@ -125,7 +139,11 @@ public function testGetFeeds() $this->serializerMock->expects($this->once()) ->method('serialize') ->with($this->feedData) - ->willReturn('serializedData'); + ->willReturn(self::STUB_SERIALIZED_DATA); + $this->serializerMock->expects($this->once()) + ->method('unserialize') + ->with(self::STUB_SERIALIZED_DATA) + ->willReturn($this->feedData); $this->assertEquals($this->feedData, $this->rss->getFeeds()); } @@ -168,6 +186,14 @@ public function testCreateRssXml() ->with($this->feedData, FeedFactoryInterface::FORMAT_RSS) ->willReturn($this->feedMock); + $this->serializerMock->expects($this->once()) + ->method('serialize') + ->willReturn(self::STUB_SERIALIZED_DATA); + $this->serializerMock->expects($this->once()) + ->method('unserialize') + ->with(self::STUB_SERIALIZED_DATA) + ->willReturn($this->feedData); + $this->rss->setDataProvider($dataProvider); $this->assertNotNull($this->rss->createRssXml()); } diff --git a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php index 2206dbef38640..7d40dbfe652f4 100644 --- a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php +++ b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php @@ -12,6 +12,7 @@ /** * Abstract Rule product condition data model * + * phpcs:disable Magento2.Classes.AbstractApi * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api @@ -661,19 +662,7 @@ public function validateByEntityId($productId) */ protected function _getAvailableInCategories($productId) { - return $this->_productResource->getConnection() - ->fetchCol( - $this->_productResource->getConnection() - ->select() - ->distinct() - ->from( - $this->_productResource->getTable('catalog_category_product'), - ['category_id'] - )->where( - 'product_id = ?', - $productId - ) - ); + return $this->productCategoryList->getCategoryIds($productId); } /** diff --git a/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php b/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php new file mode 100644 index 0000000000000..a8a9f78df7f28 --- /dev/null +++ b/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Rule\Test\Mftf\Helper; + +use Facebook\WebDriver\Remote\RemoteWebDriver as FacebookWebDriver; +use Facebook\WebDriver\WebDriverBy; +use Magento\FunctionalTestingFramework\Helper\Helper; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; + +/** + * Class for MFTF helpers for CatalogRule module. + */ +class RuleHelper extends Helper +{ + /** + * Delete all Catalog Price Rules obe by one. + * + * @param string $emptyRow + * @param string $modalAceptButton + * @param string $deleteButton + * @param string $successMessageContainer + * @param string $successMessage + * + * @return void + */ + public function deleteAllRulesOneByOne( + string $firstNotEmptyRow, + string $modalAcceptButton, + string $deleteButton, + string $successMessageContainer, + string $successMessage + ): void { + try { + /** @var MagentoWebDriver $webDriver */ + $magentoWebDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + /** @var FacebookWebDriver $webDriver */ + $webDriver = $magentoWebDriver->webDriver; + $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow)); + while (!empty($rows)) { + $rows[0]->click(); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->click($deleteButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForLoadingMaskToDisappear(); + $magentoWebDriver->waitForElementVisible($successMessageContainer, 10); + $magentoWebDriver->see($successMessage, $successMessageContainer); + $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow)); + } + } catch (\Exception $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/app/code/Magento/Sales/Api/Data/OrderPaymentInterface.php b/app/code/Magento/Sales/Api/Data/OrderPaymentInterface.php index ac400206b8a2f..8f9ab43313968 100644 --- a/app/code/Magento/Sales/Api/Data/OrderPaymentInterface.php +++ b/app/code/Magento/Sales/Api/Data/OrderPaymentInterface.php @@ -1047,6 +1047,7 @@ public function setCcTransId($id); * * @param string[] $additionalInformation * @return $this + * @since 102.1.0 */ public function setAdditionalInformation($additionalInformation); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php index 87c15e474d11f..10d58e1f312c4 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php @@ -5,7 +5,9 @@ */ namespace Magento\Sales\Block\Adminhtml\Items\Column; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Filter\TruncateFilter\Result; +use Magento\Catalog\Helper\Data as CatalogHelper; /** * Sales Order items name column renderer @@ -15,6 +17,28 @@ */ class Name extends \Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn { + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration + * @param \Magento\Framework\Registry $registry + * @param \Magento\Catalog\Model\Product\OptionFactory $optionFactory + * @param array $data + * @param CatalogHelper|null $catalogHelper + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, + \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, + \Magento\Framework\Registry $registry, + \Magento\Catalog\Model\Product\OptionFactory $optionFactory, + array $data = [], + ?CatalogHelper $catalogHelper = null + ) { + $data['catalogHelper'] = $catalogHelper ?? ObjectManager::getInstance()->get(CatalogHelper::class); + parent::__construct($context, $stockRegistry, $stockConfiguration, $registry, $optionFactory, $data); + } + /** * @var Result */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php b/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php index b9aff07cc96fd..e45405714956f 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php @@ -6,6 +6,9 @@ namespace Magento\Sales\Block\Adminhtml\Order; use Magento\Sales\Model\Order; +use Magento\Framework\App\ObjectManager; +use Magento\Shipping\Helper\Data as ShippingHelper; +use Magento\Tax\Helper\Data as TaxHelper; /** * Adminhtml order abstract block @@ -35,15 +38,21 @@ class AbstractOrder extends \Magento\Backend\Block\Widget * @param \Magento\Framework\Registry $registry * @param \Magento\Sales\Helper\Admin $adminHelper * @param array $data + * @param ShippingHelper|null $shippingHelper + * @param TaxHelper|null $taxHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Registry $registry, \Magento\Sales\Helper\Admin $adminHelper, - array $data = [] + array $data = [], + ?ShippingHelper $shippingHelper = null, + ?TaxHelper $taxHelper = null ) { $this->_adminHelper = $adminHelper; $this->_coreRegistry = $registry; + $data['shippingHelper'] = $shippingHelper ?? ObjectManager::getInstance()->get(ShippingHelper::class); + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php index b314ee24c3e27..6a1c16cd5d73a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php @@ -50,6 +50,7 @@ public function __construct( ) { $this->_messageHelper = $messageHelper; $this->_giftMessageSave = $giftMessageSave; + $data['giftMessageHelper'] = $messageHelper; parent::__construct($context, $sessionQuote, $orderCreate, $priceCurrency, $data); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Items/Grid.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Items/Grid.php index 8a427a30a6c7a..8ec07f9765204 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Items/Grid.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Items/Grid.php @@ -8,9 +8,11 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Api\StockStateInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Session\SessionManagerInterface; use Magento\Quote\Model\Quote\Item; +use Magento\Catalog\Helper\Data as CatalogHelper; /** * Adminhtml sales order create items grid block @@ -85,6 +87,7 @@ class Grid extends \Magento\Sales\Block\Adminhtml\Order\Create\AbstractCreate * @param StockRegistryInterface $stockRegistry * @param StockStateInterface $stockState * @param array $data + * @param CatalogHelper|null $catalogHelper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -99,7 +102,8 @@ public function __construct( \Magento\GiftMessage\Helper\Message $messageHelper, StockRegistryInterface $stockRegistry, StockStateInterface $stockState, - array $data = [] + array $data = [], + ?CatalogHelper $catalogHelper = null ) { $this->_messageHelper = $messageHelper; $this->_wishlistFactory = $wishlistFactory; @@ -108,6 +112,7 @@ public function __construct( $this->_taxData = $taxData; $this->stockRegistry = $stockRegistry; $this->stockState = $stockState; + $data['catalogHelper'] = $catalogHelper ?? ObjectManager::getInstance()->get(CatalogHelper::class); parent::__construct($context, $sessionQuote, $orderCreate, $priceCurrency, $data); } @@ -428,7 +433,7 @@ protected function _getTierPriceInfo($prices) * @param Item $item * @return string * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function getCustomOptions(Item $item) { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/Renderer/Product.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/Renderer/Product.php index 271b5b7afa1c9..8c1ef5f56dac2 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/Renderer/Product.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/Renderer/Product.php @@ -5,6 +5,10 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\Renderer; +use Magento\Backend\Block\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Adminhtml sales create order product search grid product name column renderer * @@ -12,6 +16,22 @@ */ class Product extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Text { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct(Context $context, array $data = [], ?SecureHtmlRenderer $secureRenderer = null) + { + parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Render product name to add Configure link * @@ -28,10 +48,14 @@ public function render(\Magento\Framework\DataObject $row) $row->getId() ) : 'disabled="disabled"'; return sprintf( - '<a href="javascript:void(0)" class="action-configure %s" %s>%s</a>', + '<a href="#" id="search-grid-product-' . $row->getId() . '" class="action-configure %s" %s>%s</a>', $style, $prodAttributes, __('Configure') + ) . $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#search-grid-product-' . $row->getId() ) . $rendered; } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/Method/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/Method/Form.php index 1fd1c28c20727..0b926e8415e41 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/Method/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/Method/Form.php @@ -52,6 +52,7 @@ public function __construct( array $data = [] ) { $this->_taxData = $taxData; + $data['taxHelper'] = $this->_taxData; parent::__construct($context, $sessionQuote, $orderCreate, $priceCurrency, $data); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php index a927b7177294a..77765b242001f 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php @@ -66,6 +66,7 @@ public function getItemCollection() /** * @inheritdoc + * @since 102.0.1 */ public function getItemPrice(Product $product) { @@ -150,6 +151,7 @@ private function getCartItemCustomPrice(Product $product): ?float /** * @inheritdoc + * @since 102.0.4 */ public function getItemCount() { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php index b4f2e132f6de4..e81d1f9589405 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php @@ -46,23 +46,15 @@ public function getItemCollection() $collection = $this->getData('item_collection'); if ($collection === null) { if ($collection = $this->getCreateOrderModel()->getCustomerCompareList()) { - $collection = $collection->getItemCollection()->useProductItem( - true - )->setStoreId( - $this->getQuote()->getStoreId() - )->addStoreFilter( - $this->getQuote()->getStoreId() - )->setCustomerId( - $this->getCustomerId() - )->addAttributeToSelect( - 'name' - )->addAttributeToSelect( - 'price' - )->addAttributeToSelect( - 'image' - )->addAttributeToSelect( - 'status' - )->load(); + $collection = $collection->getItemCollection() + ->useProductItem() + ->setStoreId($this->getQuote()->getStoreId()) + ->addStoreFilter($this->getQuote()->getStoreId()) + ->setCustomerId($this->getCustomerId()) + ->addAttributeToSelect('name') + ->addAttributeToSelect('price')->addAttributeToSelect('image') + ->addAttributeToSelect('status') + ->load(); } $this->setData('item_collection', $collection); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php index 8442d5b36466e..c9f251621f9de 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php @@ -89,13 +89,11 @@ public function getItemCollection() // get products to skip $skipProducts = []; if ($collection = $this->getCreateOrderModel()->getCustomerCompareList()) { - $collection = $collection->getItemCollection()->useProductItem( - true - )->setStoreId( - $this->getStoreId() - )->setCustomerId( - $this->getCustomerId() - )->load(); + $collection = $collection->getItemCollection() + ->useProductItem() + ->setStoreId($this->getStoreId()) + ->setCustomerId($this->getCustomerId()) + ->load(); foreach ($collection as $_item) { $skipProducts[] = $_item->getProductId(); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php index 207a4eca60213..165875955baa2 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php @@ -5,6 +5,10 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Totals; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Tax\Helper\Data as TaxHelper; + /** * Tax Total Row Renderer * @@ -13,6 +17,30 @@ */ class Tax extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\DefaultTotals { + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Backend\Model\Session\Quote $sessionQuote + * @param \Magento\Sales\Model\AdminOrder\Create $orderCreate + * @param PriceCurrencyInterface $priceCurrency + * @param \Magento\Sales\Helper\Data $salesData + * @param \Magento\Sales\Model\Config $salesConfig + * @param array $data + * @param TaxHelper|null $taxHelper + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Backend\Model\Session\Quote $sessionQuote, + \Magento\Sales\Model\AdminOrder\Create $orderCreate, + PriceCurrencyInterface $priceCurrency, + \Magento\Sales\Helper\Data $salesData, + \Magento\Sales\Model\Config $salesConfig, + array $data = [], + ?TaxHelper $taxHelper = null + ) { + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); + parent::__construct($context, $sessionQuote, $orderCreate, $priceCurrency, $salesData, $salesConfig, $data); + } + /** * Template * diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php index a7649fecaf2bb..781fb3b7501b5 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php @@ -76,6 +76,7 @@ public function initTotals() * @param null|float $value * * @return string + * @since 102.1.0 */ public function formatValue($value) { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php index 261f4b0cfd12a..51d2bfc6326ed 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php @@ -5,12 +5,29 @@ */ namespace Magento\Sales\Block\Adminhtml\Order; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; + /** - * Class Details - * @package Magento\Sales\Block\Adminhtml\Order + * Order Details */ class Details extends \Magento\Framework\View\Element\Template { + /** + * @param Template\Context $context + * @param array $data + * @param Message|null $giftMessageHelper + */ + public function __construct( + Template\Context $context, + array $data = [], + ?GiftMessageHelper $giftMessageHelper = null + ) { + $data['giftMessageHelper'] = $giftMessageHelper ?? ObjectManager::getInstance()->get(GiftMessageHelper::class); + parent::__construct($context, $data); + } + /** * @var string */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/Form.php index 3eb6cce37f567..3f41eb5ba7d8e 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Invoice\Create; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; + /** * Adminhtml invoice create form * @@ -14,6 +17,24 @@ */ class Form extends \Magento\Sales\Block\Adminhtml\Order\AbstractOrder { + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Sales\Helper\Admin $adminHelper + * @param array $data + * @param TaxHelper|null $taxHelper + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Sales\Helper\Admin $adminHelper, + array $data = [], + ?TaxHelper $taxHelper = null + ) { + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); + parent::__construct($context, $registry, $adminHelper, $data); + } + /** * Retrieve invoice order * diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php index c4ce48d162c2c..33e5250d27d26 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php @@ -8,7 +8,7 @@ /** * Adminhtml creditmemo bar * - * @deprecated + * @deprecated 101.0.6 * @api * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Totals.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Totals.php index a9f7bf3516517..6cd2c53f894f8 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Totals.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Totals.php @@ -42,13 +42,23 @@ protected function _initTotals() 'area' => 'footer', ] ); - $this->_totals['due'] = new \Magento\Framework\DataObject( + $code = 'due'; + $label = 'Total Due'; + $value = $this->getSource()->getTotalDue(); + $baseValue = $this->getSource()->getBaseTotalDue(); + if ($this->getSource()->getTotalCanceled() > 0 && $this->getSource()->getBaseTotalCanceled() > 0) { + $code = 'canceled'; + $label = 'Total Canceled'; + $value = $this->getSource()->getTotalCanceled(); + $baseValue = $this->getSource()->getBaseTotalCanceled(); + } + $this->_totals[$code] = new \Magento\Framework\DataObject( [ 'code' => 'due', 'strong' => true, - 'value' => $this->getSource()->getTotalDue(), - 'base_value' => $this->getSource()->getBaseTotalDue(), - 'label' => __('Total Due'), + 'value' => $value, + 'base_value' => $baseValue, + 'label' => __($label), 'area' => 'footer', ] ); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Totals/Tax.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Totals/Tax.php index 4b0969598fdcd..e923b006a0ac6 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Totals/Tax.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Totals/Tax.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Totals; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; + /** * Adminhtml order tax totals block * @@ -50,6 +53,7 @@ class Tax extends \Magento\Tax\Block\Sales\Order\Tax * @param \Magento\Tax\Model\Sales\Order\TaxFactory $taxOrderFactory * @param \Magento\Sales\Helper\Admin $salesAdminHelper * @param array $data + * @param Random $randomHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -58,12 +62,15 @@ public function __construct( \Magento\Tax\Model\Calculation $taxCalculation, \Magento\Tax\Model\Sales\Order\TaxFactory $taxOrderFactory, \Magento\Sales\Helper\Admin $salesAdminHelper, - array $data = [] + array $data = [], + ?Random $randomHelper = null ) { $this->_taxHelper = $taxHelper; $this->_taxCalculation = $taxCalculation; $this->_taxOrderFactory = $taxOrderFactory; $this->_salesAdminHelper = $salesAdminHelper; + $data['taxHelper'] = $this->_taxHelper; + $data['randomHelper'] = $randomHelper ?? ObjectManager::getInstance()->get(Random::class); parent::__construct($context, $taxConfig, $data); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php index 598a3e226a879..22f61d3583faa 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php @@ -305,7 +305,7 @@ public function getFormattedAddress(Address $address) /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function getChildHtml($alias = '', $useCache = true) { diff --git a/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php b/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php index 0a1e87e5e0a27..bfb668a674095 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php @@ -71,6 +71,7 @@ protected function _prepareItem(\Magento\Framework\View\Element\AbstractBlock $r * For legacy custom email templates it can pass as an object. * * @return OrderInterface|null + * @since 102.1.0 */ public function getOrder() { @@ -96,6 +97,7 @@ public function getOrder() * For legacy custom email templates it can pass as an object. * * @return CreditmemoInterface|null + * @since 102.1.0 */ public function getCreditmemo() { diff --git a/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php b/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php index cc2b197ab0eb2..7b5389a54e878 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php @@ -71,6 +71,7 @@ protected function _prepareItem(\Magento\Framework\View\Element\AbstractBlock $r * For legacy custom email templates it can pass as an object. * * @return OrderInterface|null + * @since 102.1.0 */ public function getOrder() { @@ -96,6 +97,7 @@ public function getOrder() * For legacy custom email templates it can pass as an object. * * @return InvoiceInterface|null + * @since 102.1.0 */ public function getInvoice() { diff --git a/app/code/Magento/Sales/Block/Order/Email/Items.php b/app/code/Magento/Sales/Block/Order/Email/Items.php index e11981285f04f..8a7256d1f1175 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items.php @@ -52,6 +52,7 @@ public function __construct( * For legacy custom email templates it can pass as an object. * * @return OrderInterface|null + * @since 102.1.0 */ public function getOrder() { diff --git a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php index 064405daf89a8..cbb79f188f231 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Block\Order\Email\Items; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Element\Template; use Magento\Sales\Model\Order\Creditmemo\Item as CreditmemoItem; use Magento\Sales\Model\Order\Invoice\Item as InvoiceItem; use Magento\Sales\Model\Order\Item as OrderItem; @@ -16,7 +20,7 @@ * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ -class DefaultItems extends \Magento\Framework\View\Element\Template +class DefaultItems extends Template { /** * Retrieve current order model instance @@ -92,6 +96,7 @@ public function getSku($item) * Return product additional information block * * @return \Magento\Framework\View\Element\AbstractBlock + * @throws LocalizedException */ public function getProductAdditionalInformationBlock() { @@ -103,10 +108,13 @@ public function getProductAdditionalInformationBlock() * * @param OrderItem|InvoiceItem|CreditmemoItem $item * @return string + * @throws LocalizedException */ public function getItemPrice($item) { $block = $this->getLayout()->getBlock('item_price'); + $item->setRowTotal((float) $item->getPrice() * (float) $this->getItem()->getQty()); + $item->setBaseRowTotal((float) $item->getBasePrice() * (float) $this->getItem()->getQty()); $block->setItem($item); return $block->toHtml(); } diff --git a/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php b/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php index 1f9b353180fd9..db7fa6b03715a 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php @@ -71,6 +71,7 @@ protected function _prepareItem(\Magento\Framework\View\Element\AbstractBlock $r * For legacy custom email templates it can pass as an object. * * @return OrderInterface|null + * @since 102.1.0 */ public function getOrder() { @@ -96,6 +97,7 @@ public function getOrder() * For legacy custom email templates it can pass as an object. * * @return ShipmentInterface|null + * @since 102.1.0 */ public function getShipment() { diff --git a/app/code/Magento/Sales/Block/Order/History.php b/app/code/Magento/Sales/Block/Order/History.php index 09300424212fe..98b1ccfc5b2e8 100644 --- a/app/code/Magento/Sales/Block/Order/History.php +++ b/app/code/Magento/Sales/Block/Order/History.php @@ -158,7 +158,7 @@ public function getViewUrl($order) * * @param object $order * @return string - * @deprecated Action does not exist + * @deprecated 102.0.3 Action does not exist * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getTrackUrl($order) @@ -193,6 +193,7 @@ public function getBackUrl() * Get message for no orders. * * @return \Magento\Framework\Phrase + * @since 102.1.0 */ public function getEmptyOrdersMessage() { diff --git a/app/code/Magento/Sales/Block/Order/PrintShipment.php b/app/code/Magento/Sales/Block/Order/PrintShipment.php index 0006a38f0f1ce..039bf2c79e78b 100644 --- a/app/code/Magento/Sales/Block/Order/PrintShipment.php +++ b/app/code/Magento/Sales/Block/Order/PrintShipment.php @@ -88,7 +88,7 @@ public function getOrder() * Disable pager for printing page * * @return bool - * @since 100.2.0 + * @since 100.1.9 */ public function isPagerDisplayed() { @@ -99,7 +99,7 @@ public function isPagerDisplayed() * Get order items * * @return \Magento\Framework\DataObject[] - * @since 100.2.0 + * @since 100.1.9 */ public function getItems() { diff --git a/app/code/Magento/Sales/Block/Order/Recent.php b/app/code/Magento/Sales/Block/Order/Recent.php index 79119c1851347..934f1b5efdcdd 100644 --- a/app/code/Magento/Sales/Block/Order/Recent.php +++ b/app/code/Magento/Sales/Block/Order/Recent.php @@ -120,7 +120,7 @@ public function getViewUrl($order) * * @param object $order * @return string - * @deprecated Action does not exist + * @deprecated 102.0.3 Action does not exist * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getTrackUrl($order) diff --git a/app/code/Magento/Sales/Block/Order/View.php b/app/code/Magento/Sales/Block/Order/View.php index 03d1340e0f690..eef13fd47bf94 100644 --- a/app/code/Magento/Sales/Block/Order/View.php +++ b/app/code/Magento/Sales/Block/Order/View.php @@ -29,7 +29,7 @@ class View extends \Magento\Framework\View\Element\Template /** * @var \Magento\Framework\App\Http\Context - * @since 100.2.0 + * @since 101.0.0 */ protected $httpContext; diff --git a/app/code/Magento/Sales/Controller/AbstractController/Reorder.php b/app/code/Magento/Sales/Controller/AbstractController/Reorder.php index 062ad78e5001d..5eb485e262193 100644 --- a/app/code/Magento/Sales/Controller/AbstractController/Reorder.php +++ b/app/code/Magento/Sales/Controller/AbstractController/Reorder.php @@ -8,6 +8,7 @@ namespace Magento\Sales\Controller\AbstractController; +use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Framework\App\Action; use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\ObjectManager; @@ -35,6 +36,11 @@ abstract class Reorder extends Action\Action implements HttpPostActionInterface */ private $reorder; + /** + * @var CheckoutSession + */ + private $checkoutSession; + /** * Constructor * @@ -43,6 +49,7 @@ abstract class Reorder extends Action\Action implements HttpPostActionInterface * @param Registry $registry * @param ReorderHelper|null $reorderHelper * @param \Magento\Sales\Model\Reorder\Reorder|null $reorder + * @param CheckoutSession|null $checkoutSession * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -50,12 +57,14 @@ public function __construct( OrderLoaderInterface $orderLoader, Registry $registry, ReorderHelper $reorderHelper = null, - \Magento\Sales\Model\Reorder\Reorder $reorder = null + \Magento\Sales\Model\Reorder\Reorder $reorder = null, + CheckoutSession $checkoutSession = null ) { $this->orderLoader = $orderLoader; $this->_coreRegistry = $registry; parent::__construct($context); $this->reorder = $reorder ?: ObjectManager::getInstance()->get(\Magento\Sales\Model\Reorder\Reorder::class); + $this->checkoutSession = $checkoutSession ?: ObjectManager::getInstance()->get(CheckoutSession::class); } /** @@ -81,6 +90,10 @@ public function execute() return $resultRedirect->setPath('checkout/cart'); } + // Set quote id for guest session: \Magento\Quote\Api\CartRepositoryInterface::save doesn't set quote id + // to session for guest customer, as it does \Magento\Checkout\Model\Cart::save which is deprecated. + $this->checkoutSession->setQuoteId($reorderOutput->getCart()->getId()); + $errors = $reorderOutput->getErrors(); if (!empty($errors)) { $useNotice = $this->_objectManager->get(\Magento\Checkout\Model\Session::class)->getUseNotice(true); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php index c6b45f282debc..1f75008897102 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php @@ -14,7 +14,7 @@ /** * Class AbstractMassStatus - * @deprecated 100.2.0 + * @deprecated 101.0.0 * Never extend from this action. Implement mass-action logic in the "execute" method of your controller. */ abstract class AbstractMassAction extends \Magento\Backend\App\Action diff --git a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php index fc4e238d47c99..069b2783076d9 100644 --- a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php +++ b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php @@ -33,7 +33,7 @@ class DownloadCustomOption extends \Magento\Framework\App\Action\Action implemen /** * @var \Magento\Framework\Unserialize\Unserialize - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $unserialize; diff --git a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php index a3242228b28e0..978aec1b79ec4 100644 --- a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php +++ b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php @@ -5,12 +5,15 @@ */ namespace Magento\Sales\Cron; -use Magento\Quote\Model\ResourceModel\Quote\Collection; +use Exception; +use Magento\Quote\Model\QuoteRepository; +use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection; use Magento\Sales\Model\ResourceModel\Collection\ExpiredQuotesCollection; use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; /** - * Class CleanExpiredQuotes + * Cron job for cleaning expired Quotes */ class CleanExpiredQuotes { @@ -24,16 +27,32 @@ class CleanExpiredQuotes */ private $storeManager; + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var LoggerInterface + */ + private $logger; + /** * @param StoreManagerInterface $storeManager * @param ExpiredQuotesCollection $expiredQuotesCollection + * @param QuoteRepository $quoteRepository + * @param LoggerInterface $logger */ public function __construct( StoreManagerInterface $storeManager, - ExpiredQuotesCollection $expiredQuotesCollection + ExpiredQuotesCollection $expiredQuotesCollection, + QuoteRepository $quoteRepository, + LoggerInterface $logger ) { $this->storeManager = $storeManager; $this->expiredQuotesCollection = $expiredQuotesCollection; + $this->quoteRepository = $quoteRepository; + $this->logger = $logger; } /** @@ -45,9 +64,41 @@ public function execute() { $stores = $this->storeManager->getStores(true); foreach ($stores as $store) { - /** @var $quotes Collection */ - $quotes = $this->expiredQuotesCollection->getExpiredQuotes($store); - $quotes->walk('delete'); + /** @var $quoteCollection QuoteCollection */ + $quoteCollection = $this->expiredQuotesCollection->getExpiredQuotes($store); + $quoteCollection->setPageSize(50); + + // Last page returns 1 even when we don't have any results + $lastPage = $quoteCollection->getSize() ? $quoteCollection->getLastPageNumber() : 0; + + for ($currentPage = $lastPage; $currentPage >= 1; $currentPage--) { + $quoteCollection->setCurPage($currentPage); + + $this->deleteQuotes($quoteCollection); + } } } + + /** + * Deletes all quotes in collection + * + * @param QuoteCollection $quoteCollection + */ + private function deleteQuotes(QuoteCollection $quoteCollection): void + { + foreach ($quoteCollection as $quote) { + try { + $this->quoteRepository->delete($quote); + } catch (Exception $e) { + $message = sprintf( + 'Unable to delete expired quote (ID: %s): %s', + $quote->getId(), + (string)$e + ); + $this->logger->error($message); + } + } + + $quoteCollection->clear(); + } } diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 67a533ea88550..8ef12e5889520 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -663,6 +663,14 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q if (is_numeric($qty)) { $buyRequest->setQty($qty); } + $productOptions = $orderItem->getProductOptions(); + if ($productOptions !== null && !empty($productOptions['options'])) { + $formattedOptions = []; + foreach ($productOptions['options'] as $option) { + $formattedOptions[$option['option_id']] = $option['option_value']; + } + $buyRequest->setData('options', $formattedOptions); + } $item = $this->getQuote()->addProduct($product, $buyRequest); if (is_string($item)) { return $item; @@ -737,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); + } } } @@ -777,6 +787,7 @@ public function getCustomerCompareList() public function getCustomerGroupId() { $groupId = $this->getQuote()->getCustomerGroupId(); + // @phpstan-ignore-next-line if (!isset($groupId)) { $groupId = $this->getSession()->getCustomerGroupId(); } @@ -1151,7 +1162,7 @@ public function updateQuoteItems($items) * @return array * @throws \Magento\Framework\Exception\LocalizedException * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function _parseOptions(\Magento\Quote\Model\Quote\Item $item, $additionalOptions) { @@ -1221,7 +1232,7 @@ protected function _parseOptions(\Magento\Quote\Model\Quote\Item $item, $additio * @param array $options * @return $this * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function _assignOptionsToItem(\Magento\Quote\Model\Quote\Item $item, $options) { @@ -1369,7 +1380,6 @@ protected function _setQuoteAddress(\Magento\Quote\Model\Quote\Address $address, $data = isset($data['region']) && is_array($data['region']) ? array_merge($data, $data['region']) : $data; $addressForm = $this->_metadataFormFactory->create( - AddressMetadataInterface::ENTITY_TYPE_ADDRESS, 'adminhtml_customer_address', $data, @@ -1436,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/Increment.php b/app/code/Magento/Sales/Model/Increment.php index 75ff1ee044a95..813b3dcc40a7a 100644 --- a/app/code/Magento/Sales/Model/Increment.php +++ b/app/code/Magento/Sales/Model/Increment.php @@ -9,7 +9,7 @@ /** * Class Increment - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ class Increment { diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 598b204a33097..0af42b0a99d09 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -193,7 +193,7 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface /** * @var \Magento\Catalog\Api\ProductRepositoryInterface - * @deprecated 100.1.7 Remove unused dependency. + * @deprecated 100.1.0 Remove unused dependency. */ protected $productRepository; @@ -716,7 +716,7 @@ private function canCreditmemoForZeroTotal($totalRefunded) $hasDueAmount = $this->canInvoice() && ($checkAmtTotalPaid); //case when paid amount is refunded and order has creditmemo created $creditmemos = ($this->getCreditmemosCollection() === false) ? - true : (count($this->getCreditmemosCollection()) > 0); + true : ($this->_memoCollectionFactory->create()->setOrderFilter($this)->getTotalCount() > 0); $paidAmtIsRefunded = $this->getTotalRefunded() == $totalPaid && $creditmemos; if (($hasDueAmount || $paidAmtIsRefunded) || (!$checkAmtTotalPaid && @@ -1076,6 +1076,7 @@ public function setState($state) * Retrieve frontend label of order status * * @return string + * @since 102.0.1 */ public function getFrontendStatusLabel() { @@ -1115,7 +1116,7 @@ public function addStatusToHistory($status, $comment = '', $isCustomerNotified = * @param string $comment * @param bool|string $status * @return OrderStatusHistoryInterface - * @deprecated + * @deprecated 101.0.5 * @see addCommentToStatusHistory */ public function addStatusHistoryComment($comment, $status = false) @@ -1132,6 +1133,7 @@ public function addStatusHistoryComment($comment, $status = false) * @param bool|string $status * @param bool $isVisibleOnFront * @return OrderStatusHistoryInterface + * @since 101.0.5 */ public function addCommentToStatusHistory($comment, $status = false, $isVisibleOnFront = false) { @@ -1816,7 +1818,7 @@ public function getTotalDue() $total = $this->priceCurrency->round($total); return max($total, 0); } - + /** * Retrieve order total due value * diff --git a/app/code/Magento/Sales/Model/Order/Address.php b/app/code/Magento/Sales/Model/Order/Address.php index 9b8f4e79c23fa..0fd4555238ed5 100644 --- a/app/code/Magento/Sales/Model/Order/Address.php +++ b/app/code/Magento/Sales/Model/Order/Address.php @@ -732,6 +732,7 @@ public function setExtensionAttributes(\Magento\Sales\Api\Data\OrderAddressExten /** * @inheritdoc + * @since 102.0.3 */ public function beforeSave() { diff --git a/app/code/Magento/Sales/Model/Order/AddressRepository.php b/app/code/Magento/Sales/Model/Order/AddressRepository.php index deeeb16b7714c..1a700826dbc3f 100644 --- a/app/code/Magento/Sales/Model/Order/AddressRepository.php +++ b/app/code/Magento/Sales/Model/Order/AddressRepository.php @@ -240,7 +240,7 @@ public function create() /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/Order/Config.php b/app/code/Magento/Sales/Model/Order/Config.php index 92681f3ecf181..32b9298be2b5f 100644 --- a/app/code/Magento/Sales/Model/Order/Config.php +++ b/app/code/Magento/Sales/Model/Order/Config.php @@ -157,6 +157,7 @@ public function getStatusLabel($code) * * @param string|null $code * @return string|null + * @since 102.0.1 */ public function getStatusFrontendLabel(?string $code): ?string { @@ -307,7 +308,7 @@ protected function _getStatuses($visibility) * @param string $state * @param string $status * @return \Magento\Framework\Phrase|string - * @since 100.2.0 + * @since 101.0.0 */ public function getStateLabelByStateAndStatus($state, $status) { diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php index 1278d156ba869..80053210900c3 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php @@ -32,7 +32,7 @@ class CreditmemoFactory /** * @var \Magento\Framework\Unserialize\Unserialize - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $unserialize; diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php b/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php index ce4a0aa5b3e3a..269ee313c09df 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php @@ -151,7 +151,7 @@ public function save(\Magento\Sales\Api\Data\CreditmemoInterface $entity) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/Order/CustomerManagement.php b/app/code/Magento/Sales/Model/Order/CustomerManagement.php index ae3f940dbb2ba..50c7f88af546b 100644 --- a/app/code/Magento/Sales/Model/Order/CustomerManagement.php +++ b/app/code/Magento/Sales/Model/Order/CustomerManagement.php @@ -27,17 +27,17 @@ class CustomerManagement implements \Magento\Sales\Api\OrderCustomerManagementIn protected $accountManagement; /** - * @deprecated + * @deprecated 101.0.4 */ protected $customerFactory; /** - * @deprecated + * @deprecated 101.0.4 */ protected $addressFactory; /** - * @deprecated + * @deprecated 101.0.4 */ protected $regionFactory; @@ -47,7 +47,7 @@ class CustomerManagement implements \Magento\Sales\Api\OrderCustomerManagementIn protected $orderRepository; /** - * @deprecated + * @deprecated 101.0.4 */ protected $objectCopyService; diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php index 05164d1b7b5f3..d0247294e75a1 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php @@ -3,18 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Sender; +use Magento\Framework\DataObject; +use Magento\Framework\Event\ManagerInterface; use Magento\Payment\Helper\Data as PaymentHelper; use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address\Renderer; use Magento\Sales\Model\Order\Email\Container\InvoiceIdentity; use Magento\Sales\Model\Order\Email\Container\Template; use Magento\Sales\Model\Order\Email\Sender; use Magento\Sales\Model\Order\Invoice; use Magento\Sales\Model\ResourceModel\Order\Invoice as InvoiceResource; -use Magento\Sales\Model\Order\Address\Renderer; -use Magento\Framework\Event\ManagerInterface; -use Magento\Framework\DataObject; /** * Sends order invoice email to the customer. @@ -106,6 +108,12 @@ public function send(Invoice $invoice, $forceSyncMode = false) $order = $invoice->getOrder(); $this->identityContainer->setStore($order->getStore()); + if ($this->checkIfPartialInvoice($order, $invoice)) { + $order->setBaseSubtotal((float) $invoice->getBaseSubtotal()); + $order->setBaseTaxAmount((float) $invoice->getBaseTaxAmount()); + $order->setBaseShippingAmount((float) $invoice->getBaseShippingAmount()); + } + $transport = [ 'order' => $order, 'order_id' => $order->getId(), @@ -165,4 +173,18 @@ protected function getPaymentHtml(Order $order) $this->identityContainer->getStore()->getStoreId() ); } + + /** + * Check if the order contains partial invoice + * + * @param Order $order + * @param Invoice $invoice + * @return bool + */ + private function checkIfPartialInvoice(Order $order, Invoice $invoice): bool + { + $totalQtyOrdered = (float) $order->getTotalQtyOrdered(); + $totalQtyInvoiced = (float) $invoice->getTotalQty(); + return $totalQtyOrdered !== $totalQtyInvoiced; + } } diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php index 4c8e1744ac0e0..49aef3de7fecb 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php @@ -19,7 +19,7 @@ /** * Sends order shipment email to the customer. * - * @deprecated since this class works only with the concrete model and no data interface + * @deprecated 102.1.0 since this class works only with the concrete model and no data interface * @see \Magento\Sales\Model\Order\Shipment\Sender\EmailSender * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php b/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php index acd0d0c67d8c0..ef7205b374415 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php @@ -5,13 +5,20 @@ */ namespace Magento\Sales\Model\Order\Invoice\Total; +use Magento\Sales\Model\Order\Invoice; + +/** + * Discount invoice + */ class Discount extends AbstractTotal { /** - * @param \Magento\Sales\Model\Order\Invoice $invoice + * Collect invoice + * + * @param Invoice $invoice * @return $this */ - public function collect(\Magento\Sales\Model\Order\Invoice $invoice) + public function collect(Invoice $invoice) { $invoice->setDiscountAmount(0); $invoice->setBaseDiscountAmount(0); @@ -24,14 +31,7 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) * So basically if we have invoice with positive discount and it * was not canceled we don't add shipping discount to this one. */ - $addShippingDiscount = true; - foreach ($invoice->getOrder()->getInvoiceCollection() as $previousInvoice) { - if ($previousInvoice->getDiscountAmount()) { - $addShippingDiscount = false; - } - } - - if ($addShippingDiscount) { + if ($this->isShippingDiscount($invoice)) { $totalDiscountAmount = $totalDiscountAmount + $invoice->getOrder()->getShippingDiscountAmount(); $baseTotalDiscountAmount = $baseTotalDiscountAmount + $invoice->getOrder()->getBaseShippingDiscountAmount(); @@ -71,8 +71,29 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) $invoice->setDiscountAmount(-$totalDiscountAmount); $invoice->setBaseDiscountAmount(-$baseTotalDiscountAmount); - $invoice->setGrandTotal($invoice->getGrandTotal() - $totalDiscountAmount); - $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal() - $baseTotalDiscountAmount); + $grandTotal = $invoice->getGrandTotal() - $totalDiscountAmount < 0.0001 + ? 0 : $invoice->getGrandTotal() - $totalDiscountAmount; + $baseGrandTotal = $invoice->getBaseGrandTotal() - $baseTotalDiscountAmount < 0.0001 + ? 0 : $invoice->getBaseGrandTotal() - $baseTotalDiscountAmount; + $invoice->setGrandTotal($grandTotal); + $invoice->setBaseGrandTotal($baseGrandTotal); return $this; } + + /** + * Checking if shipping discount was added in previous invoices. + * + * @param Invoice $invoice + * @return bool + */ + private function isShippingDiscount(Invoice $invoice): bool + { + $addShippingDiscount = true; + foreach ($invoice->getOrder()->getInvoiceCollection() as $previousInvoice) { + if ($previousInvoice->getDiscountAmount()) { + $addShippingDiscount = false; + } + } + return $addShippingDiscount; + } } diff --git a/app/code/Magento/Sales/Model/Order/InvoiceRepository.php b/app/code/Magento/Sales/Model/Order/InvoiceRepository.php index 2244a86260c2f..ac1a782367ab3 100644 --- a/app/code/Magento/Sales/Model/Order/InvoiceRepository.php +++ b/app/code/Magento/Sales/Model/Order/InvoiceRepository.php @@ -145,7 +145,7 @@ public function save(\Magento\Sales\Api\Data\InvoiceInterface $entity) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/Order/Item.php b/app/code/Magento/Sales/Model/Order/Item.php index ba01090e5abff..bc55b2229770d 100644 --- a/app/code/Magento/Sales/Model/Order/Item.php +++ b/app/code/Magento/Sales/Model/Order/Item.php @@ -2409,7 +2409,7 @@ public function setExtensionAttributes(\Magento\Sales\Api\Data\OrderItemExtensio * Check if it is possible to process item after cancellation * * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function isProcessingAvailable() { diff --git a/app/code/Magento/Sales/Model/Order/ItemRepository.php b/app/code/Magento/Sales/Model/Order/ItemRepository.php index 6e029ac468370..345fffc414fbc 100644 --- a/app/code/Magento/Sales/Model/Order/ItemRepository.php +++ b/app/code/Magento/Sales/Model/Order/ItemRepository.php @@ -167,10 +167,7 @@ public function deleteById($id) public function save(OrderItemInterface $entity) { if ($entity->getProductOption()) { - $request = $this->getBuyRequest($entity); - $productOptions = $entity->getProductOptions(); - $productOptions['info_buyRequest'] = $request->toArray(); - $entity->setProductOptions($productOptions); + $entity->setProductOptions($this->getItemProductOptions($entity)); } $this->metadata->getMapper()->save($entity); @@ -178,6 +175,23 @@ public function save(OrderItemInterface $entity) return $this->registry[$entity->getEntityId()]; } + /** + * Return product options + * + * @param OrderItemInterface $entity + * @return array + */ + private function getItemProductOptions(OrderItemInterface $entity): array + { + $request = $this->getBuyRequest($entity); + $productOptions = $entity->getProductOptions(); + $productOptions['info_buyRequest'] = $productOptions && !empty($productOptions['info_buyRequest']) + ? array_merge($productOptions['info_buyRequest'], $request->toArray()) + : $request->toArray(); + + return $productOptions; + } + /** * Set parent item. * diff --git a/app/code/Magento/Sales/Model/Order/Payment.php b/app/code/Magento/Sales/Model/Order/Payment.php index 3076c6dfd2ba7..6a2a77b52927a 100644 --- a/app/code/Magento/Sales/Model/Order/Payment.php +++ b/app/code/Magento/Sales/Model/Order/Payment.php @@ -1494,7 +1494,7 @@ protected function _getInvoiceForTransactionId($transactionId) /** * Get order state resolver instance. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return OrderStateResolverInterface */ private function getOrderStateResolver() diff --git a/app/code/Magento/Sales/Model/Order/Payment/Info.php b/app/code/Magento/Sales/Model/Order/Payment/Info.php index 479d96b5842d9..c7641cb861b3e 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Info.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Info.php @@ -118,6 +118,7 @@ public function getMethodInstance() $instance = $this->paymentData->getMethodInstance(Substitution::CODE); } $instance->setInfoInstance($this); + $instance->setStore($this->getOrder()->getStoreId()); $this->setMethodInstance($instance); } return $this->getData('method_instance'); diff --git a/app/code/Magento/Sales/Model/Order/Payment/Repository.php b/app/code/Magento/Sales/Model/Order/Payment/Repository.php index 4353f6b1cc391..27686ffb46c9d 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Repository.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Repository.php @@ -131,7 +131,7 @@ public function create() /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/Order/Payment/State/AuthorizeCommand.php b/app/code/Magento/Sales/Model/Order/Payment/State/AuthorizeCommand.php index 89731b5130605..d17f3b51f3934 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/State/AuthorizeCommand.php +++ b/app/code/Magento/Sales/Model/Order/Payment/State/AuthorizeCommand.php @@ -90,7 +90,7 @@ private function getNotificationMessage(OrderPaymentInterface $payment): ?string * @param string $status * @param string $state * @return void - * @deprecated 100.2.0 Replaced by a StatusResolver class call. + * @deprecated 100.1.9 Replaced by a StatusResolver class call. */ protected function setOrderStateAndStatus(Order $order, $status, $state) { diff --git a/app/code/Magento/Sales/Model/Order/Payment/State/CaptureCommand.php b/app/code/Magento/Sales/Model/Order/Payment/State/CaptureCommand.php index f57e1933a7e5a..79b329cd486e5 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/State/CaptureCommand.php +++ b/app/code/Magento/Sales/Model/Order/Payment/State/CaptureCommand.php @@ -74,7 +74,7 @@ public function execute(OrderPaymentInterface $payment, $amount, OrderInterface * @param string $status * @param string $state * @return void - * @deprecated 100.2.0 Replaced by a StatusResolver class call. + * @deprecated 100.1.9 Replaced by a StatusResolver class call. */ protected function setOrderStateAndStatus(Order $order, $status, $state) { diff --git a/app/code/Magento/Sales/Model/Order/Payment/State/OrderCommand.php b/app/code/Magento/Sales/Model/Order/Payment/State/OrderCommand.php index 2a7e7145f6886..d6acd82613c0a 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/State/OrderCommand.php +++ b/app/code/Magento/Sales/Model/Order/Payment/State/OrderCommand.php @@ -61,7 +61,7 @@ public function execute(OrderPaymentInterface $payment, $amount, OrderInterface } /** - * @deprecated 100.2.0 Replaced by a StatusResolver class call. + * @deprecated 100.1.9 Replaced by a StatusResolver class call. * * @param Order $order * @param string $status diff --git a/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php b/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php index 2551092a64e9a..ff375c995a183 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php +++ b/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php @@ -73,7 +73,7 @@ public function execute(OrderPaymentInterface $payment, $amount, OrderInterface /** * Sets the state and status of the order * - * @deprecated 100.2.0 Replaced by a StatusResolver class call. + * @deprecated 100.1.9 Replaced by a StatusResolver class call. * * @param Order $order * @param string $status diff --git a/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php b/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php index a602fe54363ed..30af4e07a42bf 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php @@ -233,7 +233,7 @@ public function create() /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php b/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php index f1430757939e7..cc67601f0ec51 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php @@ -20,6 +20,11 @@ class Creditmemo extends AbstractPdf */ protected $_storeManager; + /** + * @var \Magento\Store\Model\App\Emulation + */ + private $appEmulation; + /** * @param \Magento\Payment\Helper\Data $paymentData * @param \Magento\Framework\Stdlib\StringUtils $string @@ -32,7 +37,7 @@ class Creditmemo extends AbstractPdf * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation * @param \Magento\Sales\Model\Order\Address\Renderer $addressRenderer * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver + * @param \Magento\Store\Model\App\Emulation|null $appEmulation * @param array $data * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -50,11 +55,11 @@ public function __construct( \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, \Magento\Sales\Model\Order\Address\Renderer $addressRenderer, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Locale\ResolverInterface $localeResolver, + \Magento\Store\Model\App\Emulation $appEmulation, array $data = [] ) { $this->_storeManager = $storeManager; - $this->_localeResolver = $localeResolver; + $this->appEmulation = $appEmulation; parent::__construct( $paymentData, $string, @@ -150,7 +155,11 @@ public function getPdf($creditmemos = []) foreach ($creditmemos as $creditmemo) { if ($creditmemo->getStoreId()) { - $this->_localeResolver->emulate($creditmemo->getStoreId()); + $this->appEmulation->startEnvironmentEmulation( + $creditmemo->getStoreId(), + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ); $this->_storeManager->setCurrentStore($creditmemo->getStoreId()); } $page = $this->newPage(); @@ -185,7 +194,7 @@ public function getPdf($creditmemos = []) /* Add totals */ $this->insertTotals($page, $creditmemo); if ($creditmemo->getStoreId()) { - $this->_localeResolver->revert(); + $this->appEmulation->stopEnvironmentEmulation(); } } $this->_afterGetPdf(); diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php index f294128a72f9f..d4ce16d1bbe8e 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php @@ -19,9 +19,9 @@ class Invoice extends AbstractPdf protected $_storeManager; /** - * @var \Magento\Framework\Locale\ResolverInterface + * @var \Magento\Store\Model\App\Emulation */ - protected $_localeResolver; + private $appEmulation; /** * @param \Magento\Payment\Helper\Data $paymentData @@ -35,7 +35,7 @@ class Invoice extends AbstractPdf * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation * @param \Magento\Sales\Model\Order\Address\Renderer $addressRenderer * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver + * @param \Magento\Store\Model\App\Emulation $appEmulation * @param array $data * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -52,11 +52,11 @@ public function __construct( \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, \Magento\Sales\Model\Order\Address\Renderer $addressRenderer, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Locale\ResolverInterface $localeResolver, + \Magento\Store\Model\App\Emulation $appEmulation, array $data = [] ) { $this->_storeManager = $storeManager; - $this->_localeResolver = $localeResolver; + $this->appEmulation = $appEmulation; parent::__construct( $paymentData, $string, @@ -127,7 +127,11 @@ public function getPdf($invoices = []) foreach ($invoices as $invoice) { if ($invoice->getStoreId()) { - $this->_localeResolver->emulate($invoice->getStoreId()); + $this->appEmulation->startEnvironmentEmulation( + $invoice->getStoreId(), + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ); $this->_storeManager->setCurrentStore($invoice->getStoreId()); } $page = $this->newPage(); @@ -162,7 +166,7 @@ public function getPdf($invoices = []) /* Add totals */ $this->insertTotals($page, $invoice); if ($invoice->getStoreId()) { - $this->_localeResolver->revert(); + $this->appEmulation->stopEnvironmentEmulation(); } } $this->_afterGetPdf(); diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php index 253dbd43fa580..6ddbce49829eb 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php @@ -80,11 +80,9 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [ - [ + $lines[0][] = [ 'text' => $this->string->split($this->prepareText((string)$item->getName()), 35, true, true), 'feed' => 35 - ] ]; // draw SKU diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php b/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php index 32a289c0f5fa8..92124b7fe8b72 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php @@ -17,9 +17,9 @@ class Shipment extends AbstractPdf protected $_storeManager; /** - * @var \Magento\Framework\Locale\ResolverInterface + * @var \Magento\Store\Model\App\Emulation */ - protected $_localeResolver; + private $appEmulation; /** * @param \Magento\Payment\Helper\Data $paymentData @@ -33,7 +33,7 @@ class Shipment extends AbstractPdf * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation * @param \Magento\Sales\Model\Order\Address\Renderer $addressRenderer * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver + * @param \Magento\Store\Model\App\Emulation $appEmulation * @param array $data * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -50,11 +50,11 @@ public function __construct( \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, \Magento\Sales\Model\Order\Address\Renderer $addressRenderer, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Locale\ResolverInterface $localeResolver, + \Magento\Store\Model\App\Emulation $appEmulation, array $data = [] ) { $this->_storeManager = $storeManager; - $this->_localeResolver = $localeResolver; + $this->appEmulation = $appEmulation; parent::__construct( $paymentData, $string, @@ -118,7 +118,11 @@ public function getPdf($shipments = []) $this->_setFontBold($style, 10); foreach ($shipments as $shipment) { if ($shipment->getStoreId()) { - $this->_localeResolver->emulate($shipment->getStoreId()); + $this->appEmulation->startEnvironmentEmulation( + $shipment->getStoreId(), + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ); $this->_storeManager->setCurrentStore($shipment->getStoreId()); } $page = $this->newPage(); @@ -151,7 +155,7 @@ public function getPdf($shipments = []) $page = end($pdf->pages); } if ($shipment->getStoreId()) { - $this->_localeResolver->revert(); + $this->appEmulation->stopEnvironmentEmulation(); } } $this->_afterGetPdf(); diff --git a/app/code/Magento/Sales/Model/Order/ProductOption.php b/app/code/Magento/Sales/Model/Order/ProductOption.php index 9a4f847b135e7..3d0b5433d7a4f 100644 --- a/app/code/Magento/Sales/Model/Order/ProductOption.php +++ b/app/code/Magento/Sales/Model/Order/ProductOption.php @@ -17,6 +17,7 @@ * Adds product option to the order item according to product options processors pool. * * @api + * @since 102.0.1 */ class ProductOption { @@ -54,6 +55,7 @@ public function __construct( * Adds product option to the order item. * * @param OrderItemInterface $orderItem + * @since 102.0.1 */ public function add(OrderItemInterface $orderItem): void { diff --git a/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php b/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php index dd70c6b5481df..e4f2ff0d57035 100644 --- a/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php +++ b/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php @@ -16,7 +16,7 @@ * of the array $productAvailabilityChecks(constructor argument). A product type should be a key for the new element. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class OrderedProductAvailabilityChecker implements OrderedProductAvailabilityCheckerInterface { @@ -36,7 +36,7 @@ public function __construct(array $productAvailabilityChecks) /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function isAvailable(Item $item) { diff --git a/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityCheckerInterface.php b/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityCheckerInterface.php index 989bd482ed4e8..59f7dfc63b095 100644 --- a/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityCheckerInterface.php +++ b/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityCheckerInterface.php @@ -9,7 +9,7 @@ /** * @api - * @since 100.2.0 + * @since 101.0.0 */ interface OrderedProductAvailabilityCheckerInterface { @@ -19,7 +19,7 @@ interface OrderedProductAvailabilityCheckerInterface * * @param Item $item * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function isAvailable(Item $item); } diff --git a/app/code/Magento/Sales/Model/Order/ShipmentFactory.php b/app/code/Magento/Sales/Model/Order/ShipmentFactory.php index 21b42abeb293d..3cd318ea67adb 100644 --- a/app/code/Magento/Sales/Model/Order/ShipmentFactory.php +++ b/app/code/Magento/Sales/Model/Order/ShipmentFactory.php @@ -12,6 +12,7 @@ * Factory class for @see \Magento\Sales\Api\Data\ShipmentInterface * * @api + * @since 100.0.2 */ class ShipmentFactory { diff --git a/app/code/Magento/Sales/Model/Order/ShipmentRepository.php b/app/code/Magento/Sales/Model/Order/ShipmentRepository.php index 0b86bec895b75..ad73b22e94555 100644 --- a/app/code/Magento/Sales/Model/Order/ShipmentRepository.php +++ b/app/code/Magento/Sales/Model/Order/ShipmentRepository.php @@ -166,7 +166,7 @@ public function create() /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/OrderRepository.php b/app/code/Magento/Sales/Model/OrderRepository.php index f93de4c32d888..a600d1489857c 100644 --- a/app/code/Magento/Sales/Model/OrderRepository.php +++ b/app/code/Magento/Sales/Model/OrderRepository.php @@ -157,6 +157,9 @@ public function get($id) private function setOrderTaxDetails(OrderInterface $order) { $extensionAttributes = $order->getExtensionAttributes(); + if ($extensionAttributes === null) { + $extensionAttributes = $this->orderExtensionFactory->create(); + } $orderTaxDetails = $this->orderTaxManagement->getOrderTaxDetails($order->getEntityId()); $appliedTaxes = $orderTaxDetails->getAppliedTaxes(); @@ -180,6 +183,9 @@ private function setOrderTaxDetails(OrderInterface $order) private function setPaymentAdditionalInfo(OrderInterface $order): void { $extensionAttributes = $order->getExtensionAttributes(); + if ($extensionAttributes === null) { + $extensionAttributes = $this->orderExtensionFactory->create(); + } $paymentAdditionalInformation = $order->getPayment()->getAdditionalInformation(); $objects = []; @@ -317,7 +323,7 @@ private function getShippingAssignmentBuilderDependency() * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup * @param \Magento\Sales\Api\Data\OrderSearchResultInterface $searchResult * @return void - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @throws \Magento\Framework\Exception\InputException */ protected function addFilterGroupToCollection( diff --git a/app/code/Magento/Sales/Model/Reorder/Reorder.php b/app/code/Magento/Sales/Model/Reorder/Reorder.php index a1a8d6e8c9928..c7636696382b4 100644 --- a/app/code/Magento/Sales/Model/Reorder/Reorder.php +++ b/app/code/Magento/Sales/Model/Reorder/Reorder.php @@ -7,7 +7,6 @@ namespace Magento\Sales\Model\Reorder; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Framework\Exception\InputException; @@ -15,7 +14,8 @@ use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Model\Cart\CustomerCartResolver; -use Magento\Quote\Model\Quote as Quote; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\GuestCart\GuestCartResolver; use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Helper\Reorder as ReorderHelper; use Magento\Sales\Model\Order\Item; @@ -72,11 +72,6 @@ class Reorder */ private $cartRepository; - /** - * @var ProductRepositoryInterface - */ - private $productRepository; - /** * @var Data\Error[] */ @@ -92,11 +87,16 @@ class Reorder */ private $productCollectionFactory; + /** + * @var GuestCartResolver + */ + private $guestCartResolver; + /** * @param OrderFactory $orderFactory * @param CustomerCartResolver $customerCartProvider + * @param GuestCartResolver $guestCartResolver * @param CartRepositoryInterface $cartRepository - * @param ProductRepositoryInterface $productRepository * @param ReorderHelper $reorderHelper * @param \Psr\Log\LoggerInterface $logger * @param ProductCollectionFactory $productCollectionFactory @@ -104,18 +104,18 @@ class Reorder public function __construct( OrderFactory $orderFactory, CustomerCartResolver $customerCartProvider, + GuestCartResolver $guestCartResolver, CartRepositoryInterface $cartRepository, - ProductRepositoryInterface $productRepository, ReorderHelper $reorderHelper, \Psr\Log\LoggerInterface $logger, ProductCollectionFactory $productCollectionFactory ) { $this->orderFactory = $orderFactory; $this->cartRepository = $cartRepository; - $this->productRepository = $productRepository; $this->reorderHelper = $reorderHelper; $this->logger = $logger; $this->customerCartProvider = $customerCartProvider; + $this->guestCartResolver = $guestCartResolver; $this->productCollectionFactory = $productCollectionFactory; } @@ -141,7 +141,9 @@ public function execute(string $orderNumber, string $storeId): Data\ReorderOutpu $customerId = (int)$order->getCustomerId(); $this->errors = []; - $cart = $this->customerCartProvider->resolve($customerId); + $cart = $customerId === 0 + ? $this->guestCartResolver->resolve() + : $this->customerCartProvider->resolve($customerId); if (!$this->reorderHelper->isAllowed($order->getStore())) { $this->addError((string)__('Reorders are not allowed.'), self::ERROR_REORDER_NOT_AVAILABLE); return $this->prepareOutput($cart); @@ -225,7 +227,8 @@ private function getOrderProducts(string $storeId, array $orderItemProductIds): ->addStoreFilter() ->addAttributeToSelect('*') ->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner') - ->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner'); + ->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner') + ->addOptionsToResult(); return $collection->getItems(); } diff --git a/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php b/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php index 25c15449a9fb4..444fc589748ab 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php +++ b/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php @@ -90,7 +90,7 @@ public function purge($value, $field = null) * * @param string $default * @return string - * @deprecated 100.2.0 this method is not used in abstract model but only in single child so + * @deprecated 101.0.0 this method is not used in abstract model but only in single child so * this deprecation is a part of cleaning abstract classes. * @see \Magento\Sales\Model\ResourceModel\Provider\UpdatedIdListProvider */ 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/Model/ResourceModel/Order/Address/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Address/Collection.php index 8af6c03b44275..f2a28b613cfea 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Address/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Address/Collection.php @@ -6,12 +6,19 @@ namespace Magento\Sales\Model\ResourceModel\Order\Address; use Magento\Sales\Api\Data\OrderAddressSearchResultInterface; -use \Magento\Sales\Model\ResourceModel\Order\Collection\AbstractCollection; +use Magento\Sales\Model\ResourceModel\Order\Collection\AbstractCollection; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Data\Collection\EntityFactoryInterface; +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\App\ObjectManager; +use Psr\Log\LoggerInterface; /** - * Flat sales order payment collection - * - * @author Magento Core Team <core@magentocommerce.com> + * Order addresses collection */ class Collection extends AbstractCollection implements OrderAddressSearchResultInterface { @@ -29,6 +36,44 @@ class Collection extends AbstractCollection implements OrderAddressSearchResultI */ protected $_eventObject = 'order_address_collection'; + /** + * @var ResolverInterface + */ + private $localeResolver; + + /** + * @param EntityFactoryInterface $entityFactory + * @param LoggerInterface $logger + * @param FetchStrategyInterface $fetchStrategy + * @param ManagerInterface $eventManager + * @param Snapshot $entitySnapshot + * @param AdapterInterface|null $connection + * @param AbstractDb|null $resource + * @param ResolverInterface|null $localeResolver + */ + public function __construct( + EntityFactoryInterface $entityFactory, + LoggerInterface $logger, + FetchStrategyInterface $fetchStrategy, + ManagerInterface $eventManager, + Snapshot $entitySnapshot, + AdapterInterface $connection = null, + AbstractDb $resource = null, + ResolverInterface $localeResolver = null + ) { + $this->localeResolver = $localeResolver ?: ObjectManager::getInstance() + ->get(ResolverInterface::class); + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $entitySnapshot, + $connection, + $resource + ); + } + /** * Model initialization * @@ -42,6 +87,16 @@ protected function _construct() ); } + /** + * @inheritdoc + */ + protected function _initSelect() + { + parent::_initSelect(); + $this->joinRegions(); + return $this; + } + /** * Redeclare after load method for dispatch event * @@ -55,4 +110,31 @@ protected function _afterLoad() return $this; } + + /** + * Join region name table with current locale + * + * @return $this + */ + private function joinRegions() + { + $locale = $this->localeResolver->getLocale(); + $connection = $this->getConnection(); + + $defaultNameExpr = $connection->getIfNullSql( + $connection->quoteIdentifier('rct.default_name'), + $connection->quoteIdentifier('main_table.region') + ); + $expression = $connection->getIfNullSql($connection->quoteIdentifier('rnt.name'), $defaultNameExpr); + + $regionId = $connection->quoteIdentifier('main_table.region_id'); + $condition = $connection->quoteInto("rnt.locale=?", $locale); + $rctTable = $this->getTable('directory_country_region'); + $rntTable = $this->getTable('directory_country_region_name'); + + $this->getSelect() + ->joinLeft(['rct' => $rctTable], "rct.region_id={$regionId}", []) + ->joinLeft(['rnt' => $rntTable], "rnt.region_id={$regionId} AND {$condition}", ['region' => $expression]); + return $this; + } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/Address.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/Address.php index 0fec004a25fae..274132a7fea50 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/Address.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/Address.php @@ -9,9 +9,6 @@ use Magento\Sales\Model\Order; use Magento\Sales\Model\ResourceModel\Attribute; -/** - * Class Address - */ class Address { /** @@ -69,7 +66,7 @@ public function process(Order $order) $attributesForSave[] = 'billing_address_id'; } $shippingAddress = $order->getShippingAddress(); - if ($shippingAddress && $order->getShippigAddressId() != $shippingAddress->getId()) { + if ($shippingAddress && $order->getShippingAddressId() != $shippingAddress->getId()) { $order->setShippingAddressId($shippingAddress->getId()); $attributesForSave[] = 'shipping_address_id'; } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php index de15a627583ff..47395b17afee8 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php @@ -9,7 +9,7 @@ use Magento\Sales\Model\Order; /** - * Class State + * Checking order status and adjusting order status before saving */ class State { @@ -34,6 +34,7 @@ public function check(Order $order) if (in_array($currentState, [Order::STATE_PROCESSING, Order::STATE_COMPLETE]) && !$order->canCreditmemo() && !$order->canShip() + && $order->getIsNotVirtual() ) { $order->setState(Order::STATE_CLOSED) ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED)); diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php index 19d9b6f300eba..b1d2deb248ba1 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php @@ -43,13 +43,13 @@ public function getAllCommentCollection($orderId) $commentSelects = []; foreach (['invoice', 'shipment', 'creditmemo'] as $entityTypeCode) { $mainTable = $resource->getTableName('sales_' . $entityTypeCode); - $slaveTable = $resource->getTableName('sales_' . $entityTypeCode . '_comment'); + $commentTable = $resource->getTableName('sales_' . $entityTypeCode . '_comment'); $select = $read->select()->from( ['main' => $mainTable], ['entity_id' => 'order_id', 'entity_type_code' => new \Zend_Db_Expr("'{$entityTypeCode}'")] )->join( - ['slave' => $slaveTable], - 'main.entity_id = slave.parent_id', + ['comment' => $commentTable], + 'main.entity_id = comment.parent_id', $fields )->where( 'main.order_id = ?', diff --git a/app/code/Magento/Sales/Model/ShipOrder.php b/app/code/Magento/Sales/Model/ShipOrder.php index 2cc4c9b241172..26fe5a8e4b457 100644 --- a/app/code/Magento/Sales/Model/ShipOrder.php +++ b/app/code/Magento/Sales/Model/ShipOrder.php @@ -5,20 +5,32 @@ */ namespace Magento\Sales\Model; +use DomainException; use Magento\Framework\App\ResourceConnection; +use Magento\Sales\Api\Data\ShipmentCommentCreationInterface; +use Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface; +use Magento\Sales\Api\Data\ShipmentItemCreationInterface; +use Magento\Sales\Api\Data\ShipmentPackageCreationInterface; +use Magento\Sales\Api\Data\ShipmentTrackCreationInterface; +use Magento\Sales\Api\Exception\CouldNotShipExceptionInterface; +use Magento\Sales\Api\Exception\DocumentValidationExceptionInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Api\ShipmentRepositoryInterface; use Magento\Sales\Api\ShipOrderInterface; +use Magento\Sales\Exception\CouldNotShipException; +use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\Order\Config as OrderConfig; use Magento\Sales\Model\Order\OrderStateResolverInterface; -use Magento\Sales\Model\Order\ShipmentDocumentFactory; use Magento\Sales\Model\Order\Shipment\NotifierInterface; use Magento\Sales\Model\Order\Shipment\OrderRegistrarInterface; +use Magento\Sales\Model\Order\ShipmentDocumentFactory; use Magento\Sales\Model\Order\Validation\ShipOrderInterface as ShipOrderValidator; use Psr\Log\LoggerInterface; /** * Class ShipOrder + * + * Save shipment and order data * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ShipOrder implements ShipOrderInterface @@ -111,30 +123,30 @@ public function __construct( } /** + * Process the shipment and save shipment and order data + * * @param int $orderId - * @param \Magento\Sales\Api\Data\ShipmentItemCreationInterface[] $items + * @param ShipmentItemCreationInterface[] $items * @param bool $notify * @param bool $appendComment - * @param \Magento\Sales\Api\Data\ShipmentCommentCreationInterface|null $comment - * @param \Magento\Sales\Api\Data\ShipmentTrackCreationInterface[] $tracks - * @param \Magento\Sales\Api\Data\ShipmentPackageCreationInterface[] $packages - * @param \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface|null $arguments + * @param ShipmentCommentCreationInterface|null $comment + * @param ShipmentTrackCreationInterface[] $tracks + * @param ShipmentPackageCreationInterface[] $packages + * @param ShipmentCreationArgumentsInterface|null $arguments * @return int - * @throws \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface - * @throws \Magento\Sales\Api\Exception\CouldNotShipExceptionInterface - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\NoSuchEntityException - * @throws \DomainException + * @throws DocumentValidationExceptionInterface + * @throws CouldNotShipExceptionInterface + * @throws DomainException */ public function execute( $orderId, array $items = [], $notify = false, $appendComment = false, - \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, + ShipmentCommentCreationInterface $comment = null, array $tracks = [], array $packages = [], - \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface $arguments = null + ShipmentCreationArgumentsInterface $arguments = null ) { $connection = $this->resourceConnection->getConnection('sales'); $order = $this->orderRepository->get($orderId); @@ -158,7 +170,7 @@ public function execute( $packages ); if ($validationMessages->hasMessages()) { - throw new \Magento\Sales\Exception\DocumentValidationException( + throw new DocumentValidationException( __("Shipment Document Validation Error(s):\n" . implode("\n", $validationMessages->getMessages())) ); } @@ -169,16 +181,19 @@ public function execute( $this->orderStateResolver->getStateForOrder($order, [OrderStateResolverInterface::IN_PROGRESS]) ); $order->setStatus($this->config->getStateDefaultStatus($order->getState())); - $this->shipmentRepository->save($shipment); + $shippingData = $this->shipmentRepository->save($shipment); $this->orderRepository->save($order); $connection->commit(); } catch (\Exception $e) { $this->logger->critical($e); $connection->rollBack(); - throw new \Magento\Sales\Exception\CouldNotShipException( + throw new CouldNotShipException( __('Could not save a shipment, see error log for details') ); } + if ($shipment && empty($shipment->getEntityId())) { + $shipment->setEntityId($shippingData->getEntityId()); + } if ($notify) { if (!$appendComment) { $comment = null; diff --git a/app/code/Magento/Sales/Plugin/ProcessOrderAndShipmentViaAPI.php b/app/code/Magento/Sales/Plugin/ProcessOrderAndShipmentViaAPI.php new file mode 100644 index 0000000000000..2f81de65fad74 --- /dev/null +++ b/app/code/Magento/Sales/Plugin/ProcessOrderAndShipmentViaAPI.php @@ -0,0 +1,241 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Plugin; + +use Exception; +use Magento\Framework\DB\Transaction; +use Magento\Framework\Exception\LocalizedException; +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Shipment\Item; +use Magento\Sales\Model\Order\ShipmentRepository; +use Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader; + +/** + * Plugin to update order data before and after saving shipment via API + */ +class ProcessOrderAndShipmentViaAPI +{ + /** + * @var ShipmentLoader + */ + private $shipmentLoader; + + /** + * @var Transaction + */ + private $transaction; + + /** + * Init plugin + * + * @param ShipmentLoader $shipmentLoader + * @param Transaction $transaction + */ + public function __construct( + ShipmentLoader $shipmentLoader, + Transaction $transaction + ) { + $this->shipmentLoader = $shipmentLoader; + $this->transaction = $transaction; + } + + /** + * Process shipping details before saving shipment via API + * + * @param ShipmentRepository $shipmentRepository + * @param ShipmentInterface $shipmentData + * @return array + * @throws LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function beforeSave( + ShipmentRepository $shipmentRepository, + ShipmentInterface $shipmentData + ): array { + $this->shipmentLoader->setOrderId($shipmentData->getOrderId()); + $trackData = !empty($shipmentData->getTracks()) ? + $this->getShipmentTracking($shipmentData) : []; + $this->shipmentLoader->setTracking($trackData); + $shipmentItems = !empty($shipmentData) ? + $this->getShipmentItems($shipmentData) : []; + $orderItems = []; + if (!empty($shipmentData)) { + $order = $shipmentData->getOrder(); + $orderItems = $order ? $this->getOrderItems($order) : []; + } + $data = (!empty($shipmentItems) && !empty($orderItems)) ? + $this->getShippingData($shipmentItems, $orderItems) : []; + $this->shipmentLoader->setShipment($data); + $shipment = $this->shipmentLoader->load(); + $shipment = empty($shipment) ? $shipmentData + : $this->processShippingDetails($shipmentData, $shipment); + return [$shipment]; + } + + /** + * Save order data after saving shipment via API + * + * @param ShipmentRepository $shipmentRepository + * @param ShipmentInterface $shipment + * @return ShipmentInterface + * @throws Exception + */ + public function afterSave( + ShipmentRepository $shipmentRepository, + ShipmentInterface $shipment + ): ShipmentInterface { + $shipmentDetails = $shipmentRepository->get($shipment->getEntityId()); + $order = $shipmentDetails->getOrder(); + $shipmentItems = !empty($shipment) ? + $this->getShipmentItems($shipment) : []; + $this->processOrderItems($order, $shipmentItems); + $order->setIsInProcess(true); + $this->transaction + ->addObject($order) + ->save(); + return $shipment; + } + + /** + * Process shipment items + * + * @param ShipmentInterface $shipment + * @return array + * @throws LocalizedException + */ + private function getShipmentItems(ShipmentInterface $shipment): array + { + $shipmentItems = []; + foreach ($shipment->getItems() as $item) { + $sku = $item->getSku(); + if (isset($sku)) { + $shipmentItems[$sku]['qty'] = $item->getQty(); + } + } + return $shipmentItems; + } + + /** + * Get shipment tracking data from the shipment array + * + * @param ShipmentInterface $shipment + * @return array + */ + private function getShipmentTracking(ShipmentInterface $shipment): array + { + $trackData = []; + foreach ($shipment->getTracks() as $key => $track) { + $trackData[$key]['number'] = $track->getTrackNumber(); + $trackData[$key]['title'] = $track->getTitle(); + $trackData[$key]['carrier_code'] = $track->getCarrierCode(); + } + return $trackData; + } + + /** + * Get orderItems from shipment order + * + * @param Order $order + * @return array + */ + private function getOrderItems(Order $order): array + { + $orderItems = []; + foreach ($order->getItems() as $item) { + $orderItems[$item->getSku()] = $item->getItemId(); + } + return $orderItems; + } + + /** + * Get available shipping data from shippingItems and orderItems + * + * @param array $shipmentItems + * @param array $orderItems + * @return array + * @throws LocalizedException + */ + private function getShippingData(array $shipmentItems, array $orderItems): array + { + $data = []; + foreach ($shipmentItems as $shippingItemSku => $shipmentItem) { + if (isset($orderItems[$shippingItemSku])) { + $itemId = (int) $orderItems[$shippingItemSku]; + $data['items'][$itemId] = $shipmentItem['qty']; + } + } + return $data; + } + + /** + * Process shipping comments if available + * + * @param ShipmentInterface $shipmentData + * @param ShipmentInterface $shipment + * @return void + */ + private function processShippingComments(ShipmentInterface $shipmentData, ShipmentInterface $shipment): void + { + foreach ($shipmentData->getComments() as $comment) { + $shipment->addComment( + $comment->getComment(), + $comment->getIsCustomerNotified(), + $comment->getIsVisibleOnFront() + ); + $shipment->setCustomerNote($comment->getComment()); + $shipment->setCustomerNoteNotify((bool) $comment->getIsCustomerNotified()); + } + } + + /** + * Process shipping details + * + * @param ShipmentInterface $shipmentData + * @param ShipmentInterface $shipment + * @return ShipmentInterface + */ + private function processShippingDetails( + ShipmentInterface $shipmentData, + ShipmentInterface $shipment + ): ShipmentInterface { + if (empty($shipment->getItems())) { + $shipment->setItems($shipmentData->getItems()); + } + if (!empty($shipmentData->getComments())) { + $this->processShippingComments($shipmentData, $shipment); + } + if ((int) $shipment->getTotalQty() < 1) { + $shipment->setTotalQty($shipmentData->getTotalQty()); + } + return $shipment; + } + + /** + * Process order items data and set the proper item qty + * + * @param Order $order + * @param array $shipmentItems + * @throws LocalizedException + */ + private function processOrderItems(Order $order, array $shipmentItems): void + { + /** @var Item $item */ + foreach ($order->getAllItems() as $item) { + if (isset($shipmentItems[$item->getSku()])) { + $qty = (float)$shipmentItems[$item->getSku()]['qty']; + $item->setQty($qty); + if ((float)$item->getQtyToShip() > 0) { + $item->setQtyShipped((float)$item->getQtyToShip()); + } + } + } + } +} diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderActionGroup.xml index dee2af6cd4053..48443512ee4c8 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderActionGroup.xml @@ -18,6 +18,8 @@ <argument name="option"/> </arguments> + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitForAddProductsButton"/> <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillSkuFilterConfigurable"/> <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchConfigurable"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminClickInvoiceButtonOrderViewActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminClickInvoiceButtonOrderViewActionGroup.xml new file mode 100644 index 0000000000000..4617437595c9c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminClickInvoiceButtonOrderViewActionGroup.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="AdminClickInvoiceButtonOrderViewActionGroup"> + <annotations> + <description>Click 'Invoice' button on the order view page.</description> + </annotations> + + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <waitForPageLoad stepKey="waitForProductPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceClickSubmitActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceClickSubmitActionGroup.xml new file mode 100644 index 0000000000000..69d042591c198 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceClickSubmitActionGroup.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="AdminInvoiceClickSubmitActionGroup"> + <annotations> + <description>Click submit invoice button for creating invoice.</description> + </annotations> + + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <waitForPageLoad stepKey="waitForInvoiceToBeCreated"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderClickSubmitOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderClickSubmitOrderActionGroup.xml new file mode 100644 index 0000000000000..cf64806cda084 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderClickSubmitOrderActionGroup.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="AdminOrderClickSubmitOrderActionGroup"> + <annotations> + <description>Click "Submit Order" button for order.</description> + </annotations> + + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + <waitForPageLoad stepKey="waitForOrderPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrdersGridClearFiltersActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrdersGridClearFiltersActionGroup.xml index b301864212c8b..877f7946d7609 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrdersGridClearFiltersActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrdersGridClearFiltersActionGroup.xml @@ -16,5 +16,6 @@ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToGridOrdersPage"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.enabledFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <waitForLoadingMaskToDisappear stepKey="waitAfterClearFilters"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrdersPageOpenActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrdersPageOpenActionGroup.xml new file mode 100644 index 0000000000000..2f08637cdcb53 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrdersPageOpenActionGroup.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="AdminOrdersPageOpenActionGroup"> + <annotations> + <description>Goes to the Admin Orders page.</description> + </annotations> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="openOrdersGridPage"/> + <waitForPageLoad stepKey="waitForLoadingPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertAdminFreePaymentMethodExistsOnCreateOrderPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertAdminFreePaymentMethodExistsOnCreateOrderPageActionGroup.xml new file mode 100644 index 0000000000000..75146e891a02a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertAdminFreePaymentMethodExistsOnCreateOrderPageActionGroup.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="AssertAdminFreePaymentMethodExistsOnCreateOrderPageActionGroup"> + <annotations> + <description>Checks the free payment on the Admin Create Order page.</description> + </annotations> + <click selector="{{AdminOrderFormPaymentSection.linkPaymentOptions}}" stepKey="clickPaymentMethods"/> + <waitForElementVisible selector="{{AdminOrderFormPaymentSection.freePaymentLabel}}" stepKey="waitForPaymentLabelVisible"/> + <see selector="{{AdminOrderFormPaymentSection.freePaymentLabel}}" userInput="No Payment Information Required" stepKey="checkFreePaymentLabel"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerSingleStoreActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerSingleStoreActionGroup.xml index 3d3efc705854d..6ec3cef59e22e 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerSingleStoreActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerSingleStoreActionGroup.xml @@ -18,6 +18,7 @@ <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(_defaultStore.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(_defaultStore.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontClickRefundTabCustomerOrderViewActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontClickRefundTabCustomerOrderViewActionGroup.xml new file mode 100644 index 0000000000000..fda22395f359c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontClickRefundTabCustomerOrderViewActionGroup.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="StorefrontClickRefundTabCustomerOrderViewActionGroup"> + <annotations> + <description>Click "Refund" tab for customer order view.</description> + </annotations> + + <click selector="{{StorefrontCustomerOrderSection.tabRefund}}" stepKey="clickRefundTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> 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/Section/AdminOrderFormPaymentSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml index a478d79d8553f..72fe45465c67b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml @@ -15,7 +15,7 @@ <element name="flatRateOption" type="radio" selector="#s_method_flatrate_flatrate" timeout="30"/> <element name="shippingError" type="text" selector="#order[has_shipping]-error"/> <element name="freeShippingOption" type="radio" selector="#s_method_freeshipping_freeshipping" timeout="30"/> - <element name="linkPaymentOptions" type="button" selector="#order-billing_method_summary>a"/> + <element name="linkPaymentOptions" type="button" selector="#order-billing_method_summary>a" timeout="30"/> <element name="blockPayment" type="text" selector="#order-billing_method"/> <element name="checkMoneyOption" type="radio" selector="#p_method_checkmo" timeout="30"/> <element name="checkBankTransfer" type="radio" selector="#p_method_banktransfer" timeout="30"/> @@ -28,5 +28,6 @@ <element name="cashOnDeliveryOption" type="radio" selector="#p_method_cashondelivery" timeout="30"/> <element name="purchaseOrderOption" type="radio" selector="#p_method_purchaseorder" timeout="30"/> <element name="purchaseOrderNumber" type="input" selector="#po_number"/> + <element name="freePaymentLabel" type="text" selector="#order-billing_method_form label[for='p_method_free']"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml index 70f37352fb183..085eea12e2243 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml @@ -11,5 +11,6 @@ <section name="StorefrontGuestOrderViewSection"> <element name="orderInformationTab" type="text" selector="//*[@class='nav item current']/strong[contains(text(), 'Order Information')]"/> <element name="printOrder" type="button" selector=".order-actions-toolbar .actions .print" timeout="30"/> + <element name="reorder" type="button" selector=".order-actions-toolbar .actions .order" timeout="30"/> </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..24e6c5eddf7db 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml @@ -19,23 +19,22 @@ <group value="sales"/> <group value="mtf_migrated"/> </annotations> + <before> <!-- Create customer --> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <!-- Create product --> <createData entity="SimpleProduct2" stepKey="createProduct"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </before> + <after> <!-- Admin log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <!-- Customer log out --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> - <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <!-- Delete product --> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> </after> @@ -58,8 +57,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/AdminAvailabilityCreditMemoWithNoPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml index 11a9957fe0041..182549a6fe301 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml @@ -25,7 +25,9 @@ </createData> <!-- Enable *Free Shipping* --> <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -37,7 +39,9 @@ <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="Simple_US_Customer.email"/> </actionGroup> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> +</actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> </after> @@ -68,9 +72,7 @@ <!-- Select Free shipping --> <actionGroup ref="OrderSelectFreeShippingActionGroup" stepKey="selectFreeShippingOption"/> - - <!--Click *Submit Order* button--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="clickSubmitOrder" /> <!--Click *Invoice* button--> <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startCreateInvoice"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml index bb1940357a7f4..80336ea29e9d5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml @@ -59,9 +59,7 @@ <!-- Select bank Transfer payment method --> <waitForElementVisible selector="{{AdminOrderFormPaymentSection.paymentBlock}}" stepKey="waitForPaymentOptions"/> <conditionalClick selector="{{AdminOrderFormPaymentSection.bankTransferOption}}" dependentSelector="{{AdminOrderFormPaymentSection.bankTransferOption}}" visible="true" stepKey="checkBankTransferOption"/> - - <!-- Submit order --> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <!-- Verify order information --> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCashOnDeliveryPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCashOnDeliveryPaymentMethodTest.xml index dafd00ff60b29..5f454152de20c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCashOnDeliveryPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCashOnDeliveryPaymentMethodTest.xml @@ -59,9 +59,7 @@ <!-- Select Cash On Delivery payment method --> <waitForElementVisible selector="{{AdminOrderFormPaymentSection.paymentBlock}}" stepKey="waitForPaymentOptions"/> <checkOption selector="{{AdminOrderFormPaymentSection.cashOnDeliveryOption}}" stepKey="selectCashOnDeliveryPaymentOption"/> - - <!-- Submit order --> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <!--Verify order information--> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml index c0ebbe450119e..7d6e2048c9432 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml @@ -94,8 +94,12 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct1"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> @@ -145,7 +149,7 @@ <actionGroup ref="AdminSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> <!-- Submit order --> <comment userInput="Submit order" stepKey="submitOrderComment"/> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <!-- Verify order information --> <comment userInput="Verify order information" stepKey="verifyOrderInformationComment"/> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> @@ -215,8 +219,7 @@ </actionGroup> <!-- Open Order Index Page --> <comment userInput="Open Order Index Page" stepKey="openOrderIndexPageComemnt"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter order using orderId --> <comment userInput="Filter order using orderId" stepKey="filterOrderUsingOrderIdComment"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml index 256417c0d0d10..f47513bcadedd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml @@ -58,9 +58,7 @@ <!--Select FlatRate shipping method--> <actionGroup ref="AdminSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - - <!--Submit order--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <!--Verify order information--> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> @@ -76,8 +74,7 @@ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStockStatus"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId --> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> @@ -98,8 +95,7 @@ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStockStatusAfterCancelOrder"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders1"/> - <waitForPageLoad stepKey="waitForPageLoad6"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders1"/> <!-- Filter Order using orderId --> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById1"> @@ -111,7 +107,7 @@ <!-- Reorder the product --> <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickOnReorderButton"/> <waitForPageLoad stepKey="waitForReorderFormToLoad"/> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder1"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder1" /> <!-- Assert Simple Product Quantity in backend after Reorder --> <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterAndSelectTheProduct2"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml index 477676085cf2e..9f09a59fa8d5e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml @@ -35,8 +35,12 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct"> <field key="price">10.00</field> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{DisablePurchaseOrderConfigData.path}} {{DisablePurchaseOrderConfigData.value}}" stepKey="disablePurchaseOrderPayment"/> @@ -65,9 +69,7 @@ <fillField selector="{{AdminOrderFormPaymentSection.purchaseOrderNumber}}" userInput="{{PurchaseOrderNumber.number}}" stepKey="fillPurchaseOrderNumber"/> <click selector="{{AdminOrderFormActionSection.pageHeader}}" stepKey="clickOnHeader"/> <waitForPageLoad stepKey="waitForPageToLoad"/> - - <!--Submit order--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <!--Verify order information--> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml index d22e11bca3d0e..6ce9909d06be5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml @@ -60,9 +60,7 @@ <!--Select FlatRate shipping method--> <actionGroup ref="AdminSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - - <!--Submit order--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <!--Verify order information--> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml index b8612f7f795fb..33bc1a39ca11a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml @@ -58,7 +58,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Go to bundle product page--> <amOnPage url="{{StorefrontProductPage.url($$createCategory.name$$)}}" stepKey="navigateToBundleProductPage"/> @@ -77,7 +79,7 @@ <!--Go to order page submit invoice--> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml index 2935a56a6c0a1..6ed8510db777c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml @@ -63,7 +63,7 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> @@ -129,13 +129,11 @@ </actionGroup> <!-- Assert refunded Grand Total on frontend --> - <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="onAccountPage"/> - <waitForPageLoad stepKey="waitForPage"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="onAccountPage"/> <scrollTo selector="{{StorefrontCustomerResentOrdersSection.blockResentOrders}}" stepKey="scrollToResent"/> <click selector="{{StorefrontCustomerResentOrdersSection.viewOrder({$grabOrderId})}}" stepKey="clickOnOrder"/> <waitForPageLoad stepKey="waitForViewOrder"/> - <click selector="{{StorefrontCustomerOrderSection.tabRefund}}" stepKey="clickRefund"/> - <waitForPageLoad stepKey="waitRefundsLoad"/> + <actionGroup ref="StorefrontClickRefundTabCustomerOrderViewActionGroup" stepKey="clickRefund"/> <scrollTo selector="{{StorefrontCustomerOrderSection.grandTotalRefund}}" stepKey="scrollToGrandTotal"/> <see selector="{{StorefrontCustomerOrderSection.grandTotalRefund}}" userInput="555.00" stepKey="seeGrandTotal"/> </test> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml index ab3a2cc647740..ff5dc0e36fdbd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml @@ -93,7 +93,7 @@ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml index d9ae276de31a0..68301187d3d31 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml @@ -58,7 +58,7 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> @@ -123,13 +123,11 @@ </actionGroup> <!-- Assert refunded Grand Total on frontend --> - <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="onAccountPage"/> - <waitForPageLoad stepKey="waitForPage"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="onAccountPage"/> <scrollTo selector="{{StorefrontCustomerResentOrdersSection.blockResentOrders}}" stepKey="scrollToResent"/> <click selector="{{StorefrontCustomerResentOrdersSection.viewOrder({$grabOrderId})}}" stepKey="clickOnOrder"/> <waitForPageLoad stepKey="waitForViewOrder"/> - <click selector="{{StorefrontCustomerOrderSection.tabRefund}}" stepKey="clickRefund"/> - <waitForPageLoad stepKey="waitRefundsLoad"/> + <actionGroup ref="StorefrontClickRefundTabCustomerOrderViewActionGroup" stepKey="clickRefund"/> <scrollTo selector="{{StorefrontCustomerOrderSection.grandTotalRefund}}" stepKey="scrollToGrandTotal"/> <see selector="{{StorefrontCustomerOrderSection.grandTotalRefund}}" userInput="110.00" stepKey="seeGrandTotal"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml index d888e6841e34d..a1027a9987b1f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml @@ -64,7 +64,8 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> @@ -115,13 +116,11 @@ </actionGroup> <!-- Assert refunded Grand Total on frontend --> - <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="onAccountPage"/> - <waitForPageLoad stepKey="waitForPage"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="onAccountPage"/> <scrollTo selector="{{StorefrontCustomerResentOrdersSection.blockResentOrders}}" stepKey="scrollToResent"/> <click selector="{{StorefrontCustomerResentOrdersSection.viewOrder({$grabOrderId})}}" stepKey="clickOnOrder"/> <waitForPageLoad stepKey="waitForViewOrder"/> - <click selector="{{StorefrontCustomerOrderSection.tabRefund}}" stepKey="clickRefund"/> - <waitForPageLoad stepKey="waitRefundsLoad"/> + <actionGroup ref="StorefrontClickRefundTabCustomerOrderViewActionGroup" stepKey="clickRefund"/> <scrollTo selector="{{StorefrontCustomerOrderSection.grandTotalRefund}}" stepKey="scrollToGrandTotal"/> <see selector="{{StorefrontCustomerOrderSection.grandTotalRefund}}" userInput="555.00" stepKey="seeGrandTotal"/> </test> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml index 7974d594eb99c..141fa2a9e5d06 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml @@ -61,13 +61,12 @@ <fillField selector="{{AdminOrderFormPaymentSection.fieldPurchaseOrderNumber}}" userInput="123456" stepKey="fillPONumber"/> <click selector="{{AdminOrderFormPaymentSection.blockPayment}}" stepKey="unfocus"/> <waitForPageLoad stepKey="waitForJavascriptToFinish"/> - <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="submitOrder"/> - <waitForPageLoad stepKey="waitForSubmitOrderPage"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <see stepKey="seeSuccessMessageForOrder" selector="{{AdminIndexManagementSection.successMessage}}" userInput="You created the order."/> <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> @@ -116,13 +115,11 @@ </actionGroup> <!-- Assert refunded Grand Total on frontend --> - <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="onAccountPage"/> - <waitForPageLoad stepKey="waitForPage"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="onAccountPage"/> <scrollTo selector="{{StorefrontCustomerResentOrdersSection.blockResentOrders}}" stepKey="scrollToResent"/> <click selector="{{StorefrontCustomerResentOrdersSection.viewOrder({$grabOrderId})}}" stepKey="clickOnOrder"/> <waitForPageLoad stepKey="waitForViewOrder"/> - <click selector="{{StorefrontCustomerOrderSection.tabRefund}}" stepKey="clickRefund"/> - <waitForPageLoad stepKey="waitRefundsLoad"/> + <actionGroup ref="StorefrontClickRefundTabCustomerOrderViewActionGroup" stepKey="clickRefund"/> <scrollTo selector="{{StorefrontCustomerOrderSection.grandTotalRefund}}" stepKey="scrollToGrandTotal"/> <see selector="{{StorefrontCustomerOrderSection.grandTotalRefund}}" userInput="555.00" stepKey="seeGrandTotal"/> </test> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml new file mode 100644 index 0000000000000..8b8789d488b9c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest"> + <annotations> + <stories value="Github issue: #22762 Credit Memo with Zero Total: Order Status 'Complete' and not 'Closed'"/> + <title value="Create Credit Memo with zero total."/> + <description value="Assert order status after create CreditMemo with zero total."/> + <severity value="MAJOR"/> + <group value="sales"/> + <testCaseId value="MC-35848"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct_zero" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="FreeShippingMethodDisableConfig" stepKey="disableFreeShipping"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> + <argument name="product" value="$createProduct$"/> + <argument name="productQty" value="1"/> + </actionGroup> + <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerInfo"> + <argument name="customer" value="$createCustomer$"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <actionGroup ref="OrderSelectFreeShippingActionGroup" stepKey="selectFlatRate"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + + <actionGroup ref="AdminCreateInvoiceAndCreditMemoActionGroup" stepKey="createCreditMemo"/> + + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="seeOrderClose"> + <argument name="status" value="Closed"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml index eea948d902282..91a8f95880fbc 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"/> @@ -50,8 +50,7 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> @@ -69,10 +68,8 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoice"/> - <waitForPageLoad stepKey="waitForNewInvoicePageToLoad"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForPageLoad stepKey="waitForInvoiceToBeCreated"/> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> <click selector="{{AdminOrderDetailsOrderViewSection.invoices}}" stepKey="clickInvoices"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask5" /> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml index 1401930131b13..5c49d29ddf22e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml @@ -93,9 +93,7 @@ <!--Select FlatRate shipping method--> <actionGroup ref="OrderSelectFlatRateShippingActionGroup" stepKey="orderSelectFlatRateShippingMethod"/> - - <!--Submit order--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <!--Verify order information--> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml index 0be7e20be5aea..68a8e9d347ddd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml @@ -24,7 +24,9 @@ <requiredEntity createDataKey="category"/> </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!--Clean up created test data.--> @@ -33,7 +35,9 @@ <!--Enable required 'email' field on create order page.--> <magentoCLI command="config:set {{EnableEmailRequiredForOrder.path}} {{EnableEmailRequiredForOrder.value}}" stepKey="enableRequiredFieldEmailForAdminOrderCreation"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Create order.--> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml index ade1f783c1309..b5c9e9443d1f9 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml @@ -32,7 +32,7 @@ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <createData entity="DisabledMinimumOrderAmount" stepKey="disableMinimumOrderAmount"/> <actionGroup ref="ClearCacheActionGroup" stepKey="clearCacheAfter"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Admin creates order--> @@ -63,9 +63,7 @@ <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" stepKey="seeOrderSubTotal"/> <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProduct.shipping}}" stepKey="seeOrderShipping"/> <see selector="{{AdminOrderFormTotalSection.grandTotal}}" userInput="${{AdminOrderSimpleProduct.grandTotal}}" stepKey="seeCorrectGrandTotal"/> - - <!--Submit Order and verify information--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="clickSubmitOrder" /> <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPage"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeSuccessMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml index 1c59f6f936cef..bf8e7e1868184 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml @@ -28,7 +28,9 @@ <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShippingMethod"/> <createData entity="setFreeShippingSubtotal" stepKey="setFreeShippingSubtotal"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -38,7 +40,9 @@ <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> <createData entity="setFreeShippingSubtotalToDefault" stepKey="setFreeShippingSubtotalToDefault"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="cache:flush" stepKey="flushCache2"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Create new order with existing customer--> <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> @@ -54,8 +58,7 @@ <!--Click *Get shipping methods and rates* and see that Free Shipping is absent--> <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="clickGetShippingMehods"/> <dontSeeElement selector="{{AdminOrderFormPaymentSection.freeShippingOption}}" stepKey="seeAbsentFreeShipping"/> - <!--Submit Order and verify that Order isn't placed--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="clickSubmitOrder" /> <dontSeeElement selector="{{AdminOrderFormMessagesSection.success}}" stepKey="seeSuccessMessage"/> <seeElement selector="{{AdminOrderFormMessagesSection.error}}" stepKey="seeErrorMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml index 8d328beab1adc..a73e64cfbca10 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml @@ -60,9 +60,7 @@ <!--Select FlatRate shipping method--> <actionGroup ref="AdminSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - - <!-- Submit order --> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <!-- Verify order information --> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml index a89e9f7ce6ebe..b337af3753db3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml @@ -63,10 +63,8 @@ <actionGroup ref="AdminCreateInvoiceAndCreditMemoActionGroup" stepKey="createCreditMemo"/> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <!-- Select Mass Action according to dataset: Cancel --> <actionGroup ref="AdminTwoOrderActionOnGridActionGroup" stepKey="massActionCancel"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml index 45cbe23042e03..6eb4195524224 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml @@ -63,10 +63,8 @@ <actionGroup ref="AdminCreateInvoiceAndCreditMemoActionGroup" stepKey="createCreditMemo"/> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <!-- Select Mass Action according to dataset: Cancel --> <actionGroup ref="AdminTwoOrderActionOnGridActionGroup" stepKey="massActionCancel"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml index 22b2d69a73090..41964cbf605da 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml @@ -50,10 +50,8 @@ <actionGroup ref="AdminCreateInvoiceAndShipmentActionGroup" stepKey="createShipment"/> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <!-- Select Mass Action according to dataset: Hold --> <actionGroup ref="AdminOrderActionOnGridActionGroup" stepKey="actionHold"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml index 4b690a00ee9ed..2a4ad174abae0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml @@ -60,10 +60,8 @@ <actionGroup ref="AdminCreateInvoiceActionGroup" stepKey="createInvoice"/> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <!-- Select Mass Action according to dataset: Hold --> <actionGroup ref="AdminTwoOrderActionOnGridActionGroup" stepKey="massActionHold"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml index e1d934f794142..27ed62fee35e2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml @@ -47,10 +47,8 @@ </assertNotEmpty> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <!-- Select Mass Action according to dataset: Unhold --> <actionGroup ref="AdminOrderActionOnGridActionGroup" stepKey="actionUnhold"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml index 86a3e381cb237..163da4917b50a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml @@ -46,10 +46,8 @@ </assertNotEmpty> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <!-- Select Mass Action according to dataset: Cancel --> <actionGroup ref="AdminOrderActionOnGridActionGroup" stepKey="ActionCancel"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml new file mode 100644 index 0000000000000..fea3fe68fd522 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml @@ -0,0 +1,140 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminOrderPagerTest"> + <annotations> + <features value="Sales"/> + <stories value="Admin order pager"/> + <title value="Check pager is working"/> + <description value="Check Pager in order add products grid"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-35349"/> + <useCaseId value="MC-35316"/> + <group value="sales"/> + </annotations> + <before> + <!-- 21 products created and category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct01"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct02"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct03"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct04"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct05"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct06"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct07"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct08"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct09"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct10"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct11"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct12"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct13"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct14"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct15"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct16"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct17"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct18"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct19"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct20"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct21"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Customer is created --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Login to Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <!-- Delete products --> + <deleteData createDataKey="createProduct01" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct02" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct03" stepKey="deleteProduct3"/> + <deleteData createDataKey="createProduct04" stepKey="deleteProduct4"/> + <deleteData createDataKey="createProduct05" stepKey="deleteProduct5"/> + <deleteData createDataKey="createProduct06" stepKey="deleteProduct6"/> + <deleteData createDataKey="createProduct07" stepKey="deleteProduct7"/> + <deleteData createDataKey="createProduct08" stepKey="deleteProduct8"/> + <deleteData createDataKey="createProduct09" stepKey="deleteProduct9"/> + <deleteData createDataKey="createProduct10" stepKey="deleteProduct10"/> + <deleteData createDataKey="createProduct11" stepKey="deleteProduct11"/> + <deleteData createDataKey="createProduct12" stepKey="deleteProduct12"/> + <deleteData createDataKey="createProduct13" stepKey="deleteProduct13"/> + <deleteData createDataKey="createProduct14" stepKey="deleteProduct14"/> + <deleteData createDataKey="createProduct15" stepKey="deleteProduct15"/> + <deleteData createDataKey="createProduct16" stepKey="deleteProduct16"/> + <deleteData createDataKey="createProduct17" stepKey="deleteProduct17"/> + <deleteData createDataKey="createProduct18" stepKey="deleteProduct18"/> + <deleteData createDataKey="createProduct19" stepKey="deleteProduct19"/> + <deleteData createDataKey="createProduct20" stepKey="deleteProduct20"/> + <deleteData createDataKey="createProduct21" stepKey="deleteProduct21"/> + + <!-- Delete Category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete Customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Initiate create new order --> + <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + + <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitForAddProductsButtonAppeared"/> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <dontSee selector="{{AdminDataGridPaginationSection.prevPageActive}}" stepKey="previousPageDisabled"/> + <click selector="{{AdminDataGridPaginationSection.nextPageActive}}" stepKey="clickNextPage"/> + <seeInField selector="{{AdminDataGridPaginationSection.selectedPage}}" userInput="2" stepKey="seeSecondPageOrderGrid"/> + <click selector="{{AdminDataGridPaginationSection.prevPageActive}}" stepKey="clickPreviousPage"/> + <seeInField selector="{{AdminDataGridPaginationSection.selectedPage}}" userInput="1" stepKey="seeFirstPageOrderGrid"/> + <dontSee selector="{{AdminDataGridPaginationSection.prevPageActive}}" stepKey="prevPageDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml index bfd75a69b81d6..d2ded1cc73d2b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml @@ -52,10 +52,8 @@ <see userInput="You put the order on hold." stepKey="seeHoldMessage"/> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <!-- Select Mass Action according to dataset: Unhold --> <actionGroup ref="AdminOrderActionOnGridActionGroup" stepKey="actionUnold"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml index 0ff5080bd8df2..4799984b76745 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml @@ -25,6 +25,16 @@ <createData entity="SimpleProduct2" stepKey="createSimpleProductApi"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> + <!-- Clearing cache just in case --> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <amOnPage url="{{AdminCatalogPriceRuleGridPage.url}}" stepKey="goToAdminCatalogPriceRuleGridPage2"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded2"/> <!--Create the catalog price rule --> <createData entity="CatalogRuleToPercent" stepKey="createCatalogRule"/> <!--Create order via API--> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml index 54ae549967a3b..beaf098eee246 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml @@ -114,9 +114,7 @@ <!-- Checkout select Check/Money Order payment --> <actionGroup ref="SelectCheckMoneyPaymentMethodActionGroup" stepKey="selectCheckMoneyPayment"/> - - <!--Submit order--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <!--Verify order information--> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml index 85665dfc1b00e..bd6a21e3112ca 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml @@ -33,8 +33,7 @@ </after> <!--Create order via Admin--> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> - <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToOrderIndexPage"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml index 7615cc219d430..727aef99352ec 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -32,8 +32,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> <!--<actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/>--> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> - <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToOrderIndexPage"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml index fd26ca1ca601e..2bedb16f3d1dc 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> <!--<actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/>--> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> - <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToOrderIndexPage"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml index 692f293ef3a75..226524341efdd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml @@ -55,7 +55,7 @@ <!--Click unassign and verify AssertOrderStatusSuccessUnassignMessage--> <click selector="{{AdminOrderStatusGridSection.unassign}}" stepKey="clickUnassign"/> - <see selector="{{AdminMessagesSection.success}}" userInput="You have unassigned the order status." stepKey="seeAssertOrderStatusSuccessUnassignMessage"/> + <waitForText selector="{{AdminMessagesSection.success}}" userInput="You have unassigned the order status." stepKey="seeAssertOrderStatusSuccessUnassignMessage"/> <!--Verify the order status grid page shows the updated order status and verify AssertOrderStatusInGrid--> <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="seeAssertOrderStatusInGrid"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml index f0f4cf9d1a468..a5d210a9765ad 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml @@ -85,8 +85,7 @@ <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> <!-- Assert order status is correct --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridById"> <argument name="orderId" value="$getOrderId"/> </actionGroup> @@ -100,8 +99,7 @@ <waitForPageLoad stepKey="waitForCustomerLogin"/> <!-- Open My Orders --> - <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="goToCustomerDashboardPage"/> - <waitForPageLoad stepKey="waitForCustomerDashboardPageLoad"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="goToCustomerDashboardPage"/> <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToMyOrdersPage"> <argument name="menu" value="My Orders"/> </actionGroup> @@ -110,8 +108,7 @@ <see selector="{{StorefrontOrderInformationMainSection.emptyMessage}}" userInput="You have placed no orders." stepKey="seeEmptyMessage"/> <!-- Cancel order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToAdminOrdersPage"/> - <waitForPageLoad stepKey="waitForAdminOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToAdminOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridByOrderId"> <argument name="orderId" value="$getOrderId"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CheckXSSVulnerabilityDuringOrderCreationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CheckXSSVulnerabilityDuringOrderCreationTest.xml index df6a797372e62..528b6b61f4842 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CheckXSSVulnerabilityDuringOrderCreationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CheckXSSVulnerabilityDuringOrderCreationTest.xml @@ -54,7 +54,7 @@ <!-- Try to create order in admin with provided email --> <actionGroup ref="NavigateToNewOrderPageNewCustomerSingleStoreActionGroup" stepKey="navigateToNewOrderPage"/> <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer_Incorrect_Email.email}}" stepKey="fillEmailAddressAdminPanel"/> - <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="clickSubmitOrder" /> <!-- Order can not be created --> <actionGroup ref="AssertAdminEmailValidationMessageOnCheckoutActionGroup" stepKey="assertErrorMessageAdminPanel"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml index d8a3db76da05e..de6e7ff22b7af 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml @@ -77,19 +77,18 @@ <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> <!-- Open created order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridById"> <argument name="orderId" value="$getOrderId"/> </actionGroup> <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickCreatedOrderInGrid"/> <!-- Go to invoice tab and fill data --> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceAction"/> <fillField selector="{{AdminOrderInvoiceViewSection.invoiceQty}}" userInput="1" stepKey="fillInvoiceQuantity"/> <click selector="{{AdminOrderInvoiceViewSection.updateInvoiceBtn}}" stepKey="clickUpdateQtyInvoiceBtn"/> <fillField selector="{{AdminInvoiceTotalSection.invoiceComment}}" userInput="comment" stepKey="writeComment"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <!-- Assert invoice with shipment success message --> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> @@ -103,8 +102,7 @@ <grabFromCurrentUrl regex="~/invoice_id/(\d+)/~" stepKey="grabInvoiceId"/> <!-- Assert invoice in invoices tab --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdForAssertingInvoiceBtn"> <argument name="orderId" value="$getOrderId"/> </actionGroup> @@ -135,8 +133,7 @@ <waitForPageLoad stepKey="waitForCustomerLogin"/> <!-- Open My Account > My Orders --> - <amOnPage stepKey="goToMyAccountPage" url="{{StorefrontCustomerDashboardPage.url}}"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="goToMyAccountPage"/> <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToSidebarMenu"> <argument name="menu" value="My Orders"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml index c58b95a41b157..57e42e9b190e3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml @@ -77,19 +77,18 @@ <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> <!-- Open created order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridById"> <argument name="orderId" value="$getOrderId"/> </actionGroup> <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickCreatedOrderInGrid"/> <!-- Go to invoice tab and fill data --> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceAction"/> <fillField selector="{{AdminOrderInvoiceViewSection.invoiceQty}}" userInput="1" stepKey="fillInvoiceQuantity"/> <click selector="{{AdminOrderInvoiceViewSection.updateInvoiceBtn}}" stepKey="clickUpdateQtyInvoiceBtn"/> <fillField selector="{{AdminInvoiceTotalSection.invoiceComment}}" userInput="comment" stepKey="writeComment"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <!-- Assert invoice with shipment success message --> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> @@ -101,8 +100,7 @@ <waitForPageLoad stepKey="waitForCustomerLogin"/> <!-- Open My Account > My Orders --> - <amOnPage stepKey="goToMyAccountPage" url="{{StorefrontCustomerDashboardPage.url}}"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="goToMyAccountPage"/> <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToSidebarMenu"> <argument name="menu" value="My Orders"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml index 1c92c2dae3712..10c6be60f5ba1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml @@ -71,18 +71,17 @@ <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> <!-- Open created order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridById"> <argument name="orderId" value="$getOrderId"/> </actionGroup> <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickCreatedOrderInGrid"/> <!-- Go to invoice tab and fill data --> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceAction"/> <click selector="{{AdminInvoicePaymentShippingSection.CreateShipment}}" stepKey="createShipment"/> <fillField selector="{{AdminInvoiceTotalSection.invoiceComment}}" userInput="comment" stepKey="writeComment"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <!-- Assert invoice with shipment success message --> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the invoice and shipment." stepKey="seeSuccessMessage"/> @@ -101,8 +100,7 @@ <grabFromCurrentUrl regex="~/invoice_id/(\d+)/~" stepKey="grabInvoiceId"/> <!-- Assert no invoice button --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdForAssertingInvoiceBtn"> <argument name="orderId" value="$getOrderId"/> </actionGroup> @@ -126,8 +124,7 @@ <waitForPageLoad stepKey="waitForCustomerLogin"/> <!-- Open My Account > My Orders --> - <amOnPage stepKey="goToMyAccountPage" url="{{StorefrontCustomerDashboardPage.url}}"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="goToMyAccountPage"/> <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToSidebarMenu"> <argument name="menu" value="My Orders"/> </actionGroup> @@ -146,8 +143,7 @@ <grabFromCurrentUrl regex="~/shipment_id/(\d+)/~" stepKey="grabShipmentId"/> <!-- Assert no ship button --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToAdminOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageToLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToAdminOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdForAssertingShipBtn"> <argument name="orderId" value="$getOrderId"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml index b562073a1276f..cd36547a877ec 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml @@ -86,17 +86,16 @@ <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> <!-- Open created order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridById"> <argument name="orderId" value="$getOrderId"/> </actionGroup> <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickCreatedOrderInGrid"/> <!-- Go to invoice tab and fill data --> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceAction"/> <fillField selector="{{AdminInvoiceTotalSection.invoiceComment}}" userInput="comment" stepKey="writeComment"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <!-- Assert invoice with shipment success message --> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> @@ -108,8 +107,7 @@ <waitForPageLoad stepKey="waitForCustomerLogin"/> <!-- Open My Account > My Orders --> - <amOnPage stepKey="goToMyAccountPage" url="{{StorefrontCustomerDashboardPage.url}}"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="goToMyAccountPage"/> <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToSidebarMenu"> <argument name="menu" value="My Orders"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml index 776d84ac230b8..ca705405809bd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml @@ -75,8 +75,12 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct1"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> @@ -182,10 +186,7 @@ <waitForPageLoad stepKey="waitForShippingMethods"/> <click selector="{{AdminOrderFormPaymentSection.freeShippingOption}}" stepKey="chooseShippingMethod"/> <waitForPageLoad stepKey="waitForPageToLoad"/> - - <!-- Submit order --> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> - <waitForPageLoad stepKey="waitForAdminOrderFormLoad"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> <!-- Verify order information --> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml index bd76f5c10b488..20dcb262b5831 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"/> @@ -83,8 +83,7 @@ <!-- Choose Shippping - Flat Rate Shipping --> <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment3"/> @@ -95,8 +94,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> <!-- Search for Order in the order grid --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForPageLoad time="30" stepKey="waitForOrderListPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrdersPage"/> <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilter"/> <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="searchOrderNum"/> <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearch"/> @@ -104,8 +102,7 @@ <!-- Create invoice --> <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> - <waitForPageLoad stepKey="waitForNewInvoicePageToLoad"/> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceButton"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoiceInPageTitle" after="clickInvoiceButton"/> <!-- Verify Invoice Totals including subTotal Shipping Discount and GrandTotal --> @@ -129,7 +126,7 @@ <see selector="{{AdminInvoiceTotalSection.grandTotal}}" userInput="$113.00" stepKey="seeCorrectGrandTotal"/> <grabTextFrom selector="{{AdminInvoiceTotalSection.grandTotal}}" stepKey="grabInvoiceGrandTotal" after="seeCorrectGrandTotal"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage1"/> <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Processing" stepKey="seeOrderProcessing"/> 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 f374741c247d4..452d65ea5ae57 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml @@ -57,7 +57,9 @@ <!-- Change configuration --> <magentoCLI command="config:set reports/options/enabled 1" stepKey="enableReportModule"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Admin logout --> @@ -94,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/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml index 0888132669177..1c67d778937d1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml @@ -10,8 +10,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontCreateOrdersWithMoveJSCodeBottomTest"> <annotations> + <stories value="Create a product and orders with set 'Move Js code to the bottom' to 'Yes'."/> <title value="Create a product and orders with set 'Move Js code to the bottom' to 'Yes'."/> <description value="Create a product and orders with a set 'Move JS code to the bottom of the page' to 'Yes' for registered customers and guests."/> + <severity value="MAJOR"/> </annotations> <before> <magentoCLI command="config:set {{StorefrontEnableMoveJsCodeBottom.path}} {{StorefrontEnableMoveJsCodeBottom.value}}" stepKey="moveJsCodeBottomEnable"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml index 5cc4fae330d05..6b6b0b2ef4a16 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml @@ -90,8 +90,12 @@ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> <!-- Reindex and flush the cache to display products on the category page --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete category and products --> @@ -203,15 +207,13 @@ <!-- Place Order --> <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="onCheckout"/> <see userInput="21" selector="{{CheckoutOrderSummarySection.itemsQtyInCart}}" stepKey="see21Products"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNextButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForCheckoutLoad"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="placeOrder"/> <waitForPageLoad stepKey="waitForSuccess"/> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> <!-- Go to My Account > My Orders page --> - <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="onMyAccount"/> - <waitForPageLoad stepKey="waitForAccountPage"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="onMyAccount"/> <click selector="{{StorefrontCustomerSidebarSection.sidebarTab('My Orders')}}" stepKey="clickOnMyOrders"/> <waitForPageLoad stepKey="waitForOrdersLoad"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml index 20261de502ea3..9fba25688702d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml @@ -193,15 +193,13 @@ <!-- Place Order --> <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="onCheckout"/> <see userInput="20" selector="{{CheckoutOrderSummarySection.itemsQtyInCart}}" stepKey="see20Products"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNextButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForCheckoutLoad"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="placeOrder"/> <waitForPageLoad stepKey="waitForSuccess"/> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> <!-- Go to My Account > My Orders page --> - <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="onMyAccount"/> - <waitForPageLoad stepKey="waitForAccountPage"/> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="onMyAccount"/> <click selector="{{StorefrontCustomerSidebarSection.sidebarTab('My Orders')}}" stepKey="clickOnMyOrders"/> <waitForPageLoad stepKey="waitForOrdersLoad"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml index 00117c56de439..9fdf577abd873 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml @@ -44,8 +44,8 @@ <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" stepKey="checkOption"/> <waitForAjaxLoad stepKey="waitForAjaxLoad"/> <grabValueFrom selector="{{AdminProductDownloadableSection.addLinkTitleInput('0')}}" stepKey="grabLink"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSave"/> - <waitForLoadingMaskToDisappear stepKey="waitForSave"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSave"/> <!-- Create configurable Product --> <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> @@ -164,8 +164,12 @@ <!-- Create Customer Account --> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Place order with options according to dataset --> <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="newOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml new file mode 100644 index 0000000000000..0718783534925 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontReorderAsGuestTest"> + <annotations> + <stories value="Reorder"/> + <title value="Make reorder as guest on Frontend"/> + <description value="Make reorder as guest on Frontend"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-34465"/> + <group value="sales"/> + </annotations> + <before> + <!--Create simple product.--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create Customer Account --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCreateCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + + <!-- Order a product --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToPDP"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckout"/> + <waitForPageLoad stepKey="waitFroPaymentSelectionPageLoad"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="fillAddress"> + <argument name="customerVar" value="$$createCustomer$$"/> + <argument name="customerAddressVar" value="US_Address_TX"/> + </actionGroup> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" + stepKey="waitForPlaceOrderButtonVisible"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="placeOrder"/> + <waitForPageLoad stepKey="waitUntilOrderPlaced"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="getOrderId"/> + <assertNotEmpty stepKey="assertOrderIdIsNotEmpty" after="getOrderId"> + <actualResult type="const">$getOrderId</actualResult> + </assertNotEmpty> + + <!-- Find the Order on frontend > Navigate to: Orders and Returns --> + <amOnPage url="{{StorefrontGuestOrderSearchPage.url}}" stepKey="amOnOrdersAndReturns"/> + <waitForPageLoad stepKey="waiForStorefrontPage"/> + + <!-- Fill the form with correspondent Order data --> + <actionGroup ref="StorefrontFillOrdersAndReturnsFormActionGroup" stepKey="fillOrder"> + <argument name="orderNumber" value="{$getOrderId}"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Click on the "Continue" button --> + <click selector="{{StorefrontGuestOrderSearchSection.continue}}" stepKey="clickContinue"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- Click 'Reorder' link --> + <click selector="{{StorefrontGuestOrderViewSection.reorder}}" stepKey="clickReturnLink"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + + <!--Check that product from order is visible in cart after reorder --> + <seeElement selector="{{CheckoutCartProductSection.ProductLinkByName($$createSimpleProduct.name$$)}}" stepKey="seeProductInCart"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml index d49ea4cfcbec7..ecf71e2bc80b3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml @@ -28,11 +28,15 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php index 4d8b8033f60da..7123a81306ef1 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php @@ -11,7 +11,7 @@ use Magento\Backend\Block\Template\Context; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Layout; -use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Sales\Block\Order\Email\Items\DefaultItems; use Magento\Sales\Model\Order\Item as OrderItem; use PHPUnit\Framework\MockObject\MockObject; @@ -20,7 +20,7 @@ class DefaultItemsTest extends TestCase { /** - * @var MockObject|\Magento\Sales\Block\Order\Email\Items\DefaultItem + * @var MockObject|DefaultItems */ protected $block; @@ -39,9 +39,16 @@ class DefaultItemsTest extends TestCase */ protected $objectManager; - /** @var MockObject|Item */ + /** + * @var MockObject|OrderItem + */ protected $itemMock; + /** + * @var MockObject|QuoteItem + */ + protected $quoteItemMock; + /** * Initialize required data */ @@ -54,16 +61,6 @@ protected function setUp(): void ->setMethods(['getBlock']) ->getMock(); - $this->block = $this->objectManager->getObject( - DefaultItems::class, - [ - 'context' => $this->objectManager->getObject( - Context::class, - ['layout' => $this->layoutMock] - ) - ] - ); - $this->priceRenderBlock = $this->getMockBuilder(Template::class) ->disableOriginalConstructor() ->setMethods(['setItem', 'toHtml']) @@ -72,16 +69,47 @@ protected function setUp(): void $this->itemMock = $this->getMockBuilder(OrderItem::class) ->disableOriginalConstructor() ->getMock(); + + $this->quoteItemMock = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->setMethods(['getQty']) + ->getMock(); + + $this->block = $this->objectManager->getObject( + DefaultItems::class, + [ + 'context' => $this->objectManager->getObject( + Context::class, + ['layout' => $this->layoutMock] + ), + 'data' => [ + 'item' => $this->quoteItemMock + ] + ] + ); } - public function testGetItemPrice() + /** + * @param float $price + * @param string $html + * @param float $quantity + * @dataProvider getItemPriceDataProvider + * */ + public function testGetItemPrice($price, $html, $quantity) { - $html = '$34.28'; - $this->layoutMock->expects($this->once()) ->method('getBlock') ->with('item_price') ->willReturn($this->priceRenderBlock); + $this->quoteItemMock->expects($this->any()) + ->method('getQty') + ->willReturn($quantity); + $this->itemMock->expects($this->any()) + ->method('setRowTotal') + ->willReturn($price * $quantity); + $this->itemMock->expects($this->any()) + ->method('setBaseRowTotal') + ->willReturn($price * $quantity); $this->priceRenderBlock->expects($this->once()) ->method('setItem') @@ -93,4 +121,15 @@ public function testGetItemPrice() $this->assertEquals($html, $this->block->getItemPrice($this->itemMock)); } + + /** + * @return array + */ + public function getItemPriceDataProvider() + { + return [ + 'get default item price' => [34.28,'$34.28',1.0], + 'get item price with quantity 2.0' => [12.00,'$24.00',2.0] + ]; + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php index 4cf571d3b6108..67f1931cf7bd1 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php @@ -49,10 +49,11 @@ class CreditmemoFactoryTest extends TestCase */ protected function setUp(): void { - $this->orderItemMock = $this->createPartialMock( - Item::class, - ['getChildrenItems', 'isDummy', 'getHasChildren', 'getId', 'getParentItemId'] - ); + $this->orderItemMock = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods(['getChildrenItems', 'isDummy', 'getId', 'getParentItemId']) + ->addMethods(['getHasChildren']) + ->getMock(); $this->orderChildItemOneMock = $this->createPartialMock( Item::class, ['getQtyToRefund', 'getId'] diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/DiscountTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/DiscountTest.php new file mode 100644 index 0000000000000..f7587031337a7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/DiscountTest.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model\Order\Invoice\Total; + +use Magento\Sales\Model\Order\Invoice\Total\Discount; +use PHPUnit\Framework\TestCase; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Order\Invoice\Item as InvoiceItem; +use Magento\Sales\Model\Order\Item as OrderItem; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DiscountTest extends TestCase +{ + /** + * @var Discount + */ + protected $model; + + /** + * @var Order|MockObject + */ + protected $order; + + /** + * @var ObjectManager + */ + protected $objectManager; + + /** + * @var Invoice|MockObject + */ + protected $invoice; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + $this->model = $this->objectManager->getObject(Discount::class); + $this->order = $this->createPartialMock(Order::class, [ + 'getInvoiceCollection', + ]); + $this->invoice = $this->createPartialMock(Invoice::class, [ + 'getAllItems', + 'getOrder', + 'roundPrice', + 'isLast', + 'getGrandTotal', + 'getBaseGrandTotal', + 'setGrandTotal', + 'setBaseGrandTotal' + ]); + } + + /** + * Test for collect invoice + * + * @param array $invoiceData + * @dataProvider collectInvoiceData + * @return void + */ + public function testCollectInvoiceWithZeroGrandTotal(array $invoiceData): void + { + //Set up invoice mock + /** @var InvoiceItem[] $invoiceItems */ + $invoiceItems = []; + foreach ($invoiceData as $invoiceItemData) { + $invoiceItems[] = $this->getInvoiceItem($invoiceItemData); + } + $this->invoice->method('getOrder') + ->willReturn($this->order); + $this->order->method('getInvoiceCollection') + ->willReturn([]); + $this->invoice->method('getAllItems') + ->willReturn($invoiceItems); + $this->invoice->method('getGrandTotal') + ->willReturn(15.6801); + $this->invoice->method('getBaseGrandTotal') + ->willReturn(15.6801); + + $this->invoice->expects($this->exactly(1)) + ->method('setGrandTotal') + ->with(0); + $this->invoice->expects($this->exactly(1)) + ->method('setBaseGrandTotal') + ->with(0); + $this->model->collect($this->invoice); + } + + /** + * @return array + */ + public function collectInvoiceData(): array + { + return [ + [ + [ + [ + 'order_item' => [ + 'qty_ordered' => 1, + 'discount_amount' => 5.34, + 'base_discount_amount' => 5.34, + ], + 'is_last' => true, + 'qty' => 1, + ], + [ + 'order_item' => [ + 'qty_ordered' => 1, + 'discount_amount' => 10.34, + 'base_discount_amount' => 10.34, + ], + 'is_last' => true, + 'qty' => 1, + ], + ], + ], + ]; + } + + /** + * Get InvoiceItem + * + * @param $invoiceItemData array + * @return InvoiceItem|MockObject + */ + protected function getInvoiceItem($invoiceItemData) + { + /** @var OrderItem|MockObject $orderItem */ + $orderItem = $this->createPartialMock(OrderItem::class, [ + 'isDummy', + ]); + foreach ($invoiceItemData['order_item'] as $key => $value) { + $orderItem->setData($key, $value); + } + /** @var InvoiceItem|MockObject $invoiceItem */ + $invoiceItem = $this->createPartialMock(InvoiceItem::class, [ + 'getOrderItem', + 'isLast', + ]); + $invoiceItem->method('getOrderItem') + ->willReturn($orderItem); + $invoiceItem->method('isLast') + ->willReturn($invoiceItemData['is_last']); + $invoiceItem->getData('qty', $invoiceItemData['qty']); + return $invoiceItem; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php index 3b3a2f2816118..d5f0525ae9bbe 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Sales\Test\Unit\Model\Order\Payment; @@ -12,72 +13,90 @@ use Magento\Framework\Registry; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Payment\Helper\Data; -use Magento\Payment\Model\Method; use Magento\Payment\Model\Method\Substitution; use Magento\Payment\Model\MethodInterface; +use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\Order\Payment\Info; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Exception\LocalizedException; +/** + * Test for \Magento\Sales\Model\Order\Payment\Info. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class InfoTest extends TestCase { - /** @var \Magento\Sales\Model\Order\Payment\Info */ - protected $info; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var Context|MockObject */ - protected $contextMock; + /** + * @var Info + */ + private $info; - /** @var Registry|MockObject */ - protected $registryMock; + /** + * @var Data|MockObject + */ + private $paymentHelperMock; - /** @var Data|MockObject */ - protected $paymentHelperMock; + /** + * @var EncryptorInterface|MockObject + */ + private $encryptorInterfaceMock; - /** @var EncryptorInterface|MockObject */ - protected $encryptorInterfaceMock; + /** + * @var Data|MockObject + */ + private $methodInstanceMock; - /** @var Data|MockObject */ - protected $methodInstanceMock; + /** + * @var OrderInterface|MockObject + */ + private $orderMock; + /** + * @inheritdoc + */ protected function setUp(): void { - $this->contextMock = $this->createMock(Context::class); - $this->registryMock = $this->createMock(Registry::class); + $contextMock = $this->createMock(Context::class); + $registryMock = $this->createMock(Registry::class); $this->paymentHelperMock = $this->createPartialMock(Data::class, ['getMethodInstance']); $this->encryptorInterfaceMock = $this->getMockForAbstractClass(EncryptorInterface::class); - $this->methodInstanceMock = $this->getMockBuilder(MethodInterface::class) - ->getMockForAbstractClass(); + $this->methodInstanceMock = $this->getMockForAbstractClass(MethodInterface::class); + $this->orderMock = $this->createMock(OrderInterface::class); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->info = $this->objectManagerHelper->getObject( + $objectManagerHelper = new ObjectManagerHelper($this); + $this->info = $objectManagerHelper->getObject( Info::class, [ - 'context' => $this->contextMock, - 'registry' => $this->registryMock, + 'context' => $contextMock, + 'registry' => $registryMock, 'paymentData' => $this->paymentHelperMock, 'encryptor' => $this->encryptorInterfaceMock ] ); + $this->info->setData('order', $this->orderMock); } /** + * Get data cc number + * * @dataProvider ccKeysDataProvider * @param string $keyCc * @param string $keyCcEnc + * @return void */ - public function testGetDataCcNumber($keyCc, $keyCcEnc) + public function testGetDataCcNumber($keyCc, $keyCcEnc): void { // no data was set $this->assertNull($this->info->getData($keyCc)); // we set encrypted data $this->info->setData($keyCcEnc, $keyCcEnc); - $this->encryptorInterfaceMock->expects($this->once())->method('decrypt')->with($keyCcEnc)->willReturn( - $keyCc - ); + $this->encryptorInterfaceMock->expects($this->once()) + ->method('decrypt') + ->with($keyCcEnc) + ->willReturn($keyCc); + $this->assertEquals($keyCc, $this->info->getData($keyCc)); } @@ -86,7 +105,7 @@ public function testGetDataCcNumber($keyCc, $keyCcEnc) * * @return array */ - public function ccKeysDataProvider() + public function ccKeysDataProvider(): array { return [ ['cc_number', 'cc_number_enc'], @@ -94,14 +113,26 @@ public function ccKeysDataProvider() ]; } - public function testGetMethodInstanceWithRealMethod() + /** + * Get method instance with real method + * + * @return void + */ + public function testGetMethodInstanceWithRealMethod(): void { + $storeId = 2; $method = 'real_method'; $this->info->setData('method', $method); + $this->orderMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); $this->methodInstanceMock->expects($this->once()) ->method('setInfoInstance') ->with($this->info); + $this->methodInstanceMock->expects($this->once()) + ->method('setStore') + ->with($storeId); $this->paymentHelperMock->expects($this->once()) ->method('getMethodInstance') @@ -111,7 +142,12 @@ public function testGetMethodInstanceWithRealMethod() $this->info->getMethodInstance(); } - public function testGetMethodInstanceWithUnrealMethod() + /** + * Get method instance with unreal method + * + * @return void + */ + public function testGetMethodInstanceWithUnrealMethod(): void { $method = 'unreal_method'; $this->info->setData('method', $method); @@ -133,15 +169,26 @@ public function testGetMethodInstanceWithUnrealMethod() $this->info->getMethodInstance(); } - public function testGetMethodInstanceWithNoMethod() + /** + * Get method instance withot method + * + * @return void + */ + public function testGetMethodInstanceWithNoMethod(): void { - $this->expectException('Magento\Framework\Exception\LocalizedException'); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('The payment method you requested is not available.'); + $this->info->setData('method', false); $this->info->getMethodInstance(); } - public function testGetMethodInstanceRequestedMethod() + /** + * Get method instance requested method + * + * @return void + */ + public function testGetMethodInstanceRequestedMethod(): void { $code = 'real_method'; $this->info->setData('method', $code); @@ -160,40 +207,62 @@ public function testGetMethodInstanceRequestedMethod() $this->assertSame($this->methodInstanceMock, $this->info->getMethodInstance()); } - public function testEncrypt() + /** + * Encrypt test + * + * @return void + */ + public function testEncrypt(): void { $data = 'data'; $encryptedData = 'd1a2t3a4'; - $this->encryptorInterfaceMock->expects($this->once())->method('encrypt')->with($data)->willReturn( - $encryptedData - ); + $this->encryptorInterfaceMock->expects($this->once()) + ->method('encrypt') + ->with($data) + ->willReturn($encryptedData); + $this->assertEquals($encryptedData, $this->info->encrypt($data)); } - public function testDecrypt() + /** + * Decrypt test + * + * @return void + */ + public function testDecrypt(): void { $data = 'data'; $encryptedData = 'd1a2t3a4'; - $this->encryptorInterfaceMock->expects($this->once())->method('decrypt')->with($encryptedData)->willReturn( - $data - ); + $this->encryptorInterfaceMock->expects($this->once()) + ->method('decrypt') + ->with($encryptedData) + ->willReturn($data); + $this->assertEquals($data, $this->info->decrypt($encryptedData)); } - public function testSetAdditionalInformationException() + /** + * Set additional information exception + * + * @return void + */ + public function testSetAdditionalInformationException(): void { - $this->expectException('Magento\Framework\Exception\LocalizedException'); + $this->expectException(LocalizedException::class); $this->info->setAdditionalInformation('object', new \stdClass()); } /** + * Set additional info multiple types + * * @dataProvider additionalInformationDataProvider * @param mixed $key * @param mixed $value + * @return void */ - public function testSetAdditionalInformationMultipleTypes($key, $value = null) + public function testSetAdditionalInformationMultipleTypes($key, $value = null): void { $this->info->setAdditionalInformation($key, $value); $this->assertEquals($value ? [$key => $value] : $key, $this->info->getAdditionalInformation()); @@ -204,7 +273,7 @@ public function testSetAdditionalInformationMultipleTypes($key, $value = null) * * @return array */ - public function additionalInformationDataProvider() + public function additionalInformationDataProvider(): array { return [ [['key1' => 'data1', 'key2' => 'data2'], null], @@ -212,7 +281,12 @@ public function additionalInformationDataProvider() ]; } - public function testGetAdditionalInformationByKey() + /** + * Get additional info by key + * + * @return void + */ + public function testGetAdditionalInformationByKey(): void { $key = 'key'; $value = 'value'; @@ -220,7 +294,12 @@ public function testGetAdditionalInformationByKey() $this->assertEquals($value, $this->info->getAdditionalInformation($key)); } - public function testUnsAdditionalInformation() + /** + * Unsetter additional info + * + * @return void + */ + public function testUnsAdditionalInformation(): void { // set array to additional $data = ['key1' => 'data1', 'key2' => 'data2']; @@ -236,7 +315,12 @@ public function testUnsAdditionalInformation() $this->assertEmpty($this->info->unsAdditionalInformation()->getAdditionalInformation()); } - public function testHasAdditionalInformation() + /** + * Has additional info + * + * @return void + */ + public function testHasAdditionalInformation(): void { $this->assertFalse($this->info->hasAdditionalInformation()); @@ -248,7 +332,12 @@ public function testHasAdditionalInformation() $this->assertTrue($this->info->hasAdditionalInformation()); } - public function testInitAdditionalInformationWithUnserialize() + /** + * Init additional info with unserialize + * + * @return void + */ + public function testInitAdditionalInformationWithUnserialize(): void { $data = ['key1' => 'data1', 'key2' => 'data2']; $this->info->setData('additional_information', $data); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php index a64ad8c53bcf8..91a269300a7fc 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php @@ -637,7 +637,7 @@ public function testAcceptApprovePaymentTrue() ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -690,7 +690,7 @@ public function testAcceptApprovePaymentFalse($isFraudDetected, $status) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -730,7 +730,7 @@ public function testAcceptApprovePaymentFalseOrderState($isFraudDetected) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -756,7 +756,7 @@ public function testDenyPaymentFalse() ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -812,7 +812,7 @@ public function testDenyPaymentNegative($isFraudDetected, $status) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -846,7 +846,7 @@ public function testDenyPaymentNegativeStateReview() ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -925,13 +925,13 @@ public function testUpdateOnlineTransactionApproved() $this->invoice->setBaseGrandTotal($baseGrandTotal); $this->mockResultTrueMethods($this->transactionId, $baseGrandTotal, $message); - $this->order->expects($this->once()) + $this->order->expects($this->exactly(2)) ->method('getStoreId') ->willReturn($storeId); $this->helper->expects($this->once()) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore') ->with($storeId) ->willReturn($this->paymentMethod); @@ -985,13 +985,13 @@ public function testUpdateOnlineTransactionDenied() $this->mockInvoice($this->transactionId); $this->mockResultFalseMethods($message); - $this->order->expects($this->once()) + $this->order->expects($this->exactly(2)) ->method('getStoreId') ->willReturn($storeId); $this->helper->expects($this->once()) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore') ->with($storeId) ->willReturn($this->paymentMethod); @@ -1027,13 +1027,13 @@ public function testUpdateOnlineTransactionDeniedFalse($isFraudDetected, $status $this->assertOrderUpdated(Order::STATE_PAYMENT_REVIEW, $status, $message); - $this->order->expects($this->once()) + $this->order->expects($this->exactly(2)) ->method('getStoreId') ->willReturn($storeId); $this->helper->expects($this->once()) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore') ->with($storeId) ->willReturn($this->paymentMethod); @@ -1069,13 +1069,13 @@ public function testUpdateOnlineTransactionDeniedFalseHistoryComment() ->method('addStatusHistoryComment') ->with($message); - $this->order->expects($this->once()) + $this->order->expects($this->exactly(2)) ->method('getStoreId') ->willReturn($storeId); $this->helper->expects($this->once()) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore') ->with($storeId) ->willReturn($this->paymentMethod); @@ -1144,7 +1144,7 @@ public function testAcceptWithoutInvoiceResultTrue() ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -1178,7 +1178,7 @@ public function testDenyWithoutInvoiceResultFalse() ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/CreditmemoTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/CreditmemoTest.php new file mode 100644 index 0000000000000..2c62f0fb8122f --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/CreditmemoTest.php @@ -0,0 +1,202 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Pdf; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Address\Renderer; +use Magento\Sales\Model\Order\Creditmemo; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class CreditmemoTest + * + * Tests Sales Order Creditmemo PDF model + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CreditmemoTest extends TestCase +{ + /** + * @var \Magento\Sales\Model\Order\Pdf\Invoice + */ + protected $_model; + + /** + * @var \Magento\Sales\Model\Order\Pdf\Config|MockObject + */ + protected $_pdfConfigMock; + + /** + * @var Database|MockObject + */ + protected $databaseMock; + + /** + * @var ScopeConfigInterface|MockObject + */ + protected $scopeConfigMock; + + /** + * @var \Magento\Framework\Filesystem\Directory\Write|MockObject + */ + protected $directoryMock; + + /** + * @var Renderer|MockObject + */ + protected $addressRendererMock; + + /** + * @var \Magento\Payment\Helper\Data|MockObject + */ + protected $paymentDataMock; + + /** + * @var \Magento\Store\Model\App\Emulation + */ + private $appEmulation; + + protected function setUp(): void + { + $this->_pdfConfigMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Pdf\Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->directoryMock = $this->createMock(\Magento\Framework\Filesystem\Directory\Write::class); + $this->directoryMock->expects($this->any())->method('getAbsolutePath')->will( + $this->returnCallback( + function ($argument) { + return BP . '/' . $argument; + } + ) + ); + $filesystemMock = $this->createMock(\Magento\Framework\Filesystem::class); + $filesystemMock->expects($this->any()) + ->method('getDirectoryRead') + ->will($this->returnValue($this->directoryMock)); + $filesystemMock->expects($this->any()) + ->method('getDirectoryWrite') + ->will($this->returnValue($this->directoryMock)); + + $this->databaseMock = $this->createMock(Database::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->addressRendererMock = $this->createMock(Renderer::class); + $this->paymentDataMock = $this->createMock(\Magento\Payment\Helper\Data::class); + $this->appEmulation = $this->createMock(\Magento\Store\Model\App\Emulation::class); + + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->_model = $helper->getObject( + \Magento\Sales\Model\Order\Pdf\Creditmemo::class, + [ + 'filesystem' => $filesystemMock, + 'pdfConfig' => $this->_pdfConfigMock, + 'fileStorageDatabase' => $this->databaseMock, + 'scopeConfig' => $this->scopeConfigMock, + 'addressRenderer' => $this->addressRendererMock, + 'string' => new \Magento\Framework\Stdlib\StringUtils(), + 'paymentData' => $this->paymentDataMock, + 'appEmulation' => $this->appEmulation + ] + ); + } + + public function testInsertLogoDatabaseMediaStorage() + { + $filename = 'image.jpg'; + $path = '/sales/store/logo/'; + $storeId = 1; + + $this->appEmulation->expects($this->once()) + ->method('startEnvironmentEmulation') + ->with( + $storeId, + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ) + ->willReturnSelf(); + $this->appEmulation->expects($this->once()) + ->method('stopEnvironmentEmulation') + ->willReturnSelf(); + $this->_pdfConfigMock->expects($this->once()) + ->method('getRenderersPerProduct') + ->with('creditmemo') + ->will($this->returnValue(['product_type_one' => 'Renderer_Type_One_Product_One'])); + $this->_pdfConfigMock->expects($this->any()) + ->method('getTotals') + ->will($this->returnValue([])); + + $block = $this->getMockBuilder(\Magento\Framework\View\Element\Template::class) + ->disableOriginalConstructor() + ->setMethods(['setIsSecureMode','toPdf']) + ->getMock(); + $block->expects($this->any()) + ->method('setIsSecureMode') + ->willReturn($block); + $block->expects($this->any()) + ->method('toPdf') + ->will($this->returnValue('')); + $this->paymentDataMock->expects($this->any()) + ->method('getInfoBlock') + ->willReturn($block); + + $this->addressRendererMock->expects($this->any()) + ->method('format') + ->will($this->returnValue('')); + + $this->databaseMock->expects($this->any()) + ->method('checkDbUsage') + ->will($this->returnValue(true)); + + $creditmemoMock = $this->createMock(Creditmemo::class); + $orderMock = $this->createMock(Order::class); + $addressMock = $this->createMock(Address::class); + $orderMock->expects($this->any()) + ->method('getBillingAddress') + ->willReturn($addressMock); + $orderMock->expects($this->any()) + ->method('getIsVirtual') + ->will($this->returnValue(true)); + $infoMock = $this->createMock(\Magento\Payment\Model\InfoInterface::class); + $orderMock->expects($this->any()) + ->method('getPayment') + ->willReturn($infoMock); + $creditmemoMock->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); + $creditmemoMock->expects($this->any()) + ->method('getOrder') + ->willReturn($orderMock); + $creditmemoMock->expects($this->any()) + ->method('getAllItems') + ->willReturn([]); + + $this->scopeConfigMock->expects($this->at(0)) + ->method('getValue') + ->with('sales/identity/logo', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null) + ->will($this->returnValue($filename)); + $this->scopeConfigMock->expects($this->at(1)) + ->method('getValue') + ->with('sales/identity/address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null) + ->will($this->returnValue('')); + + $this->directoryMock->expects($this->any()) + ->method('isFile') + ->with($path . $filename) + ->willReturnOnConsecutiveCalls( + $this->returnValue(false), + $this->returnValue(false) + ); + + $this->databaseMock->expects($this->once()) + ->method('saveFileToFilesystem') + ->with($path . $filename); + + $this->_model->getPdf([$creditmemoMock]); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/InvoiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/InvoiceTest.php index 9254abee50175..b628ffc48f81a 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/InvoiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/InvoiceTest.php @@ -21,11 +21,14 @@ use Magento\Sales\Model\Order\Address\Renderer; use Magento\Sales\Model\Order\Invoice; use Magento\Sales\Model\Order\Pdf\Config; +use Magento\Store\Model\App\Emulation; use Magento\Store\Model\ScopeInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** + * + * Tests Sales Order Invoice PDF model * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -66,6 +69,11 @@ class InvoiceTest extends TestCase */ protected $paymentDataMock; + /** + * @var Emulation + */ + private $appEmulation; + protected function setUp(): void { $this->_pdfConfigMock = $this->getMockBuilder(Config::class) @@ -89,6 +97,7 @@ function ($argument) { $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->addressRendererMock = $this->createMock(Renderer::class); $this->paymentDataMock = $this->createMock(Data::class); + $this->appEmulation = $this->createMock(Emulation::class); $helper = new ObjectManager($this); $this->_model = $helper->getObject( @@ -100,7 +109,8 @@ function ($argument) { 'scopeConfig' => $this->scopeConfigMock, 'addressRenderer' => $this->addressRendererMock, 'string' => new StringUtils(), - 'paymentData' => $this->paymentDataMock + 'paymentData' => $this->paymentDataMock, + 'appEmulation' => $this->appEmulation ] ); } @@ -136,7 +146,19 @@ public function testInsertLogoDatabaseMediaStorage() { $filename = 'image.jpg'; $path = '/sales/store/logo/'; - + $storeId = 1; + + $this->appEmulation->expects($this->once()) + ->method('startEnvironmentEmulation') + ->with( + $storeId, + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ) + ->willReturnSelf(); + $this->appEmulation->expects($this->once()) + ->method('stopEnvironmentEmulation') + ->willReturnSelf(); $this->_pdfConfigMock->expects($this->once()) ->method('getRenderersPerProduct') ->with('invoice') @@ -180,6 +202,9 @@ public function testInsertLogoDatabaseMediaStorage() $orderMock->expects($this->any()) ->method('getPayment') ->willReturn($infoMock); + $invoiceMock->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); $invoiceMock->expects($this->any()) ->method('getOrder') ->willReturn($orderMock); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/ShipmentTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/ShipmentTest.php new file mode 100644 index 0000000000000..19c61e4d37bdb --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/ShipmentTest.php @@ -0,0 +1,202 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Pdf; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Address\Renderer; +use Magento\Sales\Model\Order\Shipment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class ShipmentTest + * + * Tests Sales Order Shipment PDF model + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ShipmentTest extends TestCase +{ + /** + * @var \Magento\Sales\Model\Order\Pdf\Invoice + */ + protected $_model; + + /** + * @var \Magento\Sales\Model\Order\Pdf\Config|MockObject + */ + protected $_pdfConfigMock; + + /** + * @var Database|MockObject + */ + protected $databaseMock; + + /** + * @var ScopeConfigInterface|MockObject + */ + protected $scopeConfigMock; + + /** + * @var \Magento\Framework\Filesystem\Directory\Write|MockObject + */ + protected $directoryMock; + + /** + * @var Renderer|MockObject + */ + protected $addressRendererMock; + + /** + * @var \Magento\Payment\Helper\Data|MockObject + */ + protected $paymentDataMock; + + /** + * @var \Magento\Store\Model\App\Emulation + */ + private $appEmulation; + + protected function setUp(): void + { + $this->_pdfConfigMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Pdf\Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->directoryMock = $this->createMock(\Magento\Framework\Filesystem\Directory\Write::class); + $this->directoryMock->expects($this->any())->method('getAbsolutePath')->will( + $this->returnCallback( + function ($argument) { + return BP . '/' . $argument; + } + ) + ); + $filesystemMock = $this->createMock(\Magento\Framework\Filesystem::class); + $filesystemMock->expects($this->any()) + ->method('getDirectoryRead') + ->will($this->returnValue($this->directoryMock)); + $filesystemMock->expects($this->any()) + ->method('getDirectoryWrite') + ->will($this->returnValue($this->directoryMock)); + + $this->databaseMock = $this->createMock(Database::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->addressRendererMock = $this->createMock(Renderer::class); + $this->paymentDataMock = $this->createMock(\Magento\Payment\Helper\Data::class); + $this->appEmulation = $this->createMock(\Magento\Store\Model\App\Emulation::class); + + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->_model = $helper->getObject( + \Magento\Sales\Model\Order\Pdf\Shipment::class, + [ + 'filesystem' => $filesystemMock, + 'pdfConfig' => $this->_pdfConfigMock, + 'fileStorageDatabase' => $this->databaseMock, + 'scopeConfig' => $this->scopeConfigMock, + 'addressRenderer' => $this->addressRendererMock, + 'string' => new \Magento\Framework\Stdlib\StringUtils(), + 'paymentData' => $this->paymentDataMock, + 'appEmulation' => $this->appEmulation + ] + ); + } + + public function testInsertLogoDatabaseMediaStorage() + { + $filename = 'image.jpg'; + $path = '/sales/store/logo/'; + $storeId = 1; + + $this->appEmulation->expects($this->once()) + ->method('startEnvironmentEmulation') + ->with( + $storeId, + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ) + ->willReturnSelf(); + $this->appEmulation->expects($this->once()) + ->method('stopEnvironmentEmulation') + ->willReturnSelf(); + $this->_pdfConfigMock->expects($this->once()) + ->method('getRenderersPerProduct') + ->with('shipment') + ->will($this->returnValue(['product_type_one' => 'Renderer_Type_One_Product_One'])); + $this->_pdfConfigMock->expects($this->any()) + ->method('getTotals') + ->will($this->returnValue([])); + + $block = $this->getMockBuilder(\Magento\Framework\View\Element\Template::class) + ->disableOriginalConstructor() + ->setMethods(['setIsSecureMode','toPdf']) + ->getMock(); + $block->expects($this->any()) + ->method('setIsSecureMode') + ->willReturn($block); + $block->expects($this->any()) + ->method('toPdf') + ->will($this->returnValue('')); + $this->paymentDataMock->expects($this->any()) + ->method('getInfoBlock') + ->willReturn($block); + + $this->addressRendererMock->expects($this->any()) + ->method('format') + ->will($this->returnValue('')); + + $this->databaseMock->expects($this->any()) + ->method('checkDbUsage') + ->will($this->returnValue(true)); + + $shipmentMock = $this->createMock(Shipment::class); + $orderMock = $this->createMock(Order::class); + $addressMock = $this->createMock(Address::class); + $orderMock->expects($this->any()) + ->method('getBillingAddress') + ->willReturn($addressMock); + $orderMock->expects($this->any()) + ->method('getIsVirtual') + ->will($this->returnValue(true)); + $infoMock = $this->createMock(\Magento\Payment\Model\InfoInterface::class); + $orderMock->expects($this->any()) + ->method('getPayment') + ->willReturn($infoMock); + $shipmentMock->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); + $shipmentMock->expects($this->any()) + ->method('getOrder') + ->willReturn($orderMock); + $shipmentMock->expects($this->any()) + ->method('getAllItems') + ->willReturn([]); + + $this->scopeConfigMock->expects($this->at(0)) + ->method('getValue') + ->with('sales/identity/logo', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null) + ->will($this->returnValue($filename)); + $this->scopeConfigMock->expects($this->at(1)) + ->method('getValue') + ->with('sales/identity/address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null) + ->will($this->returnValue('')); + + $this->directoryMock->expects($this->any()) + ->method('isFile') + ->with($path . $filename) + ->willReturnOnConsecutiveCalls( + $this->returnValue(false), + $this->returnValue(false) + ); + + $this->databaseMock->expects($this->once()) + ->method('saveFileToFilesystem') + ->with($path . $filename); + + $this->_model->getPdf([$shipmentMock]); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php index 84c66d12c10d8..2e51bd8d75b89 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php @@ -18,12 +18,13 @@ use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderPaymentInterface; use Magento\Sales\Api\Data\OrderSearchResultInterfaceFactory as SearchResultFactory; +use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Shipping; use Magento\Sales\Model\Order\ShippingAssignment; use Magento\Sales\Model\Order\ShippingAssignmentBuilder; use Magento\Sales\Model\OrderRepository; use Magento\Sales\Model\ResourceModel\Metadata; -use Magento\Sales\Model\ResourceModel\Order; +use Magento\Sales\Model\ResourceModel\Order as OrderResource; use Magento\Sales\Model\ResourceModel\Order\Collection; use Magento\Tax\Api\Data\OrderTaxDetailsInterface; use Magento\Tax\Api\OrderTaxManagementInterface; @@ -70,6 +71,11 @@ class OrderRepositoryTest extends TestCase */ private $paymentAdditionalInfoFactory; + /** + * @var OrderExtensionFactory|\MockObject + */ + private $orderExtensionFactoryMock; + /** * Setup the test * @@ -88,7 +94,7 @@ protected function setUp(): void $this->collectionProcessor = $this->createMock( CollectionProcessorInterface::class ); - $orderExtensionFactoryMock = $this->getMockBuilder(OrderExtensionFactory::class) + $this->orderExtensionFactoryMock = $this->getMockBuilder(OrderExtensionFactory::class) ->disableOriginalConstructor() ->getMock(); $this->orderTaxManagementMock = $this->getMockBuilder(OrderTaxManagementInterface::class) @@ -103,7 +109,7 @@ protected function setUp(): void 'metadata' => $this->metadata, 'searchResultFactory' => $this->searchResultFactory, 'collectionProcessor' => $this->collectionProcessor, - 'orderExtensionFactory' => $orderExtensionFactoryMock, + 'orderExtensionFactory' => $this->orderExtensionFactoryMock, 'orderTaxManagement' => $this->orderTaxManagementMock, 'paymentAdditionalInfoFactory' => $this->paymentAdditionalInfoFactory ] @@ -178,10 +184,10 @@ public function testGetList() */ public function testSave() { - $mapperMock = $this->getMockBuilder(Order::class) + $mapperMock = $this->getMockBuilder(OrderResource::class) ->disableOriginalConstructor() ->getMock(); - $orderEntity = $this->createMock(\Magento\Sales\Model\Order::class); + $orderEntity = $this->createMock(Order::class); $extensionAttributes = $this->getMockBuilder(OrderExtension::class) ->addMethods(['getShippingAssignments']) ->getMock(); @@ -207,4 +213,57 @@ public function testSave() $orderEntity->expects($this->any())->method('getEntityId')->willReturn(1); $this->orderRepository->save($orderEntity); } + + /** + * Test for method get. + * + * @return void + */ + public function testGet() + { + $orderId = 1; + $appliedTaxes = 'applied_taxes'; + $items = 'items'; + $paymentInfo = []; + + $orderEntity = $this->createMock(Order::class); + $paymentMock = $this->getMockBuilder(OrderPaymentInterface::class) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $paymentMock->expects($this->once())->method('getAdditionalInformation')->willReturn($paymentInfo); + $orderExtension = $this->getMockBuilder(OrderExtension::class) + ->setMethods( + [ + 'getShippingAssignments', + 'setAppliedTaxes', + 'setConvertingFromQuote', + 'setItemAppliedTaxes', + 'setPaymentAdditionalInfo' + ] + ) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $orderExtension->expects($this->once())->method('getShippingAssignments')->willReturn(true); + $orderExtension->expects($this->once())->method('setAppliedTaxes')->with($appliedTaxes); + $orderExtension->expects($this->once())->method('setConvertingFromQuote')->with(true); + $orderExtension->expects($this->once())->method('setItemAppliedTaxes')->with($items); + $orderExtension->expects($this->once())->method('setPaymentAdditionalInfo')->with($paymentInfo); + $this->orderExtensionFactoryMock->expects($this->once())->method('create')->willReturn($orderExtension); + $orderEntity->expects($this->once())->method('load')->with($orderId)->willReturn($orderEntity); + $orderEntity->expects($this->exactly(2))->method('getEntityId')->willReturn($orderId); + $orderEntity->expects($this->once())->method('getPayment')->willReturn($paymentMock); + $orderEntity->expects($this->exactly(2))->method('setExtensionAttributes')->with($orderExtension); + $orderEntity->expects($this->exactly(3)) + ->method('getExtensionAttributes') + ->willReturnOnConsecutiveCalls(null, $orderExtension, $orderExtension); + $this->metadata->expects($this->once())->method('getNewInstance')->willReturn($orderEntity); + $orderTaxDetailsMock = $this->getMockBuilder(OrderTaxDetailsInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setAppliedTaxes'])->getMockForAbstractClass(); + $orderTaxDetailsMock->expects($this->once())->method('getAppliedTaxes')->willReturn($appliedTaxes); + $orderTaxDetailsMock->expects($this->once())->method('getItems')->willReturn($items); + $this->orderTaxManagementMock->expects($this->atLeastOnce())->method('getOrderTaxDetails') + ->willReturn($orderTaxDetailsMock); + + $this->orderRepository->get($orderId); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/AddressTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/AddressTest.php index 5267686a447cc..0978dda09f7a7 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/AddressTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/AddressTest.php @@ -133,6 +133,66 @@ public function testProcessShippingAddress() $this->assertEquals($this->address, $this->address->process($this->orderMock)); } + /** + * Test processing of the shipping address when shipping address id was not changed. + * setShippingAddressId and saveAttribute methods must not be executed. + */ + public function testProcessShippingAddressNotChanged() + { + $this->orderMock->expects($this->exactly(2)) + ->method('getAddresses') + ->willReturn([$this->addressMock]); + $this->addressMock->expects($this->once()) + ->method('save')->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getBillingAddress') + ->willReturn(null); + $this->orderMock->expects($this->once()) + ->method('getShippingAddress') + ->willReturn($this->addressMock); + $this->addressMock->expects($this->once()) + ->method('getId')->willReturn(1); + $this->orderMock->expects($this->once()) + ->method('getShippingAddressId') + ->willReturn(1); + $this->orderMock->expects($this->never()) + ->method('setShippingAddressId')->willReturnSelf(); + $this->attributeMock->expects($this->never()) + ->method('saveAttribute') + ->with($this->orderMock, ['shipping_address_id'])->willReturnSelf(); + $this->assertEquals($this->address, $this->address->process($this->orderMock)); + } + + /** + * Test processing of the billing address when billing address id was not changed. + * setBillingAddressId and saveAttribute methods must not be executed. + */ + public function testProcessBillingAddressNotChanged() + { + $this->orderMock->expects($this->exactly(2)) + ->method('getAddresses') + ->willReturn([$this->addressMock]); + $this->addressMock->expects($this->once()) + ->method('save')->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getBillingAddress') + ->willReturn($this->addressMock); + $this->orderMock->expects($this->once()) + ->method('getShippingAddress') + ->willReturn(null); + $this->addressMock->expects($this->once()) + ->method('getId')->willReturn(1); + $this->orderMock->expects($this->once()) + ->method('getBillingAddressId') + ->willReturn(1); + $this->orderMock->expects($this->never()) + ->method('setBillingAddressId')->willReturnSelf(); + $this->attributeMock->expects($this->never()) + ->method('saveAttribute') + ->with($this->orderMock, ['billing_address_id'])->willReturnSelf(); + $this->assertEquals($this->address, $this->address->process($this->orderMock)); + } + /** * Test method removeEmptyAddresses */ diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php index ea655bb32f05f..a48f0702f5a4b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php @@ -41,7 +41,8 @@ protected function setUp(): void 'getBaseGrandTotal', 'canCreditmemo', 'getTotalRefunded', - 'getConfig' + 'getConfig', + 'getIsNotVirtual' ] ) ->disableOriginalConstructor() @@ -49,24 +50,21 @@ protected function setUp(): void $this->orderMock->expects($this->any()) ->method('getConfig') ->willReturnSelf(); - $this->addressMock = $this->createMock(Address::class); - $this->addressCollectionMock = $this->createMock( - Collection::class - ); $this->state = new State(); } /** - * @param bool $isCanceled - * @param bool $canUnhold - * @param bool $canInvoice - * @param bool $canShip - * @param int $callCanSkipNum * @param bool $canCreditmemo * @param int $callCanCreditmemoNum + * @param bool $canShip + * @param int $callCanSkipNum * @param string $currentState * @param string $expectedState - * @param int $callSetStateNum + * @param bool $isInProcess + * @param int $callGetIsInProcessNum + * @param bool $isCanceled + * @param bool $canUnhold + * @param bool $isNotVirtual * @dataProvider stateCheckDataProvider * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -76,12 +74,12 @@ public function testCheck( bool $canShip, int $callCanSkipNum, string $currentState, - string $expectedState = '', - bool $isInProcess = false, - int $callGetIsInProcessNum = 0, - bool $isCanceled = false, - bool $canUnhold = false, - bool $canInvoice = false + string $expectedState, + bool $isInProcess, + int $callGetIsInProcessNum, + bool $isCanceled, + bool $canUnhold, + bool $isNotVirtual ) { $this->orderMock->setState($currentState); $this->orderMock->expects($this->any()) @@ -92,7 +90,7 @@ public function testCheck( ->willReturn($canUnhold); $this->orderMock->expects($this->any()) ->method('canInvoice') - ->willReturn($canInvoice); + ->willReturn(false); $this->orderMock->expects($this->exactly($callCanSkipNum)) ->method('canShip') ->willReturn($canShip); @@ -102,11 +100,16 @@ public function testCheck( $this->orderMock->expects($this->exactly($callGetIsInProcessNum)) ->method('getIsInProcess') ->willReturn($isInProcess); + $this->orderMock->method('getIsNotVirtual') + ->willReturn($isNotVirtual); $this->state->check($this->orderMock); $this->assertEquals($expectedState, $this->orderMock->getState()); } /** + * Data provider for testCheck + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @return array */ public function stateCheckDataProvider() @@ -118,7 +121,12 @@ public function stateCheckDataProvider() 'can_ship' => false, 'call_can_skip_num' => 1, 'current_state' => Order::STATE_PROCESSING, - 'expected_state' => Order::STATE_CLOSED + 'expected_state' => Order::STATE_CLOSED, + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'complete - !canCreditmemo,!canShip -> closed' => [ 'can_credit_memo' => false, @@ -126,7 +134,12 @@ public function stateCheckDataProvider() 'can_ship' => false, 'call_can_skip_num' => 1, 'current_state' => Order::STATE_COMPLETE, - 'expected_state' => Order::STATE_CLOSED + 'expected_state' => Order::STATE_CLOSED, + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'processing - !canCreditmemo,canShip -> processing' => [ 'can_credit_memo' => false, @@ -134,7 +147,12 @@ public function stateCheckDataProvider() 'can_ship' => true, 'call_can_skip_num' => 2, 'current_state' => Order::STATE_PROCESSING, - 'expected_state' => Order::STATE_PROCESSING + 'expected_state' => Order::STATE_PROCESSING, + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'complete - !canCreditmemo,canShip -> complete' => [ 'can_credit_memo' => false, @@ -142,7 +160,12 @@ public function stateCheckDataProvider() 'can_ship' => true, 'call_can_skip_num' => 1, 'current_state' => Order::STATE_COMPLETE, - 'expected_state' => Order::STATE_COMPLETE + 'expected_state' => Order::STATE_COMPLETE, + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'processing - canCreditmemo,!canShip -> complete' => [ 'can_credit_memo' => true, @@ -150,7 +173,12 @@ public function stateCheckDataProvider() 'can_ship' => false, 'call_can_skip_num' => 1, 'current_state' => Order::STATE_PROCESSING, - 'expected_state' => Order::STATE_COMPLETE + 'expected_state' => Order::STATE_COMPLETE, + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'complete - canCreditmemo,!canShip -> complete' => [ 'can_credit_memo' => true, @@ -158,7 +186,12 @@ public function stateCheckDataProvider() 'can_ship' => false, 'call_can_skip_num' => 0, 'current_state' => Order::STATE_COMPLETE, - 'expected_state' => Order::STATE_COMPLETE + 'expected_state' => Order::STATE_COMPLETE, + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'processing - canCreditmemo, canShip -> processing' => [ 'can_credit_memo' => true, @@ -166,7 +199,12 @@ public function stateCheckDataProvider() 'can_ship' => true, 'call_can_skip_num' => 1, 'current_state' => Order::STATE_PROCESSING, - 'expected_state' => Order::STATE_PROCESSING + 'expected_state' => Order::STATE_PROCESSING, + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'complete - canCreditmemo, canShip -> complete' => [ 'can_credit_memo' => true, @@ -174,7 +212,12 @@ public function stateCheckDataProvider() 'can_ship' => true, 'call_can_skip_num' => 0, 'current_state' => Order::STATE_COMPLETE, - 'expected_state' => Order::STATE_COMPLETE + 'expected_state' => Order::STATE_COMPLETE, + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'new - canCreditmemo, canShip, IsInProcess -> processing' => [ 'can_credit_memo' => true, @@ -183,8 +226,11 @@ public function stateCheckDataProvider() 'call_can_skip_num' => 1, 'current_state' => Order::STATE_NEW, 'expected_state' => Order::STATE_PROCESSING, - true, - 1 + 'is_in_process' => true, + 'get_is_in_process_invoke_count' => 1, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'new - canCreditmemo, !canShip, IsInProcess -> processing' => [ 'can_credit_memo' => true, @@ -193,8 +239,11 @@ public function stateCheckDataProvider() 'call_can_skip_num' => 1, 'current_state' => Order::STATE_NEW, 'expected_state' => Order::STATE_COMPLETE, - true, - 1 + 'is_in_process' => true, + 'get_is_in_process_invoke_count' => 1, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'new - canCreditmemo, canShip, !IsInProcess -> new' => [ 'can_credit_memo' => true, @@ -203,8 +252,11 @@ public function stateCheckDataProvider() 'call_can_skip_num' => 0, 'current_state' => Order::STATE_NEW, 'expected_state' => Order::STATE_NEW, - false, - 1 + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 1, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => true ], 'hold - canUnhold -> hold' => [ 'can_credit_memo' => true, @@ -213,10 +265,11 @@ public function stateCheckDataProvider() 'call_can_skip_num' => 0, 'current_state' => Order::STATE_HOLDED, 'expected_state' => Order::STATE_HOLDED, - false, - 0, - false, - true + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => true, + 'is_not_virtual' => true ], 'payment_review - canUnhold -> payment_review' => [ 'can_credit_memo' => true, @@ -225,10 +278,11 @@ public function stateCheckDataProvider() 'call_can_skip_num' => 0, 'current_state' => Order::STATE_PAYMENT_REVIEW, 'expected_state' => Order::STATE_PAYMENT_REVIEW, - false, - 0, - false, - true + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => true, + 'is_not_virtual' => true ], 'pending_payment - canUnhold -> pending_payment' => [ 'can_credit_memo' => true, @@ -237,10 +291,11 @@ public function stateCheckDataProvider() 'call_can_skip_num' => 0, 'current_state' => Order::STATE_PENDING_PAYMENT, 'expected_state' => Order::STATE_PENDING_PAYMENT, - false, - 0, - false, - true + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => true, + 'is_not_virtual' => true ], 'cancelled - isCanceled -> cancelled' => [ 'can_credit_memo' => true, @@ -249,9 +304,24 @@ public function stateCheckDataProvider() 'call_can_skip_num' => 0, 'current_state' => Order::STATE_HOLDED, 'expected_state' => Order::STATE_HOLDED, - false, - 0, - true + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => true, + 'can_unhold' => false, + 'is_not_virtual' => true + ], + 'processing - !canCreditmemo!canShip -> complete(virtual product)' => [ + 'can_credit_memo' => false, + 'can_credit_memo_invoke_count' => 1, + 'can_ship' => false, + 'call_can_skip_num' => 2, + 'current_state' => Order::STATE_PROCESSING, + 'expected_state' => Order::STATE_COMPLETE, + 'is_in_process' => false, + 'get_is_in_process_invoke_count' => 0, + 'is_canceled' => false, + 'can_unhold' => false, + 'is_not_virtual' => false ], ]; } diff --git a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php index 8531c54c6c4a0..5909ebd76feb1 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php @@ -34,6 +34,9 @@ use Psr\Log\LoggerInterface; /** + * Class ShipOrderTest + * + * Test Save shipment and order data * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ @@ -186,7 +189,7 @@ protected function setUp(): void ->getMockForAbstractClass(); $this->shipOrderValidatorMock = $this->getMockBuilder(ShipOrderInterface::class) ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->getMock(); $this->validationMessagesMock = $this->getMockBuilder(ValidatorResultInterface::class) ->disableOriginalConstructor() ->setMethods(['hasMessages', 'getMessages', 'addMessage']) @@ -291,7 +294,7 @@ public function testExecute($orderId, $items, $notify, $appendComment) ->method('notify') ->with($this->orderMock, $this->shipmentMock, $this->shipmentCommentCreationMock); } - $this->shipmentMock->expects($this->once()) + $this->shipmentMock->expects($this->exactly(2)) ->method('getEntityId') ->willReturn(2); $this->assertEquals( diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index 9ede9a79f7f8b..de062029fb53b 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -220,7 +220,7 @@ <column xsi:type="varchar" name="shipping_method" nullable="true" length="120"/> <column xsi:type="varchar" name="store_currency_code" nullable="true" length="3" comment="Store Currency Code"/> <column xsi:type="varchar" name="store_name" nullable="true" length="255" comment="Store Name"/> - <column xsi:type="varchar" name="x_forwarded_for" nullable="true" length="32" comment="X Forwarded For"/> + <column xsi:type="varchar" name="x_forwarded_for" nullable="true" length="255" comment="X Forwarded For"/> <column xsi:type="text" name="customer_note" nullable="true" comment="Customer Note"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> diff --git a/app/code/Magento/Sales/etc/webapi_rest/di.xml b/app/code/Magento/Sales/etc/webapi_rest/di.xml index 5d7838297a7c7..1a8478438b04a 100644 --- a/app/code/Magento/Sales/etc/webapi_rest/di.xml +++ b/app/code/Magento/Sales/etc/webapi_rest/di.xml @@ -19,4 +19,7 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Order\ShipmentRepository"> + <plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" /> + </type> </config> diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 5d7838297a7c7..1a8478438b04a 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -19,4 +19,7 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Order\ShipmentRepository"> + <plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" /> + </type> </config> diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index 44626027bac69..97c1706f975da 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -117,6 +117,7 @@ Capture,Capture "Total Paid","Total Paid" "Total Refunded","Total Refunded" "Total Due","Total Due" +"Total Canceled","Total Canceled" Edit,Edit "Are you sure you want to send an order email to customer?","Are you sure you want to send an order email to customer?" "This will create an offline refund. To create an online refund, open an invoice and create credit memo for it. Do you want to continue?","This will create an offline refund. To create an online refund, open an invoice and create credit memo for it. Do you want to continue?" diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml index c9b2f7c8de254..a3904ac09c6b4 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml @@ -4,41 +4,64 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php -/* @var $block \Magento\Sales\Block\Adminhtml\Items\Column\Name */ +/** + * @var $block \Magento\Sales\Block\Adminhtml\Items\Column\Name + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ +?> + +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); ?> -<?php if ($_item = $block->getItem()) : ?> +<?php if ($_item = $block->getItem()): ?> <div id="order_item_<?= (int) $_item->getId() ?>_title" class="product-title"> <?= $block->escapeHtml($_item->getName()) ?> </div> <div class="product-sku-block"> - <span><?= $block->escapeHtml(__('SKU'))?>:</span> <?= /* @noEscape */ implode('<br />', $this->helper(\Magento\Catalog\Helper\Data::class)->splitSku($block->escapeHtml($block->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU'))?>:</span> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($block->escapeHtml($block->getSku()))) ?> </div> - <?php if ($block->getOrderOptions()) : ?> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $_option) : ?> + <?php foreach ($block->getOrderOptions() as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?>:</dt> <dd> - <?php if (isset($_option['custom_view']) && $_option['custom_view']) : ?> + <?php if (isset($_option['custom_view']) && $_option['custom_view']): ?> <?= /* @noEscape */ $block->getCustomizedOptionValue($_option) ?> - <?php else : ?> + <?php else: ?> <?php $_option = $block->getFormattedOption($_option['value']); ?> <?php $dots = 'dots' . uniqid(); ?> <?php $id = 'id' . uniqid(); ?> - <?= $block->escapeHtml($_option['value'], ['a', 'br']) ?><?php if (isset($_option['remainder']) && $_option['remainder']) : ?><span id="<?= /* @noEscape */ $dots; ?>"> ...</span><span id="<?= /* @noEscape */ $id; ?>"><?= $block->escapeHtml($_option['remainder'], ['a']) ?></span> - <script> + <?= $block->escapeHtml($_option['value'], ['a', 'br']) ?> + <?php if (isset($_option['remainder']) && $_option['remainder']): ?> + <span id="<?= /* @noEscape */ $dots; ?>"> ...</span> + <span id="<?= /* @noEscape */ $id; ?>"> + <?= $block->escapeHtml($_option['remainder'], ['a']) ?> + </span> + <?php $scriptString = <<<script require(['prototype'], function() { - $('<?= /* @noEscape */ $id; ?>').hide(); - $('<?= /* @noEscape */ $id; ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $id; ?>').show();}); - $('<?= /* @noEscape */ $id; ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $dots; ?>').hide();}); - $('<?= /* @noEscape */ $id; ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $id; ?>').hide();}); - $('<?= /* @noEscape */ $id; ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $dots; ?>').show();}); - }); - </script> + +script; + $scriptString .= "$('" . /* @noEscape */ $id . "').hide();" . PHP_EOL; + $scriptString .= "$('" . /* @noEscape */ $id . + "').up().observe('mouseover', function(){ $('" . /* @noEscape */ $id . "').show();});" . + PHP_EOL; + $scriptString .= "$('" . /* @noEscape */ $id . + "').up().observe('mouseover', function(){ $('" . /* @noEscape */ $dots . + "').hide();});" . PHP_EOL; + $scriptString .= "$('" . /* @noEscape */ $id . + "').up().observe('mouseout', function(){ $('" . /* @noEscape */ $id . + "').hide();});" . PHP_EOL; + $scriptString .= "$('" . /* @noEscape */ $id . + "').up().observe('mouseout', function(){ $('" . /* @noEscape */ $dots . + "').show();});" . PHP_EOL . "});" . PHP_EOL; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> <?php endif; ?> </dd> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml index 05e753c78f4a3..c3a7321a3052f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($_entity = $block->getEntity()) : ?> +<?php if ($_entity = $block->getEntity()): ?> <div id="comments_block" class="edit-order-comments"> <div class="order-history-block"> <div class="admin__field field-row"> @@ -20,7 +22,7 @@ </div> <div class="admin__field"> <div class="order-history-comments-options"> - <?php if ($block->canSendCommentEmail()) : ?> + <?php if ($block->canSendCommentEmail()): ?> <div class="admin__field admin__field-option"> <input name="comment[is_customer_notified]" type="checkbox" @@ -48,31 +50,41 @@ </div> <ul class="note-list"> - <?php foreach ($_entity->getCommentsCollection(true) as $_comment) : ?> + <?php foreach ($_entity->getCommentsCollection(true) as $_comment): ?> <li> - <span class="note-list-date"><?= /* @noEscape */ $block->formatDate($_comment->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> - <span class="note-list-time"><?= /* @noEscape */ $block->formatTime($_comment->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> + <span class="note-list-date"> + <?= /* @noEscape */ $block->formatDate($_comment->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?> + </span> + <span class="note-list-time"> + <?= /* @noEscape */ $block->formatTime($_comment->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?> + </span> <span class="note-list-customer"> <?= $block->escapeHtml(__('Customer')) ?> - <?php if ($_comment->getIsCustomerNotified()) : ?> + <?php if ($_comment->getIsCustomerNotified()): ?> <span class="note-list-customer-notified"><?= $block->escapeHtml(__('Notified')) ?></span> - <?php else : ?> - <span class="note-list-customer-not-notified"><?= $block->escapeHtml(__('Not Notified')) ?></span> + <?php else: ?> + <span class="note-list-customer-not-notified"> + <?= $block->escapeHtml(__('Not Notified')) ?> + </span> <?php endif; ?> </span> - <div class="note-list-comment"><?= $block->escapeHtml($_comment->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?></div> + <div class="note-list-comment"> + <?= $block->escapeHtml($_comment->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?> + </div> </li> <?php endforeach; ?> </ul> </div> -<script> + <?php $scriptString = <<<script require(['prototype'], function(){ submitComment = function() { - submitAndReloadArea($('comments_block').parentNode, '<?= $block->escapeUrl($block->getSubmitUrl()) ?>') + submitAndReloadArea($('comments_block').parentNode, '{$block->escapeJs($block->getSubmitUrl())}') }; if ($('submit_comment_button')) { $('submit_comment_button').observe('click', submitComment); } }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml index e29c1d2db01ce..f1c8b249fe68a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->hasMethods()) : ?> +<?php if ($block->hasMethods()): ?> <div id="order-billing_method_form"> <dl class="admin__payment-methods control"> <?php @@ -13,23 +15,27 @@ $_counter = 0; $currentSelectedMethod = $block->getSelectedMethodCode(); ?> - <?php foreach ($_methods as $_method) : + <?php foreach ($_methods as $_method): $_code = $_method->getCode(); $_counter++; ?> <dt class="admin__field-option"> - <?php if ($_methodsCount > 1) : ?> + <?php if ($_methodsCount > 1): ?> <input id="p_method_<?= $block->escapeHtmlAttr($_code); ?>" value="<?= $block->escapeHtmlAttr($_code); ?>" type="radio" name="payment[method]" title="<?= $block->escapeHtmlAttr($_method->getTitle()); ?>" - onclick="payment.switchMethod('<?= $block->escapeJs($_code); ?>')" - <?php if ($currentSelectedMethod == $_code) : ?> + <?php if ($currentSelectedMethod == $_code): ?> checked="checked" <?php endif; ?> data-validate="{'validate-one-required-by-name':true}" class="admin__control-radio"/> - <?php else :?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "payment.switchMethod('" . $block->escapeJs($_code) . "')", + 'input#p_method_' . $block->escapeJs($_code) + ) ?> + <?php else:?> <span class="no-display"> <input id="p_method_<?= $block->escapeHtmlAttr($_code); ?>" value="<?= $block->escapeHtmlAttr($_code); ?>" @@ -49,19 +55,30 @@ <?php endforeach; ?> </dl> </div> - <script> + <?php $scriptString = <<<script require([ 'mage/apply/main', 'Magento_Sales/order/create/form' ], function(mage) { mage.apply(); - <?php if ($_methodsCount !== 1) : ?> - order.setPaymentMethod('<?= $block->escapeJs($currentSelectedMethod); ?>'); - <?php else : ?> - payment.switchMethod('<?= $block->escapeJs($currentSelectedMethod); ?>'); - <?php endif; ?> + +script; + if ($_methodsCount !== 1): + $scriptString .= <<<script + order.setPaymentMethod('{$block->escapeJs($currentSelectedMethod)}'); +script; + else: + $scriptString .= <<<script + payment.switchMethod('{$block->escapeJs($currentSelectedMethod)}'); +script; + endif; + $scriptString .= <<<script + }); - </script> -<?php else : ?> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<?php else: ?> <div class="admin__message-empty"><?= $block->escapeHtml(__('No Payment Methods')); ?></div> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml index dfa6b5e6fff79..ae3d8831d276f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml @@ -4,11 +4,16 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Comment $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Comment $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="admin__field field-comment"> - <label for="order-comment" class="admin__field-label"><span><?= $block->escapeHtml(__('Order Comments')) ?></span></label> + <label for="order-comment" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Order Comments')) ?></span> + </label> <div class="admin__field-control"> <textarea id="order-comment" @@ -16,8 +21,10 @@ class="admin__control-textarea"><?= $block->escapeHtml($block->getCommentNote()) ?></textarea> </div> </div> -<script> +<?php $scriptString = <<<script require(["Magento_Sales/order/create/form"], function(){ order.commentFieldsBind('order-comment') }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml index 87ef29c7d42ed..469155a80891a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml @@ -5,31 +5,40 @@ */ ?> <?php -/* @var \Magento\Sales\Block\Adminhtml\Order\Create\Coupons $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Coupons $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="admin__field field-apply-coupon-code"> <label class="admin__field-label"><span><?= $block->escapeHtml(__('Apply Coupon Code')) ?></span></label> <div class="admin__field-control"> - <?php if (!$block->getCouponCode()) : ?> + <?php if (!$block->getCouponCode()): ?> <input type="text" class="admin__control-text" id="coupons:code" value="" name="coupon_code" /> <?= $block->getButtonHtml(__('Apply'), 'order.handleOnclickCoupon($F(\'coupons:code\'))') ?> <?php endif; ?> - <?php if ($block->getCouponCode()) : ?> + <?php if ($block->getCouponCode()): ?> <p class="added-coupon-code"> <span><?= $block->escapeHtml($block->getCouponCode()) ?></span> - <a href="#" onclick="order.applyCoupon(''); return false;" title="<?= $block->escapeHtmlAttr(__('Remove Coupon Code')) ?>" + <a href="#" title="<?= $block->escapeHtmlAttr(__('Remove Coupon Code')) ?>" class="action-remove"><span><?= $block->escapeHtml(__('Remove')) ?></span></a> </p> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.applyCoupon('');event.preventDefault();", + 'p.added-coupon-code a.action-remove' + ) ?> <?php endif; ?> - <script> + <?php $isVirtual = ($block->getQuote()->isVirtual() ? 'false' : 'true'); + $scriptString = <<<script require([ "jquery", 'Magento_Ui/js/modal/alert', 'mage/translate', "Magento_Sales/order/create/form" ], function($, alert) { - order.overlay('shipping-method-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); - order.overlay('address-shipping-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); + order.overlay('shipping-method-overlay', {$isVirtual}); + order.overlay('address-shipping-overlay', {$isVirtual}); order.handleOnclickCoupon = function (code) { if (!code) { alert({ @@ -40,6 +49,8 @@ } }; }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml index f5edf0949374b..ced1ea5e7b73a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml @@ -5,22 +5,39 @@ */ /** @var \Magento\Sales\Block\Adminhtml\Order\Create\Data $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="page-create-order"> - <script> + <?php $scriptString = <<<script require(["Magento_Sales/order/create/form"], function(){ - order.setCurrencySymbol('<?= $block->escapeJs($block->getCurrencySymbol($block->getCurrentCurrencyCode())) ?>') + order.setCurrencySymbol('{$block->escapeJs($block->getCurrencySymbol($block->getCurrentCurrencyCode()))}') }); -</script> - <div class="order-details<?php if ($block->getCustomerId()) : ?> order-details-existing-customer<?php endif; ?>"> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <div class="order-details<?php if ($block->getCustomerId()): ?> order-details-existing-customer<?php endif; ?>"> - <div id="order-additional_area" style="display: none" class="admin__page-section order-additional-area"> + <div id="order-additional_area" class="admin__page-section order-additional-area"> <?= $block->getChildHtml('additional_area') ?> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#order-additional_area' + ) ?> - <div id="order-search" style="display: none" class="admin__page-section order-search-items"> + <div id="order-search" class="admin__page-section order-search-items no-display"> <?= $block->getChildHtml('search') ?> </div> + <?= /* @noEscape */ $secureRenderer->renderTag( + 'script', + [], + "var elemOrderSearch = document.querySelector('div#order-search'); + if (elemOrderSearch) { + elemOrderSearch.style.display = 'none'; + elemOrderSearch.classList.remove('no-display'); + }", + false + ) ?> <section id="order-items" class="admin__page-section order-items" data-mage-init='{"loader": {}}'> <?= $block->getChildHtml('items') ?> @@ -60,7 +77,7 @@ </div> </section> - <?php if ($block->getChildBlock('card_validation')) : ?> + <?php if ($block->getChildBlock('card_validation')): ?> <section id="order-card_validation" class="admin__page-section order-card-validation"> <?= $block->getChildHtml('card_validation') ?> </section> @@ -85,7 +102,7 @@ </section> </div> - <?php if ($block->getCustomerId()) : ?> + <?php if ($block->getCustomerId()): ?> <div class="order-sidebar"> <div class="store-switcher order-currency"> <label class="admin__field-label" for="currency_switcher"> @@ -93,14 +110,22 @@ </label> <select id="currency_switcher" class="admin__control-select" - name="order[currency]" - onchange="order.setCurrencyId(this.value); order.setCurrencySymbol(this.options[this.selectedIndex].getAttribute('symbol'));"> - <?php foreach ($block->getAvailableCurrencies() as $_code) : ?> - <option value="<?= $block->escapeHtmlAttr($_code) ?>"<?php if ($_code == $block->getCurrentCurrencyCode()) : ?> selected="selected"<?php endif; ?> symbol="<?= $block->escapeHtmlAttr($block->getCurrencySymbol($_code)) ?>"> + name="order[currency]"> + <?php foreach ($block->getAvailableCurrencies() as $_code): ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>" + <?php if ($_code == $block->getCurrentCurrencyCode()): ?> selected="selected"<?php endif; ?> + symbol="<?= $block->escapeHtmlAttr($block->getCurrencySymbol($_code)) ?>"> <?= $block->escapeHtml($block->getCurrencyName($_code)) ?> </option> <?php endforeach; ?> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + "order.setCurrencyId(this.value); + order.setCurrencySymbol(this.options[this.selectedIndex].getAttribute('symbol'));", + 'select#currency_switcher' + ) ?> + </div> <div class="customer-current-activity" id="order-sidebar"> <?= $block->getChildHtml('sidebar') ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml index c38acb9b79e47..bd2e08d30ccdd 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml @@ -5,19 +5,50 @@ */ /** @var \Magento\Sales\Block\Adminhtml\Order\Create\Form $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<form id="edit_form" data-order-config='<?= $block->escapeHtml($block->getOrderDataJson()) ?>' data-load-base-url="<?= $block->escapeUrl($block->getLoadBlockUrl()) ?>" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" enctype="multipart/form-data"> +<form id="edit_form" data-order-config='<?= $block->escapeHtml($block->getOrderDataJson()) ?>' + data-load-base-url="<?= $block->escapeUrl($block->getLoadBlockUrl()) ?>" + action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" enctype="multipart/form-data"> <?= $block->getBlockHtml('formkey') ?> <div id="order-message"> <?= $block->getChildHtml('message') ?> </div> - <div id="order-customer-selector" class="fieldset-wrapper order-customer-selector" style="display:<?= /* @noEscape */ $block->getCustomerSelectorDisplay() ?>"> + <div id="order-customer-selector" class="fieldset-wrapper order-customer-selector no-display"> <?= $block->getChildHtml('customer.grid.container') ?> </div> - <div id="order-store-selector" class="fieldset-wrapper" style="display:<?= /* @noEscape */ $block->getStoreSelectorDisplay() ?>"> + <div id="order-store-selector" class="fieldset-wrapper no-display"> <?= $block->getChildHtml('store') ?> </div> - <div id="order-data" style="display:<?= /* @noEscape */ $block->getDataSelectorDisplay() ?>"> + <div id="order-data" class="no-display"> <?= $block->getChildHtml('data') ?> </div> </form> +<?php $scriptString = <<<Script +require(['jquery'], function($){ + 'use strict'; + +Script; +if ($block->getCustomerSelectorDisplay()) { + $scriptString .= <<<Script + $('div#order-customer-selector').css('display', '{$block->getCustomerSelectorDisplay()}'); + $('div#order-customer-selector').removeClass('no-display'); +Script; +} +if ($block->getStoreSelectorDisplay()) { + $scriptString .= <<<Script + $('div#order-store-selector').css('display', '{$block->getStoreSelectorDisplay()}'); + $('div#order-store-selector').removeClass('no-display'); +Script; +} +if ($block->getDataSelectorDisplay()) { + $scriptString .= <<<Script + $('div#order-data').css('display', '{$block->getDataSelectorDisplay()}'); + $('div#order-data').removeClass('no-display'); +Script; +} +$scriptString .= <<<Script +}); +Script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml index 85ca9c8159bcc..39303568f8899 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Form\Account */ +/** + * @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Form\Account + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="admin__page-section-title <?= $block->escapeHtmlAttr($block->getHeaderCssClass()) ?>"> @@ -14,9 +17,10 @@ <div id="customer_account_fields" class="admin__page-section-content"> <?= $block->getForm()->getHtml() ?> </div> - -<script> +<?php $scriptString = <<<script require(["prototype", "Magento_Sales/order/create/form"], function(){ order.accountFieldsBind($('customer_account_fields')); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index 5ce001474f5f5..dc007e4801b41 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ /** * @var \Magento\Customer\Model\ResourceModel\Address\Collection $addressCollection @@ -12,7 +15,7 @@ $addressCollection = $block->getData('customerAddressCollection'); $addressArray = []; -if ($block->getCustomerId()) : +if ($block->getCustomerId()): $addressArray = $addressCollection->setCustomerFilter([$block->getCustomerId()])->toArray(); endif; @@ -22,28 +25,34 @@ endif; $customerAddressFormatter = $block->getData('customerAddressFormatter'); /** - * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address|\Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address| + * \Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block */ -if ($block->getIsShipping()) : +if ($block->getIsShipping()): $_fieldsContainerId = 'order-shipping_address_fields'; $_addressChoiceContainerId = 'order-shipping_address_choice'; - ?> - <script> + + $addressCollectionJson = /* @noEscape */ $block->getAddressCollectionJson(); + $scriptString= <<<script require(["Magento_Sales/order/create/form"], function(){ - order.shippingAddressContainer = '<?= $block->escapeJs($_fieldsContainerId) ?>'; - order.setAddresses(<?= /* @noEscape */ $block->getAddressCollectionJson() ?>); + order.shippingAddressContainer = '{$block->escapeJs($_fieldsContainerId)}'; + order.setAddresses({$addressCollectionJson}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php -else : +else: $_fieldsContainerId = 'order-billing_address_fields'; $_addressChoiceContainerId = 'order-billing_address_choice'; ?> - <script> + <?php $scriptString = <<<script require(["Magento_Sales/order/create/form"], function(){ - order.billingAddressContainer = '<?= $block->escapeJs($_fieldsContainerId) ?>'; + order.billingAddressContainer = '{$block->escapeJs($_fieldsContainerId)}'; }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> @@ -52,33 +61,47 @@ endif; ?> <span><?= $block->escapeHtml($block->getHeaderText()) ?></span> </legend><br> - <fieldset id="<?= $block->escapeHtmlAttr($_addressChoiceContainerId) ?>" class="admin__fieldset order-choose-address"> - <?php if ($block->getIsShipping()) : ?> + <fieldset id="<?= $block->escapeHtmlAttr($_addressChoiceContainerId) ?>" + class="admin__fieldset order-choose-address"> + <?php if ($block->getIsShipping()): ?> <div class="admin__field admin__field-option admin__field-shipping-same-as-billing"> <input type="checkbox" id="order-shipping_same_as_billing" name="shipping_same_as_billing" - onclick="order.setShippingAsBilling(this.checked)" class="admin__control-checkbox" - <?php if ($block->getIsAsBilling()) : ?>checked<?php endif; ?> /> + class="admin__control-checkbox" + <?php if ($block->getIsAsBilling()): ?>checked<?php endif; ?> /> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.setShippingAsBilling(this.checked)", + 'input#order-shipping_same_as_billing' + ) ?> <label for="order-shipping_same_as_billing" class="admin__field-label"> <?= $block->escapeHtml(__('Same As Billing Address')) ?> </label> </div> <?php endif; ?> <div class="admin__field admin__field-select-from-existing-address"> - <label class="admin__field-label"><?= $block->escapeHtml(__('Select from existing customer addresses:')) ?></label> + <label class="admin__field-label"> + <?= $block->escapeHtml(__('Select from existing customer addresses:')) ?> + </label> <?php $_id = $block->getForm()->getHtmlIdPrefix() . 'customer_address_id' ?> <div class="admin__field-control"> <select id="<?= $block->escapeHtmlAttr($_id) ?>" - name="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlNamePrefix()) ?>[customer_address_id]" - onchange="order.selectAddress(this, '<?= $block->escapeJs($_fieldsContainerId) ?>')" + name="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlNamePrefix()) + ?>[customer_address_id]" class="admin__control-select"> <option value=""><?= $block->escapeHtml(__('Add New Address')) ?></option> - <?php foreach ($addressArray as $addressId => $address) : ?> + <?php foreach ($addressArray as $addressId => $address): ?> <option - value="<?= $block->escapeHtmlAttr($addressId) ?>"<?php if ($addressId == $block->getAddressId()) : ?> selected="selected"<?php endif; ?>> + value="<?= $block->escapeHtmlAttr($addressId) ?>" + <?php if ($addressId == $block->getAddressId()): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($customerAddressFormatter->getAddressAsString($address)) ?> </option> <?php endforeach; ?> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + "order.selectAddress(this, '" . $block->escapeJs($_fieldsContainerId) . "')", + 'select#' . $block->escapeJs($_id) + ) ?> </div> </div> </fieldset> @@ -87,23 +110,40 @@ endif; ?> <?= $block->getForm()->toHtml() ?> <div class="admin__field admin__field-option order-save-in-address-book"> - <input name="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlNamePrefix()) ?>[save_in_address_book]" type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1"<?php if (!$block->getDontSaveInAddressBook()) : ?> checked="checked"<?php endif; ?> class="admin__control-checkbox"/> + <input name="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlNamePrefix()) ?>[save_in_address_book]" + type="checkbox" + id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" + value="1" + <?php if (!$block->getDontSaveInAddressBook()): ?> checked="checked"<?php endif; ?> + class="admin__control-checkbox"/> <label for="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" class="admin__field-label"><?= $block->escapeHtml(__('Save in address book')) ?></label> </div> </div> <?php $hideElement = 'address-' . ($block->getIsShipping() ? 'shipping' : 'billing') . '-overlay'; ?> - <div style="display: none;" id="<?= /* @noEscape */ $hideElement ?>" class="order-methods-overlay"> + <div id="<?= /* @noEscape */ $hideElement ?>" class="order-methods-overlay"> <span><?= $block->escapeHtml(__('You don\'t need to select a shipping address.')) ?></span> </div> - - <script> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display: none;", + 'div#' . /* @noEscape */ $hideElement + ) ?> + <?php $scriptString = <<<script require(["Magento_Sales/order/create/form"], function(){ - order.bindAddressFields('<?= $block->escapeJs($_fieldsContainerId) ?>'); - order.bindAddressFields('<?= $block->escapeJs($_addressChoiceContainerId) ?>'); - <?php if ($block->getIsShipping() && $block->getIsAsBilling()) : ?> + order.bindAddressFields('{$block->escapeJs($_fieldsContainerId)}'); + order.bindAddressFields('{$block->escapeJs($_addressChoiceContainerId)}'); + +script; + if ($block->getIsShipping() && $block->getIsAsBilling()): + $scriptString .= <<<script order.disableShippingAddress(true); - <?php endif; ?> + +script; + endif; + $scriptString .= <<<script }); - </script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </fieldset> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml index d27782fd20b15..baf283e673e40 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml @@ -4,25 +4,37 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Giftmessage $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Giftmessage $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ +?> +<?php /** @var \Magento\GiftMessage\Helper\Message $giftMessageHelper */ +$giftMessageHelper = $block->getData('giftMessageHelper'); ?> -<?php if ($this->helper(\Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())) : ?> +<?php if ($giftMessageHelper->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())): ?> <?php $_items = $block->getItems(); ?> <div id="order-giftmessage" class="giftmessage-order-create"> <fieldset class="admin__fieldset"> - <legend class="admin__legend"><span><?= $block->escapeHtml(__('Gift Message for the Entire Order')) ?></span></legend> + <legend class="admin__legend"> + <span><?= $block->escapeHtml(__('Gift Message for the Entire Order')) ?></span> + </legend> <br> - <?php if ($this->helper(\Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())) : ?> - <p><?= $block->escapeHtml(__('Leave this box blank if you don\'t want to leave a gift message for the entire order.')) ?></p> + <?php if ($giftMessageHelper->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())): ?> + <p> + <?= $block->escapeHtml( + __('Leave this box blank if you don\'t want to leave a gift message for the entire order.') + ) ?> + </p> <?= $block->getFormHtml($block->getQuote(), 'main') ?> <?php endif; ?> </fieldset> - <script> + <?php $scriptString = <<<script require(['Magento_Sales/order/create/form'], function(){ order.giftmessageFieldsBind('order-giftmessage'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml index eee167dde50d6..ada5cb36cdbb0 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml @@ -4,16 +4,19 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php /** * @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Items\Grid + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper =$block->getData('catalogHelper'); ?> <?php $_items = $block->getItems() ?> -<?php if (empty($_items)) : ?> +<?php if (empty($_items)): ?> <div id="order-items_grid"> <div class="admin__table-wrapper"> <table class="data-table admin__table-primary order-tables"> @@ -36,9 +39,9 @@ </table> </div> </div> -<?php else : ?> +<?php else: ?> <div class="admin__table-wrapper" id="order-items_grid"> - <?php if (count($_items) > 10) : ?> + <?php if (count($_items) > 10): ?> <div class="actions update actions-update"> <?= $block->getButtonHtml(__('Update Items and Quantities'), 'order.itemsUpdate()', 'action-secondary') ?> </div> @@ -59,21 +62,31 @@ <tr> <td class="col-total"><?= $block->escapeHtml(__('Total %1 product(s)', count($_items))) ?></td> <td colspan="2" class="col-subtotal"><?= $block->escapeHtml(__('Subtotal:')) ?></td> - <td class="col-price"><strong><?= /* @noEscape */ $block->formatPrice($block->getSubtotal()) ?></strong></td> - <td class="col-price"><strong><?= /* @noEscape */ $block->formatPrice($block->getDiscountAmount()) ?></strong></td> - <td class="col-price"><strong><?= /* @noEscape */ $block->formatPrice($block->getSubtotalWithDiscount()); ?></strong></td> + <td class="col-price"> + <strong><?= /* @noEscape */ $block->formatPrice($block->getSubtotal()) ?></strong> + </td> + <td class="col-price"> + <strong><?= /* @noEscape */ $block->formatPrice($block->getDiscountAmount()) ?></strong> + </td> + <td class="col-price"> + <strong><?= /* @noEscape */ $block->formatPrice($block->getSubtotalWithDiscount()); ?></strong> + </td> <td colspan="2"> </td> </tr> </tfoot> <?php $i = 0 ?> - <?php foreach ($_items as $_item) : $i++ ?> + <?php foreach ($_items as $_item): $i++ ?> <tbody class="<?= /* @noEscape */ ($i%2) ? 'even' : 'odd' ?>"> <tr> <td class="col-product"> - <span id="order_item_<?= (int) $_item->getId() ?>_title"><?= $block->escapeHtml($_item->getName()) ?></span> + <span id="order_item_<?= (int) $_item->getId() ?>_title"><?= + $block->escapeHtml($_item->getName()) ?></span> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(\Magento\Catalog\Helper\Data::class)->splitSku($block->escapeHtml($_item->getSku()))) ?> + <?= /* @noEscape */ implode( + '<br />', + $catalogHelper->splitSku($block->escapeHtml($_item->getSku())) + ) ?> </div> <div class="product-configure-block"> <?= $block->getConfigureButtonHtml($_item) ?> @@ -84,33 +97,56 @@ <?= $block->getItemUnitPriceHtml($_item) ?> <?php $_isCustomPrice = $block->usedCustomPriceForItem($_item) ?> - <?php if ($_tier = $block->getTierHtml($_item)) : ?> - <div id="item_tier_block_<?= (int) $_item->getId() ?>"<?php if ($_isCustomPrice) : ?> style="display:none"<?php endif; ?>> - <a href="#" onclick="$('item_tier_<?= (int) $_item->getId() ?>').toggle();return false;"><?= $block->escapeHtml(__('Tier Pricing')) ?></a> - <div style="display:none" id="item_tier_<?= (int) $_item->getId() ?>"><?= /* @noEscape */ $_tier ?></div> + <?php if ($_tier = $block->getTierHtml($_item)): ?> + <div id="item_tier_block_<?= (int) $_item->getId() ?>"> + <a href="#"><?= $block->escapeHtml(__('Tier Pricing')) ?></a> + <div id="item_tier_<?= (int) $_item->getId() ?>"><?= /* @noEscape */ $_tier ?></div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'div#item_tier_' . (int) $_item->getId() + ) ?> </div> + <?php if ($_isCustomPrice): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'div#item_tier_block_' . (int) $_item->getId() + ) ?> + <?php endif; ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "$('item_tier_" . (int) $_item->getId() ."').toggle();event.preventDefault();", + 'div#item_tier_block_' . (int) $_item->getId() . ' a' + ) ?> <?php endif; ?> - <?php if ($block->canApplyCustomPrice($_item)) : ?> + <?php if ($block->canApplyCustomPrice($_item)): ?> <div class="custom-price-block"> <input type="checkbox" class="admin__control-checkbox" id="item_use_custom_price_<?= (int) $_item->getId() ?>" - <?php if ($_isCustomPrice) : ?> checked="checked"<?php endif; ?> - onclick="order.toggleCustomPrice(this, 'item_custom_price_<?= (int) $_item->getId() ?>', 'item_tier_block_<?= (int) $_item->getId() ?>');"/> + <?php if ($_isCustomPrice): ?> checked="checked"<?php endif; ?> /> <label class="normal admin__field-label" for="item_use_custom_price_<?= (int) $_item->getId() ?>"> <span><?= $block->escapeHtml(__('Custom Price')) ?>*</span></label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.toggleCustomPrice(this, 'item_custom_price_" . (int) $_item->getId() . + "', 'item_tier_block_" . (int) $_item->getId() . "');", + 'input#item_use_custom_price_' . (int) $_item->getId() + ) ?> </div> <?php endif; ?> <input id="item_custom_price_<?= (int) $_item->getId() ?>" name="item[<?= (int) $_item->getId() ?>][custom_price]" value="<?= /* @noEscape */ sprintf("%.2f", $block->getOriginalEditablePrice($_item)) ?>" - <?php if (!$_isCustomPrice) : ?> - style="display:none" + <?php if (!$_isCustomPrice): ?> disabled="disabled" <?php endif; ?> class="input-text item-price admin__control-text"/> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'input#item_custom_price_' . (int) $_item->getId() + ) ?> </td> <td class="col-qty"> <input name="item[<?= (int) $_item->getId() ?>][qty]" @@ -127,7 +163,7 @@ <input id="item_use_discount_<?= (int) $_item->getId() ?>" class="admin__control-checkbox" name="item[<?= (int) $_item->getId() ?>][use_discount]" - <?php if (!$_item->getNoDiscount()) : ?>checked="checked"<?php endif; ?> + <?php if (!$_item->getNoDiscount()): ?>checked="checked"<?php endif; ?> value="1" type="checkbox" /> <label @@ -144,16 +180,19 @@ <select class="admin__control-select" name="item[<?= (int) $_item->getId() ?>][action]"> <option value=""><?= $block->escapeHtml(__('Please select')) ?></option> <option value="remove"><?= $block->escapeHtml(__('Remove')) ?></option> - <?php if ($block->getCustomerId() && $block->getMoveToCustomerStorage()) : ?> + <?php if ($block->getCustomerId() && $block->getMoveToCustomerStorage()): ?> <option value="cart"><?= $block->escapeHtml(__('Move to Shopping Cart')) ?></option> - <?php if ($block->isMoveToWishlistAllowed($_item)) : ?> + <?php if ($block->isMoveToWishlistAllowed($_item)): ?> <?php $wishlists = $block->getCustomerWishlists();?> - <?php if (count($wishlists) <= 1) : ?> - <option value="wishlist"><?= $block->escapeHtml(__('Move to Wish List')) ?></option> - <?php else : ?> + <?php if (count($wishlists) <= 1): ?> + <option value="wishlist"><?= $block->escapeHtml(__('Move to Wish List')) ?> + </option> + <?php else: ?> <optgroup label="<?= $block->escapeHtml(__('Move to Wish List')) ?>"> - <?php foreach ($wishlists as $wishlist) :?> - <option value="wishlist_<?= (int) $wishlist->getId() ?>"><?= $block->escapeHtml($wishlist->getName()) ?></option> + <?php foreach ($wishlists as $wishlist):?> + <option value="wishlist_<?= (int) $wishlist->getId() ?>"> + <?= $block->escapeHtml($wishlist->getName()) ?> + </option> <?php endforeach;?> </optgroup> <?php endif; ?> @@ -164,21 +203,22 @@ </tr> <?php $hasMessageError = false; ?> - <?php foreach ($_item->getMessage(false) as $messageError) : ?> - <?php if (!empty($messageError)) : + <?php foreach ($_item->getMessage(false) as $messageError): ?> + <?php if (!empty($messageError)): $hasMessageError = true; endif; ?> <?php endforeach; ?> - <?php if ($hasMessageError) : ?> + <?php if ($hasMessageError): ?> <tr class="row-messages-error"> <td colspan="100"> <!-- ToDo UI: remove the 100 --> - <?php foreach ($_item->getMessage(false) as $message) : + <?php foreach ($_item->getMessage(false) as $message): if (empty($message)) { continue; } ?> - <div class="message <?php if ($_item->getHasError()) : ?>message-error<?php else : ?>message-notice<?php endif; ?>"> + <div class="message <?php if ($_item->getHasError()): ?>message-error<?php else: + ?>message-notice<?php endif; ?>"> <?= $block->escapeHtml($message) ?> </div> <?php endforeach; ?> @@ -198,15 +238,17 @@ <div id="order-coupons" class="order-coupons"><?= $block->getChildHtml() ?></div> </div> - <script> + <?php $scriptString = <<<script require([ 'Magento_Sales/order/create/form' ], function(){ order.itemsOnchangeBind() }); - </script> - <?php if ($block->isGiftMessagesAvailable()) : ?> - <script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <?php if ($block->isGiftMessagesAvailable()): ?> + <?php $scriptString = <<<script require([ "prototype", "Magento_Sales/order/giftoptions_tooltip" @@ -241,6 +283,9 @@ //]]> }); - </script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml index eb39f71265cd6..0eb3caac12318 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script + require([ "prototype", "Magento_Sales/order/create/form", @@ -14,11 +18,14 @@ require([ order.sidebarHide(); if (window.productConfigure) { productConfigure.addListType('product_to_add', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/configureProductToAdd'))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('sales/order_create/configureProductToAdd'))}' }); productConfigure.addListType('quote_items', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/configureQuoteItems'))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('sales/order_create/configureQuoteItems'))}' }); } }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml index 1dcf57d879543..34f4bae1947e1 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml @@ -3,5 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<input type="checkbox" name="newsletter:subscribe"> <label for="newsletter:subscribe" style="width: 90%; float: none;"><?= $block->escapeHtml(__('Subscribe to Newsletter')) ?></label><br/> +<input type="checkbox" name="newsletter:subscribe"> +<label for="newsletter:subscribe"> + <?= $block->escapeHtml(__('Subscribe to Newsletter')) ?> +</label> +<?=/* @noEscape */ $secureRenderer->renderStyleAsTag("width: 90%; float: none;", "label[for='newsletter:subscribe']") ?> +<br/> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml index baaf4c078f2c7..fd5b7a55b4960 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml @@ -4,40 +4,57 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** + * @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ +?> +<?php +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); ?> -<?php /** @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form */ ?> <?php $_shippingRateGroups = $block->getShippingRates(); ?> -<?php if ($_shippingRateGroups) : ?> - <div id="order-shipping-method-choose" class="control" style="display:none"> +<?php if ($_shippingRateGroups): ?> + <div id="order-shipping-method-choose" class="control"> <dl class="admin__order-shipment-methods"> - <?php foreach ($_shippingRateGroups as $code => $_rates) : ?> - <dt class="admin__order-shipment-methods-title"><?= $block->escapeHtml($block->getCarrierName($code)) ?></dt> + <?php foreach ($_shippingRateGroups as $code => $_rates): ?> + <dt class="admin__order-shipment-methods-title"><?= $block->escapeHtml($block->getCarrierName($code)) ?> + </dt> <dd class="admin__order-shipment-methods-options"> <ul class="admin__order-shipment-methods-options-list"> - <?php foreach ($_rates as $_rate) : ?> - <?php $_radioProperty = 'name="order[shipping_method]" type="radio" onclick="order.setShippingMethod(this.value)"' ?> + <?php foreach ($_rates as $_rate): ?> + <?php $_radioProperty = 'name="order[shipping_method]" type="radio"' ?> <?php $_code = $_rate->getCode() ?> <li class="admin__field-option"> - <?php if ($_rate->getErrorMessage()) : ?> + <?php if ($_rate->getErrorMessage()): ?> <div class="messages"> <div class="message message-error error"> <div><?= $block->escapeHtml($_rate->getErrorMessage()) ?></div> </div> </div> - <?php else : ?> + <?php else: ?> <?php $_checked = $block->isMethodActive($_code) ? 'checked="checked"' : '' ?> - <input <?= /* @noEscape */ $_radioProperty ?> value="<?= $block->escapeHtmlAttr($_code) ?>" - id="s_method_<?= $block->escapeHtmlAttr($_code) ?>" <?= /* @noEscape */ $_checked ?> - class="admin__control-radio required-entry"/> + <input <?= /* @noEscape */ $_radioProperty ?> + value="<?= $block->escapeHtmlAttr($_code) ?>" + id="s_method_<?= $block->escapeHtmlAttr($_code) ?>" <?= /* @noEscape */ $_checked ?> + class="admin__control-radio required-entry"/> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.setShippingMethod(this.value)", + 'input#s_method_' . $block->escapeHtmlAttr($_code) + ) ?> <label class="admin__field-label" for="s_method_<?= $block->escapeHtmlAttr($_code) ?>"> - <?= $block->escapeHtml($_rate->getMethodTitle() ? $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - + <?= $block->escapeHtml($_rate->getMethodTitle() ? + $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - <strong> - <?php $_excl = $block->getShippingPrice($_rate->getPrice(), $this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()); ?> + <?php $_excl = $block->getShippingPrice( + $_rate->getPrice(), + $taxHelper->displayShippingPriceIncludingTax() + ); ?> <?php $_incl = $block->getShippingPrice($_rate->getPrice(), true); ?> <?= /* @noEscape */ $_excl ?> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() && $_incl != $_excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </strong> @@ -50,57 +67,83 @@ <?php endforeach; ?> </dl> </div> - <?php if ($_rate = $block->getActiveMethodRate()) : ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none", 'div#order-shipping-method-choose') ?> + <?php if ($_rate = $block->getActiveMethodRate()): ?> <div id="order-shipping-method-info" class="order-shipping-method-info"> <dl class="admin__order-shipment-methods"> <dt class="admin__order-shipment-methods-title"> <?= $block->escapeHtml($block->getCarrierName($_rate->getCarrier())) ?> </dt> <dd class="admin__order-shipment-methods-options"> - <?= $block->escapeHtml($_rate->getMethodTitle() ? $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - + <?= $block->escapeHtml($_rate->getMethodTitle() ? + $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - <strong> - <?php $_excl = $block->getShippingPrice($_rate->getPrice(), $this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()); ?> + <?php $_excl = $block->getShippingPrice( + $_rate->getPrice(), + $taxHelper->displayShippingPriceIncludingTax() + ); ?> <?php $_incl = $block->getShippingPrice($_rate->getPrice(), true); ?> <?= /* @noEscape */ $_excl ?> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() && $_incl != $_excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </strong> </dd> </dl> <a href="#" - onclick="$('order-shipping-method-info').hide();$('order-shipping-method-choose').show();return false" class="action-default"> <span><?= $block->escapeHtml(__('Click to change shipping method')) ?></span> </a> </div> - <?php else : ?> - <script> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "$('order-shipping-method-info').hide();$('order-shipping-method-choose').show();event.preventDefault()", + 'div#order-shipping-method-info a.action-default' + ) ?> + <?php else: ?> + <?php $scriptString = <<<script require(['prototype'], function(){ $('order-shipping-method-choose').show(); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> -<?php elseif ($block->getIsRateRequest()) : ?> +<?php elseif ($block->getIsRateRequest()): ?> <div class="order-shipping-method-summary"> - <strong class="order-shipping-method-not-available"><?= $block->escapeHtml(__('Sorry, no quotes are available for this order.')) ?></strong> + <strong class="order-shipping-method-not-available"> + <?= $block->escapeHtml(__('Sorry, no quotes are available for this order.')) ?> + </strong> </div> -<?php else : ?> +<?php else: ?> <div id="order-shipping-method-summary" class="order-shipping-method-summary"> - <a href="#" onclick="order.loadShippingRates();return false" class="action-default"> + <a href="#" class="action-default"> <span><?= $block->escapeHtml(__('Get shipping methods and rates')) ?></span> </a> <input type="hidden" name="order[has_shipping]" value="" class="required-entry" /> </div> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.loadShippingRates();event.preventDefault();", + 'div#order-shipping-method-summary a.action-default' + ) ?> <?php endif; ?> -<div style="display: none;" id="shipping-method-overlay" class="order-methods-overlay"> +<div id="shipping-method-overlay" class="order-methods-overlay"> <span><?= $block->escapeHtml(__('You don\'t need to select a shipping method.')) ?></span> </div> -<script> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#shipping-method-overlay') ?> +<?php $scriptString = <<<script require(["Magento_Sales/order/create/form"], function(){ - order.overlay('shipping-method-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); - order.overlay('address-shipping-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); - order.isOnlyVirtualProduct = <?= /* @noEscape */ $block->getQuote()->isVirtual() ? 'true' : 'false'; ?>; + +script; +$scriptString .= "order.overlay('shipping-method-overlay', " . ($block->getQuote()->isVirtual() ? 'false' : 'true') . + ');' . PHP_EOL; +$scriptString .= "order.overlay('address-shipping-overlay', " . ($block->getQuote()->isVirtual() ? 'false' : 'true') . + ');' . PHP_EOL; +$scriptString .= "order.isOnlyVirtualProduct = " . ($block->getQuote()->isVirtual() ? 'true' : 'false') . ';' . PHP_EOL; +$scriptString .= <<<script }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml index d4dea4eb85a57..fe8910dc3e956 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml @@ -4,15 +4,18 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="customer-current-activity-inner"> <h4 class="customer-activity-title"><?= $block->escapeHtml(__('Customer\'s Activities')) ?></h4> <div class="create-order-sidebar-container"> <?= $block->getChildHtml('top_button') ?> - <?php foreach ($block->getLayout()->getChildBlocks($block->getNameInLayout()) as $_alias => $_child) : ?> - <?php if ($_alias != 'top_button' && $_alias != 'bottom_button') : ?> - <?php if ($block->canDisplay($_child)) : ?> + <?php foreach ($block->getLayout()->getChildBlocks($block->getNameInLayout()) as $_alias => $_child): ?> + <?php if ($_alias != 'top_button' && $_alias != 'bottom_button'): ?> + <?php if ($block->canDisplay($_child)): ?> <div class="order-sidebar-block" id="order-sidebar_<?= $block->escapeHtmlAttr($_alias) ?>"> <?= $block->getChildHtml($_alias) ?> </div> @@ -22,7 +25,7 @@ <?= $block->getChildHtml('bottom_button') ?> </div> </div> -<script> +<?php $scriptString = <<<script require([ "prototype", "Magento_Catalog/catalog/product/composite/configure" @@ -30,12 +33,12 @@ require([ function addSidebarCompositeListType() { productConfigure.addListType('sidebar', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/configureProductToAdd'))) ?>', - urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/addConfigured'))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('sales/order_create/configureProductToAdd'))}', + urlConfirm: '{$block->escapeJs($block->getUrl('sales/order_create/addConfigured'))}' }); productConfigure.addListType('sidebar_wishlist', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/wishlist_product_composite_wishlist/configure'))) ?>', - urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/addConfigured'))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('customer/wishlist_product_composite_wishlist/configure'))}', + urlConfirm: '{$block->escapeJs($block->getUrl('sales/order_create/addConfigured'))}' }); } @@ -55,4 +58,6 @@ require([ window.addSidebarCompositeListType = addSidebarCompositeListType; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml index afb58a626ada8..9b5bffcf01eef 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml @@ -3,13 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\AbstractSidebar */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$blockDataId = $block->getDataId(); +$jsEscapedBlockDataId = $block->escapeJs($blockDataId); ?> -<?php /* @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\AbstractSidebar */ ?> -<div class="create-order-sidebar-block" id="sidebar_data_<?= $block->escapeHtmlAttr($block->getDataId()) ?>"> +<div class="create-order-sidebar-block" id="sidebar_data_<?= $block->escapeHtmlAttr($blockDataId) ?>"> <div class="head sidebar-title-block"> - <a href="#" class="action-refresh" - title="<?= $block->escapeHtml(__('Refresh')) ?>" - onclick="order.loadArea('sidebar_<?= $block->escapeJs($block->getDataId()) ?>', 'sidebar_data_<?= $block->escapeJs($block->getDataId()) ?>');return false;"> + <a href="#" class="action-refresh" title="<?= $block->escapeHtml(__('Refresh')) ?>"> <span><?= $block->escapeHtml(__('Refresh')) ?></span> </a> <h5 class="create-order-sidebar-label"> @@ -17,23 +20,32 @@ <span class="normal">(<?= $block->escapeHtml($block->getItemCount()) ?>)</span> </h5> </div> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.loadArea( + 'sidebar_" . $jsEscapedBlockDataId ."', + 'sidebar_data_" . $jsEscapedBlockDataId . "' + ); + event.preventDefault();", + 'div#sidebar_data_'. $jsEscapedBlockDataId . ' a.action-refresh' + ) ?> <div class="content"> <div class="auto-scroll"> - <?php if ($block->getItemCount()) : ?> + <?php if ($block->getItemCount()): ?> <table class="admin__table-primary"> <thead> <tr> <th class="col-item"><?= $block->escapeHtml(__('Item')) ?></th> - <?php if ($block->canDisplayItemQty()) : ?> + <?php if ($block->canDisplayItemQty()): ?> <th class="col-qty"><?= $block->escapeHtml(__('Qty')) ?></th> <?php endif; ?> - <?php if ($block->canDisplayPrice()) : ?> + <?php if ($block->canDisplayPrice()): ?> <th class="col-price"><?= $block->escapeHtml(__('Price')) ?></th> <?php endif; ?> - <?php if ($block->canRemoveItems()) : ?> + <?php if ($block->canRemoveItems()): ?> <th class="col-remove"> <span title="<?= $block->escapeHtml(__('Remove')) ?>" class="icon icon-remove"> @@ -52,33 +64,37 @@ </thead> <tbody> - <?php foreach ($block->getItems() as $_item) : ?> - <tr> + <?php foreach ($block->getItems() as $_item): ?> + <tr id="product-id-<?= (int) $block->getProductId($_item) ?>"> <td class="col-item"><?= $block->escapeHtml($_item->getName()) ?></td> - <?php if ($block->canDisplayItemQty()) : ?> + <?php if ($block->canDisplayItemQty()): ?> <td class="col-qty"> <?= (float) $block->getItemQty($_item) ?> </td> <?php endif; ?> - <?php if ($block->canDisplayPrice()) : ?> + <?php if ($block->canDisplayPrice()): ?> <td class="col-price"> <?= /* @noEscape */ $block->getItemPrice($block->getProduct($_item)) ?> </td> <?php endif; ?> - <?php if ($block->canRemoveItems()) : ?> + <?php if ($block->canRemoveItems()): ?> <td class="col-remove"> <div class="admin__field-option"> - <input id="sidebar-remove-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getItemId($_item) ?>" + <input id="sidebar-remove-<?= + $block->escapeHtmlAttr($block->getSidebarStorageAction()) + ?>-<?= (int) $block->getItemId($_item) ?>" type="checkbox" class="admin__control-checkbox" name="sidebar[remove][<?= (int) $block->getItemId($_item) ?>]" - value="<?= $block->escapeHtmlAttr($block->getDataId()) ?>" + value="<?= $block->escapeHtmlAttr($blockDataId) ?>" title="<?= $block->escapeHtml(__('Remove')) ?>" /> <label class="admin__field-label" - for="sidebar-remove-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getItemId($_item) ?>"> + for="sidebar-remove-<?= + $block->escapeHtmlAttr($block->getSidebarStorageAction()) + ?>-<?= (int) $block->getItemId($_item) ?>"> </label> </div> </td> @@ -86,29 +102,44 @@ <td class="col-add"> <div class="admin__field-option"> - <?php if ($block->isConfigurationRequired($_item->getTypeId()) && $block->getDataId() == 'wishlist') : ?> + <?php if ($block->isConfigurationRequired($_item->getTypeId()) && + $blockDataId == 'wishlist'): ?> <a href="#" class="icon icon-configure" - title="<?= $block->escapeHtml(__('Configure and Add to Order')) ?>" - onclick="order.sidebarConfigureProduct('sidebar_wishlist', <?= (int) $block->getProductId($_item) ?>, <?= (int) $block->getItemId($_item) ?>); return false;"> + title="<?= $block->escapeHtml(__('Configure and Add to Order')) ?>"> <span><?= $block->escapeHtml(__('Configure and Add to Order')) ?></span> </a> - <?php elseif ($block->isConfigurationRequired($_item->getTypeId())) : ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.sidebarConfigureProduct('sidebar_wishlist', " . + (int) $block->getProductId($_item) . ", " . (int) $block->getItemId($_item) . + ");event.preventDefault();", + 'tr#product-id-' . (int) $block->getProductId($_item) .' a.icon.icon-configure' + ) ?> + <?php elseif ($block->isConfigurationRequired($_item->getTypeId())): ?> <a href="#" class="icon icon-configure" - title="<?= $block->escapeHtml(__('Configure and Add to Order')) ?>" - onclick="order.sidebarConfigureProduct('sidebar', <?= (int) $block->getProductId($_item) ?>); return false;"> + title="<?= $block->escapeHtml(__('Configure and Add to Order')) ?>"> <span><?= $block->escapeHtml(__('Configure and Add to Order')) ?></span> </a> - <?php else : ?> - <input id="sidebar-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getIdentifierId($_item) ?>" + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.sidebarConfigureProduct('sidebar', " . + (int) $block->getProductId($_item) . ");event.preventDefault();", + 'tr#product-id-' . (int) $block->getProductId($_item) . ' a.icon.icon-configure' + ) ?> + <?php else: ?> + <input id="sidebar-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) + ?>-<?= (int) $block->getIdentifierId($_item) ?>" type="checkbox" class="admin__control-checkbox" - name="sidebar[<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>][<?= (int) $block->getIdentifierId($_item) ?>]" + name="sidebar[<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) + ?>][<?= (int) $block->getIdentifierId($_item) ?>]" value="<?= $block->canDisplayItemQty() ? (float) $_item->getQty() : 1 ?>" title="<?= $block->escapeHtml(__('Add To Order')) ?>"/> <label class="admin__field-label" - for="sidebar-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getIdentifierId($_item) ?>"> + for="sidebar-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) + ?>-<?= (int) $block->getIdentifierId($_item) ?>"> </label> <?php endif; ?> </div> @@ -117,11 +148,11 @@ <?php endforeach; ?> </tbody> </table> - <?php else : ?> + <?php else: ?> <span class="no-items"><?= $block->escapeHtml(__('No items')) ?></span> <?php endif ?> </div> - <?php if ($block->getItemCount() && $block->canRemoveItems()) : ?> + <?php if ($block->getItemCount() && $block->canRemoveItems()): ?> <?= $block->getChildHtml('empty_customer_cart_button') ?> <?php endif; ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml index 407bd0272e9fd..1c21d51a2df32 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml @@ -3,17 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Store\Select */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /* @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Store\Select */ ?> <div class="store-scope form-inline"> <div class="admin__fieldset tree-store-scope"> <?php $showHelpHint = 0; ?> - <?php foreach ($block->getWebsiteCollection() as $_website) : ?> + <?php foreach ($block->getWebsiteCollection() as $_website): ?> <?php $showWebsite = false; ?> - <?php foreach ($block->getGroupCollection($_website) as $_group) : ?> + <?php foreach ($block->getGroupCollection($_website) as $_group): ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStoreCollection($_group) as $_store) : ?> - <?php if ($showWebsite == false) : ?> + <?php foreach ($block->getStoreCollection($_group) as $_store): ?> + <?php if ($showWebsite == false): ?> <?php $showWebsite = true; ?> <div class="admin__field field-website_label"> <label class="admin__field-label" for=""> @@ -21,7 +23,7 @@ </label> <div class="admin__field-control"> <div class="admin__field admin__field-option"> - <?php if ($showHelpHint == 0) : + <?php if ($showHelpHint == 0): echo $block->getHintHtml(); $showHelpHint = 1; endif; ?> @@ -30,29 +32,39 @@ </div> <?php endif; ?> - <?php if ($showGroup == false) : ?> + <?php if ($showGroup == false): ?> <?php $showGroup = true; ?> <div class="admin__field field-group_label"> - <label class="admin__field-label" for=""><span><?= $block->escapeHtml($_group->getName()) ?></span></label> + <label class="admin__field-label" for=""> + <span><?= $block->escapeHtml($_group->getName()) ?></span> + </label> <div class="admin__field-control"></div> </div> <?php endif; ?> <div class="admin__field field-store_label"> - <label class="admin__field-label" for=""><span><?= $block->escapeHtml($_group->getName()) ?></span></label> + <label class="admin__field-label" for=""> + <span><?= $block->escapeHtml($_group->getName()) ?></span> + </label> <div class="admin__field-control"> <div class="nested"> <div class="admin__field admin__field-option"> - <input type="radio" id="store_<?= (int) $_store->getId() ?>" class="admin__control-radio" onclick="order.setStoreId('<?= (int) $_store->getId() ?>')"/> + <input type="radio" + id="store_<?= (int) $_store->getId() ?>" class="admin__control-radio"/> <label class="admin__field-label" for="store_<?= (int) $_store->getId() ?>"> <?= $block->escapeHtml($_store->getName()) ?> </label> + <?= /* @noEscape*/ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.setStoreId('" . (int)$_store->getId() . "')", + 'input#store_' . (int)$_store->getId() + ) ?> </div> </div> </div> </div> <?php endforeach; ?> - <?php if ($showGroup) : ?> + <?php if ($showGroup): ?> <?php endif; ?> <?php endforeach; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml index 9a901d99ae8f8..73f53c2eba03e 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Totals $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Totals $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <legend class="admin__legend"><span><?= $block->escapeHtml(__('Order Totals')) ?></span></legend> <br> @@ -19,15 +22,19 @@ <div class="order-totals-actions"> <div class="admin__field admin__field-option field-append-comments"> <input type="checkbox" id="notify_customer" name="order[comment][customer_note_notify]" - value="1"<?php if ($block->getNoteNotify()) : ?> checked="checked"<?php endif; ?> + value="1"<?php if ($block->getNoteNotify()): ?> checked="checked"<?php endif; ?> class="admin__control-checkbox"/> - <label for="notify_customer" class="admin__field-label"><?= $block->escapeHtml(__('Append Comments')) ?></label> + <label for="notify_customer" class="admin__field-label"> + <?= $block->escapeHtml(__('Append Comments')) ?> + </label> </div> - <?php if ($block->canSendNewOrderConfirmationEmail()) : ?> + <?php if ($block->canSendNewOrderConfirmationEmail()): ?> <div class="admin__field admin__field-option field-email-order-confirmation"> <input type="checkbox" id="send_confirmation" name="order[send_confirmation]" value="1" checked="checked" class="admin__control-checkbox"/> - <label for="send_confirmation" class="admin__field-label"><?= $block->escapeHtml(__('Email Order Confirmation')) ?></label> + <label for="send_confirmation" class="admin__field-label"> + <?= $block->escapeHtml(__('Email Order Confirmation')) ?> + </label> </div> <?php endif; ?> <div class="actions"> @@ -35,7 +42,7 @@ </div> </div> -<script> +<?php $scriptString = <<<script require(['prototype'], function(){ //<![CDATA[ @@ -58,4 +65,6 @@ window.notifyCustomerUpdate = notifyCustomerUpdate; window.sendEmailCheckbox = sendEmailCheckbox; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml index 4a55eb609924f..7462e8ac1f87d 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml @@ -3,16 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<tr class="<?= $block->escapeHtmlAttr($block->getTotal()->getCode()) ?> row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?><strong><?php endif; ?> +<tr id="totals-default" class="<?= $block->escapeHtmlAttr($block->getTotal()->getCode()) ?> row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?><strong><?php endif; ?> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?></strong><?php endif; ?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?></strong><?php endif; ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-amount"> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?><strong><?php endif; ?> + <td class="admin__total-amount"> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?><strong><?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?></strong><?php endif; ?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?></strong><?php endif; ?> </td> </tr> +<?php if ($block->escapeHtmlAttr($block->getTotal()->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#totals-default td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#totals-default td.admin__total-amount' + ) ?> +<?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml index 4c4f94b5b3bb1..1ff1c68baaa52 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml @@ -6,34 +6,65 @@ /** * @var $block \Magento\Tax\Block\Checkout\Grandtotal + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Tax\Block\Checkout\Grandtotal */ ?> -<?php if ($block->includeTax() && $block->getTotalExclTax() >= 0) : ?> - <tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> +<?php if ($block->includeTax() && $block->getTotalExclTax() >= 0): ?> + <tr id="grand-total-exclude-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <strong><?= $block->escapeHtml(__('Grand Total Excl. Tax')) ?></strong> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <strong><?= /* @noEscape */ $block->formatPrice($block->getTotalExclTax()) ?></strong> </td> </tr> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-exclude-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-exclude-tax td.admin__total-amount' + ) ?> + <?php endif; ?> <?= /* @noEscape */ $block->renderTotals('taxes', $block->getColspan()) ?> - <tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <tr id="grand-total-include-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <strong><?= $block->escapeHtml(__('Grand Total Incl. Tax')) ?></strong> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <strong><?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?></strong> </td> </tr> - <?php else : ?> - <tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-include-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-include-tax td.admin__total-amount' + ) ?> + <?php endif; ?> + <?php else: ?> + <tr id="grand-total" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <strong><?= $block->escapeHtml($block->getTotal()->getTitle()) ?></strong> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <strong><?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?></strong> </td> </tr> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-include-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-include-tax td.admin__total-amount' + ) ?> + <?php endif; ?> <?php endif;?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml index db204a46f1f94..c842901f7c16a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml @@ -6,42 +6,83 @@ /** * @var $block \Magento\Tax\Block\Checkout\Shipping + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Tax\Block\Checkout\Shipping */ ?> -<?php if ($block->displayBoth()) :?> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> +<?php if ($block->displayBoth()):?> +<tr id="shipping-exclude-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getExcludeTaxLabel()) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getShippingExcludeTax()) ?> </td> </tr> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-exclude-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-exclude-tax td.admin__total-amount' + ) ?> + <?php endif; ?> +<tr id="shipping-include-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getIncludeTaxLabel()) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getShippingIncludeTax()) ?> </td> </tr> -<?php elseif ($block->displayIncludeTax()) : ?> -<tr> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-include-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-include-tax td.admin__total-amount' + ) ?> + <?php endif; ?> +<?php elseif ($block->displayIncludeTax()): ?> +<tr id="shipping-include-tax"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getShippingIncludeTax()) ?> </td> </tr> -<?php else : ?> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-include-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-include-tax td.admin__total-amount' + ) ?> + <?php endif; ?> +<?php else: ?> +<tr id="shipping-exclude-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getShippingExcludeTax()) ?> </td> </tr> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-exclude-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-exclude-tax td.admin__total-amount' + ) ?> + <?php endif; ?> <?php endif;?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml index a63458491baea..91ba11f90ba9a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml @@ -6,33 +6,64 @@ /** * @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Subtotal + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Subtotal */ ?> -<?php if ($block->displayBoth()) : ?> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> +<?php if ($block->displayBoth()): ?> +<tr id="subtotal-exclude-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml(__('Subtotal (Excl. Tax)')) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValueExclTax()) ?> </td> </tr> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-exclude-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-exclude-tax td.admin__total-amount' + ) ?> + <?php endif; ?> +<tr id="subtotal-include-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml(__('Subtotal (Incl. Tax)')) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValueInclTax()) ?> </td> </tr> -<?php else : ?> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-include-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-include-tax td.admin__total-amount' + ) ?> + <?php endif; ?> +<?php else: ?> +<tr id="subtotal-total" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?> </td> </tr> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-total td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-total td.admin__total-amount' + ) ?> + <?php endif; ?> <?php endif;?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml index 042b2f5113cac..5c39449d79840 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml @@ -4,56 +4,91 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate // phpcs:disable Squiz.PHP.GlobalKeyword.NotAllowed +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Tax $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Tax $block */ - +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); $taxAmount = $block->getTotal()->getValue(); ?> -<?php if (($taxAmount == 0 && $this->helper(\Magento\Tax\Helper\Data::class)->displayZeroTax()) || ($taxAmount > 0)) : +<?php if (($taxAmount == 0 && $taxHelper->displayZeroTax()) || ($taxAmount > 0)): global $taxIter; $taxIter++; ?> - <?php $class = $block->escapeHtmlAttr("{$block->getTotal()->getCode()} " . ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary() ? 'summary-total' : '')); ?> - <tr<?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> - onclick="expandDetails(this, '.summary-details-<?= $block->escapeJs($taxIter) ?>')" - <?php endif; ?> + <?php $class = $block->escapeHtmlAttr("{$block->getTotal()->getCode()} " . ($taxHelper->displayFullSummary() ? + 'summary-total' : '')); ?> + <tr id="tax-summary-<?= $block->escapeHtmlAttr($taxIter) ?>" class="<?= /* @noEscape */ $class ?> row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($taxHelper->displayFullSummary()): ?> <div class="summary-collapse"><?= $block->escapeHtml($block->getTotal()->getTitle()) ?></div> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> <?php endif;?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?> </td> </tr> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + <?php if ($taxHelper->displayFullSummary()): ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "expandDetails(this, '.summary-details-" . $block->escapeJs($taxIter) ."')", + 'tr#tax-summary-' . $block->escapeHtmlAttr($taxIter) + ) ?> + <?php endif; ?> + <?php if ($block->escapeHtmlAttr($block->getTotal()->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#tax-summary td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#tax-summary td.admin__total-amount' + ) ?> + <?php endif; ?> + <?php if ($taxHelper->displayFullSummary()): ?> <?php $isTop = 1; ?> - <?php foreach ($block->getTotal()->getFullInfo() as $info) : ?> - <?php if (isset($info['hidden']) && $info['hidden']) : + <?php foreach ($block->getTotal()->getFullInfo() as $info): ?> + <?php if (isset($info['hidden']) && $info['hidden']): continue; endif; ?> <?php $percent = $info['percent']; ?> <?php $amount = $info['amount']; ?> <?php $rates = $info['rates']; ?> - <?php foreach ($rates as $rate) : ?> - <tr class="summary-details-<?= $block->escapeHtmlAttr($taxIter) ?> summary-details<?= ($isTop ? ' summary-details-first' : '') ?>" style="display:none;"> - <td class="admin__total-mark" style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" colspan="<?= (int) $block->getColspan() ?>"> + <?php foreach ($rates as $rate): ?> + <tr id="tax-summary-details-<?= $block->escapeHtmlAttr($taxIter) ?>" + class="summary-details-<?= $block->escapeHtmlAttr($taxIter) ?> + summary-details<?= ($isTop ? ' summary-details-first' : '') ?>"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($rate['title']) ?> - <?php if ($rate['percent'] !== null) : ?> + <?php if ($rate['percent'] !== null): ?> (<?= (float) $rate['percent'] ?>%) <?php endif; ?> <br /> </td> - <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice(($amount*(float)$rate['percent'])/$percent) ?> </td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none;", + 'tr#tax-summary-details-' . $block->escapeHtmlAttr($taxIter) + ) ?> + <?php if ($block->escapeHtmlAttr($block->getTotal()->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#tax-summary-details-' . $block->escapeHtmlAttr($taxIter) . ' td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#tax-summary-details-' . $block->escapeHtmlAttr($taxIter) . ' td.admin__total-amount' + ) ?> + <?php endif; ?> <?php $isTop = 0; ?> <?php endforeach; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml index 2bb085a51e377..8d50e3103b8a1 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml @@ -5,6 +5,7 @@ */ /* @var \Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Items $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php /** @var Magento\Sales\ViewModel\CreditMemo\Create\UpdateTotalsButton $viewModel */ @@ -142,7 +143,8 @@ $commentText = $block->getCreditmemo()->getCommentText(); </div> </section> -<script> +<?php $scriptString = <<<script + require(['jquery'], function(jQuery){ //<![CDATA[ @@ -220,4 +222,6 @@ window.checkButtonsRelation = checkButtonsRelation; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml index fc624bfd803b6..7aa264b9fd04c 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_source = $block->getSource() ?> <?php if ($_source): ?> @@ -43,14 +45,14 @@ value="<?= /* @noEscape */ $block->formatValue($_source->getBaseAdjustmentNegative()) ?>" class="input-text admin__control-text not-negative-amount" id="adjustment_negative"/> - <script> + <?php $scriptString = <<<script require(['prototype'], function(){ //<![CDATA[ Validation.addAllThese([ [ 'not-negative-amount', - '<?= $block->escapeJs(__('Please enter a positive number in this field.')) ?>', + '{$block->escapeJs(__('Please enter a positive number in this field.'))}', function (v) { if (v.length) return /^\s*\d+([,.]\d+)*\s*%?\s*$/.test(v); @@ -86,7 +88,9 @@ //]]> }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </td> </tr> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml index 70373f177d8be..10c44cf99471b 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml @@ -4,85 +4,165 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - -/* -store view name = $_order->getStore()->getName() -web site name = $_order->getStore()->getWebsite()->getName() -store name = $_order->getStore()->getGroup()->getName() -*/ - /* @var \Magento\Sales\Block\Adminhtml\Order\Details $block */ -?> -<?php +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + /* @var \Magento\Sales\Model\Order $_order */ -$_order = $block->getOrder() ?> +$_order = $block->getOrder(); +/** @var \Magento\GiftMessage\Helper\Message $giftMessageHelper */ +$giftMessageHelper = $block->getData('giftMessageHelper'); +?> <div> -<?= $block->escapeHtml(__('Customer Name: %1', $_order->getCustomerFirstname() ? $_order->getCustomerName() : $_order->getBillingAddress()->getName())) ?><br /> +<?= $block->escapeHtml(__('Customer Name: %1', $_order->getCustomerFirstname() ? $_order->getCustomerName() : + $_order->getBillingAddress()->getName())) ?><br /> <?= $block->escapeHtml(__('Purchased From: %1', $_order->getStore()->getGroup()->getName())) ?><br /> </div> -<table cellpadding="0" border="0" width="100%" style="border:1px solid #bebcb7; background:#f8f7f5;"> +<table id="order-details" cellpadding="0" border="0" width="100%"> <thead> <tr> - <th align="left" bgcolor="#d9e5ee" style="padding:3px 9px">Item</th> - <th align="center" bgcolor="#d9e5ee" style="padding:3px 9px">Qty</th> - <th align="right" bgcolor="#d9e5ee" width="10%" style="padding:3px 9px">Subtotal</th> + <th align="left" bgcolor="#d9e5ee">Item</th> + <th align="center" bgcolor="#d9e5ee">Qty</th> + <th align="right" bgcolor="#d9e5ee" width="10%">Subtotal</th> </tr> </thead> <tbody> -<?php $i = 0; foreach ($_order->getAllItems() as $_item) : $i++ ?> - <tr <?= $i%2 ? 'bgcolor="#eeeded"' : '' ?>> - <td align="left" valign="top" style="padding:3px 9px"><strong><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_item->getGiftMessageId() && $_giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class)->getGiftMessage($_item->getGiftMessageId())) : ?> +<?php $i = 0; foreach ($_order->getAllItems() as $_item): $i++ ?> + <tr id="item-<?= /* @noEscape */ $i ?>" <?= $i%2 ? 'bgcolor="#eeeded"' : '' ?>> + <td align="left" valign="top"><strong><?= $block->escapeHtml($_item->getName()) ?></strong> + <?php if ($_item->getGiftMessageId() && + $_giftMessage = $giftMessageHelper->getGiftMessage($_item->getGiftMessageId())): ?> <br /><strong><?= $block->escapeHtml(__('Gift Message')) ?></strong> <br /><?= $block->escapeHtml(__('From:')) ?> <?= $block->escapeHtml($_giftMessage->getSender()) ?> <br /><?= $block->escapeHtml(__('To:')) ?> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> <br /><?= $block->escapeHtml(__('Message:')) ?><br /> <?= $block->escapeHtml($_giftMessage->getMessage()) ?> <?php endif; ?> </td> - <td align="center" valign="top" style="padding:3px 9px"><?= (float) $_item->getQtyOrdered() ?></td> - <td align="right" valign="top" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_item->getRowTotal()) ?></td> + <td align="center" valign="top"><?= (float) $_item->getQtyOrdered() ?></td> + <td align="right" valign="top"><?= /* @noEscape */ $_order->formatPrice($_item->getRowTotal()) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'tr#item-' . $i + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'tr#item-' . $i . ' td:nth-child(1)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'tr#item-' . $i . ' td:nth-child(2)' + ) ?> <?php endforeach; ?> </tbody> <tfoot> - <?php if ($_order->getGiftMessageId() && $_giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class)->getGiftMessage($_order->getGiftMessageId())) : ?> - <tr> - <td colspan="3" align="left" style="padding:3px 9px"> + <?php if ($_order->getGiftMessageId() && + $_giftMessage = $giftMessageHelper->getGiftMessage($_order->getGiftMessageId())): ?> + <tr id="gift-message"> + <td colspan="3" align="left"> <strong><?= $block->escapeHtml(__('Gift Message')) ?></strong> <br /><?= $block->escapeHtml(__('From:')) ?> <?= $block->escapeHtml($_giftMessage->getSender()) ?> <br /><?= $block->escapeHtml(__('To:')) ?> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> <br /><?= $block->escapeHtml(__('Message:')) ?><br /> <?= $block->escapeHtml($_giftMessage->getMessage()) ?> </td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#gift-message td' + ) ?> <?php endif; ?> - <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml(__('Subtotal')) ?></td> - <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_order->getSubtotal()) ?></td> + <tr id="subtotal"> + <td colspan="2" align="right"><?= $block->escapeHtml(__('Subtotal')) ?></td> + <td align="right"><?= /* @noEscape */ $_order->formatPrice($_order->getSubtotal()) ?></td> </tr> - <?php if ($_order->getDiscountAmount() > 0) : ?> - <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml((($_order->getCouponCode()) ? __('Discount (%1)', $_order->getCouponCode()) : __('Discount'))) ?></td> - <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice(0.00 - $_order->getDiscountAmount()) ?></td> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#subtotal td:nth-child(0)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#subtotal td:nth-child(1)' + ) ?> + + <?php if ($_order->getDiscountAmount() > 0): ?> + <tr id="discount"> + <td colspan="2" align="right"><?= $block->escapeHtml((($_order->getCouponCode()) ? + __('Discount (%1)', $_order->getCouponCode()) : __('Discount'))) ?></td> + <td align="right"><?= /* @noEscape */ $_order->formatPrice(0.00 - $_order->getDiscountAmount()) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#discount td:nth-child(0)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#discount td:nth-child(1)' + ) ?> <?php endif; ?> - <?php if ($_order->getShippingAmount() || $_order->getShippingDescription()) : ?> - <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml(__('Shipping & Handling')) ?></td> - <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_order->getShippingAmount()) ?></td> + <?php if ($_order->getShippingAmount() || $_order->getShippingDescription()): ?> + <tr id="shipping"> + <td colspan="2" align="right"><?= $block->escapeHtml(__('Shipping & Handling')) ?></td> + <td align="right"><?= /* @noEscape */ $_order->formatPrice($_order->getShippingAmount()) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#shipping td:nth-child(0)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#shipping td:nth-child(1)' + ) ?> <?php endif; ?> - <?php if ($_order->getTaxAmount() > 0) : ?> - <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml(__('Tax')) ?></td> - <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_order->getTaxAmount()) ?></td> + <?php if ($_order->getTaxAmount() > 0): ?> + <tr id="tax"> + <td colspan="2" align="right"><?= $block->escapeHtml(__('Tax')) ?></td> + <td align="right"><?= /* @noEscape */ $_order->formatPrice($_order->getTaxAmount()) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#tax td:nth-child(0)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#tax td:nth-child(1)' + ) ?> <?php endif; ?> - <tr bgcolor="#DEE5E8"> - <td colspan="2" align="right" style="padding:3px 9px"><strong style="font-size: larger"><?= $block->escapeHtml(__('Grand Total')) ?></strong></td> - <td align="right" style="padding:6px 9px"><strong style="font-size: larger"><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></strong></td> + <tr id="grand-total" bgcolor="#DEE5E8"> + <td colspan="2" align="right"><strong><?= $block->escapeHtml(__('Grand Total')) ?></strong></td> + <td align="right"><strong><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal())?></strong></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#grand-total td:nth-child(0)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'font-size: larger', + 'table#order-details tr#grand-total td:nth-child(0) strong' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#grand-total td:nth-child(1)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'font-size: larger', + 'table#order-details tr#grand-total td:nth-child(1) strong' + ) ?> </tfoot> </table> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "border:1px solid #bebcb7; background:#f8f7f5;", + 'table#order-details' +) ?> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details thead tr th:nth-child(0)' +) ?> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details thead tr th:nth-child(1)' +) ?> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details thead tr th:nth-child(2)' +) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml index da9f0d273af24..cf2311de5dbb0 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml @@ -4,13 +4,16 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /* @var \Magento\Sales\Block\Adminhtml\Order\Invoice\Create\Form $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form id="edit_form" class="order-invoice-edit" method="post" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>"> <?= $block->getBlockHtml('formkey') ?> <?php $_order = $block->getInvoice()->getOrder() ?> + <?php + /** @var \Magento\Tax\Helper\Data $taxHelper */ + $taxHelper = $block->getData('taxHelper'); + ?> <?= $block->getChildHtml('order_info') ?> <section class="admin__page-section"> @@ -18,17 +21,20 @@ <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')) ?></span> </div> <div class="admin__page-section-content"> - <div class="admin__page-section-item order-payment-method<?php if ($_order->getIsVirtual()) : ?> order-payment-method-virtual<?php endif; ?>"> + <div class="admin__page-section-item order-payment-method + <?php if ($_order->getIsVirtual()): ?> order-payment-method-virtual<?php endif; ?>"> <div class="admin__page-section-item-title"> <span class="title"><?= $block->escapeHtml(__('Payment Information')) ?></span> </div> <div class="admin__page-section-item-content"> <div class="order-payment-method-title"><?= $block->getChildHtml('order_payment') ?></div> - <div class="order-payment-currency"><?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?></div> + <div class="order-payment-currency"> + <?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?> + </div> <div class="order-payment-additional"><?= $block->getChildHtml('order_payment_additional') ?></div> </div> </div> - <?php if (!$_order->getIsVirtual()) : ?> + <?php if (!$_order->getIsVirtual()): ?> <div class="admin__page-section-item order-shipping-address"> <?php /*Shipping Address */ ?> <div class="admin__page-section-item-title"> @@ -36,35 +42,45 @@ </div> <div class="admin__page-section-item-content"> <div class="shipping-description-wrapper"> - <div class="shipping-description-title"><?= $block->escapeHtml($_order->getShippingDescription()) ?></div> + <div class="shipping-description-title"> + <?= $block->escapeHtml($_order->getShippingDescription()) ?></div> <div class="shipping-description-content"> <?= $block->escapeHtml(__('Total Shipping Charges')) ?>: - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + <?php if ($taxHelper->displayShippingPriceIncludingTax()): ?> <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> - <?php else : ?> + <?php else: ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> <?= /* @noEscape */ $_excl ?> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() && $_incl != $_excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </div> </div> - <?php if ($block->canCreateShipment() && $block->canShipPartiallyItem()) : ?> + <?php if ($block->canCreateShipment() && $block->canShipPartiallyItem()): ?> <div class="admin__field admin__field-option"> <input type="checkbox" name="invoice[do_shipment]" id="invoice_do_shipment" value="1" - class="admin__control-checkbox" <?= $block->hasInvoiceShipmentTypeMismatch() ? ' disabled="disabled"' : '' ?> /> + class="admin__control-checkbox" + <?= $block->hasInvoiceShipmentTypeMismatch() ? ' disabled="disabled"' : '' ?> /> <label for="invoice_do_shipment" - class="admin__field-label"><span><?= $block->escapeHtml(__('Create Shipment')) ?></span></label> + class="admin__field-label"> + <span><?= $block->escapeHtml(__('Create Shipment')) ?></span> + </label> </div> - <?php if ($block->hasInvoiceShipmentTypeMismatch()) : ?> - <small><?= $block->escapeHtml(__('Invoice and shipment types do not match for some items on this order. You can create a shipment only after creating the invoice.')) ?></small> + <?php if ($block->hasInvoiceShipmentTypeMismatch()): ?> + <small> + <?= $block->escapeHtml(__( + 'Invoice and shipment types do not match for some items on this order. ' . + 'You can create a shipment only after creating the invoice.' + )) ?> + </small> <?php endif; ?> <?php endif; ?> - <div id="tracking" style="display:none;"><?= $block->getChildHtml('tracking', false) ?></div> + <div id="tracking"><?= $block->getChildHtml('tracking', false) ?></div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'div#tracking') ?> </div> </div> <?php endif; ?> @@ -75,7 +91,10 @@ <?= $block->getChildHtml('order_items') ?> </section> </form> -<script> + +<?php $forcedShipmentCreate = (int) $block->getForcedShipmentCreate(); +$scriptString = <<<script + require(['prototype'], function(){ //<![CDATA[ @@ -91,7 +110,7 @@ require(['prototype'], function(){ } /*forced creating of shipment*/ - var forcedShipmentCreate = <?= (int) $block->getForcedShipmentCreate() ?>; + var forcedShipmentCreate = {$forcedShipmentCreate}; var shipmentElement = $('invoice_do_shipment'); if (forcedShipmentCreate && shipmentElement) { shipmentElement.checked = true; @@ -105,4 +124,6 @@ require(['prototype'], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml index 9837a6b3c209b..37805a68a603f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Sales\Block\Adminhtml\Order\Invoice\Create\Items $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <section class="admin__page-section"> <div class="admin__page-section-title"> - <?php $_itemsGridLabel = $block->getForcedShipmentCreate() ? 'Items to Invoice and Ship' : 'Items to Invoice'; ?> + <?php $_itemsGridLabel = $block->getForcedShipmentCreate() ? 'Items to Invoice and Ship' : 'Items to Invoice';?> <span class="title"><?= $block->escapeHtml(__('%1', $_itemsGridLabel)) ?></span> </div> <div class="admin__page-section-content grid"> @@ -24,7 +27,7 @@ <th class="col-total last"><span><?= $block->escapeHtml(__('Row Total')) ?></span></th> </tr> </thead> - <?php if ($block->canEditQty()) : ?> + <?php if ($block->canEditQty()): ?> <tfoot> <tr> <td colspan="3"> </td> @@ -34,10 +37,10 @@ </tfoot> <?php endif; ?> <?php $_items = $block->getInvoice()->getAllItems() ?> - <?php $_i = 0; foreach ($_items as $_item) : ?> - <?php if ($_item->getOrderItem()->getParentItem()) : + <?php $_i = 0; foreach ($_items as $_item): ?> + <?php if ($_item->getOrderItem()->getParentItem()): continue; - else : + else: $_i++; endif; ?> <tbody class="<?= /* @noEscape */ $_i%2 ? 'even' : 'odd' ?>"> @@ -52,7 +55,7 @@ <?php $orderTotalBar = $block->getChildHtml('order_totalbar'); ?> -<?php if (!empty($orderTotalBar)) : ?> +<?php if (!empty($orderTotalBar)): ?> <section class="admin__page-section"> <?= /* @noEscape */ $orderTotalBar ?> </section> @@ -73,8 +76,11 @@ <span><?= $block->escapeHtml(__('Invoice Comments')) ?></span> </label> <div class="admin__field-control"> - <textarea id="invoice_comment_text" name="invoice[comment_text]" class="admin__control-textarea" - rows="3" cols="5"><?= $block->escapeHtml($block->getInvoice()->getCommentText()) ?></textarea> + <textarea id="invoice_comment_text" + name="invoice[comment_text]" + class="admin__control-textarea" + rows="3" + cols="5"><?= $block->escapeHtml($block->getInvoice()->getCommentText())?></textarea> </div> </div> </div> @@ -86,37 +92,41 @@ </div> <div class="admin__page-section-item-content order-totals-actions"> <?= $block->getChildHtml('invoice_totals') ?> - <?php if ($block->isCaptureAllowed()) : ?> - <?php if ($block->canCapture()) : ?> + <?php if ($block->isCaptureAllowed()): ?> + <?php if ($block->canCapture()): ?> <div class="admin__field"> - <?php - /* - <label for="invoice_do_capture" class="normal"><?= __('Capture Amount') ?></label> - <input type="checkbox" name="invoice[do_capture]" id="invoice_do_capture" value="1" checked/> - */ - ?> - <label for="invoice_do_capture" class="admin__field-label"><?= $block->escapeHtml(__('Amount')) ?></label> + <label for="invoice_do_capture" class="admin__field-label"> + <?= $block->escapeHtml(__('Amount')) ?> + </label> <select class="admin__control-select" name="invoice[capture_case]"> <option value="online"><?= $block->escapeHtml(__('Capture Online')) ?></option> <option value="offline"><?= $block->escapeHtml(__('Capture Offline')) ?></option> <option value="not_capture"><?= $block->escapeHtml(__('Not Capture')) ?></option> </select> </div> - <?php elseif ($block->isGatewayUsed()) :?> + <?php elseif ($block->isGatewayUsed()):?> <input type="hidden" name="invoice[capture_case]" value="offline"/> - <div><?= $block->escapeHtml(__('The invoice will be created offline without the payment gateway.')) ?></div> + <div> + <?= $block->escapeHtml(__( + 'The invoice will be created offline without the payment gateway.' + )) ?> + </div> <?php endif; ?> <?php endif; ?> <div class="admin__field admin__field-option field-append"> <input id="notify_customer" name="invoice[comment_customer_notify]" value="1" type="checkbox" class="admin__control-checkbox" /> - <label class="admin__field-label" for="notify_customer"><?= $block->escapeHtml(__('Append Comments')) ?></label> + <label class="admin__field-label" for="notify_customer"> + <?= $block->escapeHtml(__('Append Comments')) ?> + </label> </div> - <?php if ($block->canSendInvoiceEmail()) : ?> + <?php if ($block->canSendInvoiceEmail()): ?> <div class="admin__field admin__field-option field-email"> <input id="send_email" name="invoice[send_email]" value="1" type="checkbox" class="admin__control-checkbox" /> - <label class="admin__field-label" for="send_email"><?= $block->escapeHtml(__('Email Copy of Invoice')) ?></label> + <label class="admin__field-label" for="send_email"> + <?= $block->escapeHtml(__('Email Copy of Invoice')) ?> + </label> </div> <?php endif; ?> <?= $block->getChildHtml('submit_before') ?> @@ -129,13 +139,16 @@ </div> </section> -<script> +<?php +$enableSubmitButton = (int) !$block->getDisableSubmitButton(); +$scriptString = <<<script + require(['jquery'], function(jQuery){ //<![CDATA[ var submitButtons = jQuery('.submit-button'); var updateButtons = jQuery('.update-button'); -var enableSubmitButtons = <?= (int) !$block->getDisableSubmitButton() ?>; +var enableSubmitButtons = {$enableSubmitButton}; var fields = jQuery('.qty-input'); function enableButtons(buttons) { @@ -193,4 +206,6 @@ window.checkButtonsRelation = checkButtonsRelation; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml index f8e914a2c9b2f..7dc009a9bc662 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml @@ -3,10 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->getCanDisplayTotalDue()) : ?> -<tr> - <td class="label"><strong style="font-size: larger"><?= $block->escapeHtml(__('Total Due')) ?></strong></td> - <td class="emph" style="font-size: larger"><?= /* @noEscape */ $block->displayPriceAttribute('total_due', true) ?></td> +<?php if ($block->getCanDisplayTotalDue()): ?> +<tr id="total-due"> + <td class="label"><strong><?= $block->escapeHtml(__('Total Due')) ?></strong></td> + + <td class="emph"><?= /* @noEscape */ $block->displayPriceAttribute('total_due', true) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('font-size: larger', 'tr.total-due td.label strong') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('font-size: larger', 'tr.total-due td.emph') ?> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml index af5d58d47fce1..d501069f1d979 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml @@ -3,19 +3,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_source = $block->getSource() ?> <?php $block->setPriceDataObject($_source) ?> -<tr> +<tr id="grand-totals"> <td class="label"> - <strong style="font-size: larger"> - <?php if ($block->getGrandTotalTitle()) : ?> + <strong> + <?php if ($block->getGrandTotalTitle()): ?> <?= $block->escapeHtml($block->getGrandTotalTitle()) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml(__('Grand Total')) ?> <?php endif; ?> </strong> </td> - <td class="emph" style="font-size: larger"><?= /* @noEscape */ $block->displayPriceAttribute('grand_total', true) ?></td> + <td class="emph"><?= /* @noEscape */ $block->displayPriceAttribute('grand_total', true) ?></td> </tr> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "font-size: larger", + 'tr#grand-totals td.label strong' +) ?> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "font-size: larger", + 'tr#grand-totals td.emph' +) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml index a68fb09fd2058..0ae7f71145dcc 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml @@ -4,26 +4,32 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - -/** @var $block \Magento\Sales\Block\Adminhtml\Order\Totals\Tax */ +/** + * @var $block \Magento\Sales\Block\Adminhtml\Order\Totals\Tax + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ /** @var $_source \Magento\Sales\Model\Order\Invoice */ $_source = $block->getSource(); $_order = $block->getOrder(); $_fullInfo = $block->getFullTaxInfo(); + +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); +/** @var \Magento\Framework\Math\Random $randomHelper */ +$randomHelper = $block->getData('randomHelper'); ?> -<?php if ($block->displayFullSummary() && $_fullInfo) : ?> -<tr class="summary-total" onclick="expandDetails(this, '.summary-details')"> -<?php else : ?> +<?php if ($block->displayFullSummary() && $_fullInfo): ?> +<tr class="summary-total"> +<?php else: ?> <tr> - <?php endif; ?> +<?php endif; ?> <td class="label"> <div class="summary-collapse" tabindex="0"> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + <?php if ($taxHelper->displayFullSummary()): ?> <?= $block->escapeHtml(__('Total Tax')) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml(__('Tax')) ?> <?php endif;?> </div> @@ -32,11 +38,16 @@ $_fullInfo = $block->getFullTaxInfo(); <?= /* @noEscape */ $block->displayAmount($_source->getTaxAmount(), $_source->getBaseTaxAmount()) ?> </td> </tr> -<?php if ($block->displayFullSummary()) : ?> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "expandDetails(this, '.summary-details')", + 'tr.summary-total' +) ?> +<?php if ($block->displayFullSummary()): ?> <?php $isTop = 1; ?> - <?php if (isset($_fullInfo[0]['rates'])) : ?> - <?php foreach ($_fullInfo as $info) : ?> - <?php if (isset($info['hidden']) && $info['hidden']) : + <?php if (isset($_fullInfo[0]['rates'])): ?> + <?php foreach ($_fullInfo as $info): ?> + <?php if (isset($info['hidden']) && $info['hidden']): continue; endif; ?> <?php @@ -47,39 +58,50 @@ $_fullInfo = $block->getFullTaxInfo(); $isFirst = 1; ?> - <?php foreach ($rates as $rate) : ?> - <tr class="summary-details<?= ($isTop ? ' summary-details-first' : '') ?>" style="display:none;"> - <?php if ($rate['percent'] !== null) : ?> - <td class="admin__total-mark"><?= $block->escapeHtml($rate['title']) ?> (<?= (float)$rate['percent'] ?>%)<br /></td> - <?php else : ?> + <?php foreach ($rates as $rate): ?> + <tr id="rate-<?= /* @noEscape */ $rate->getId() ?>" + class="summary-details<?= ($isTop ? ' summary-details-first' : '') ?>"> + <?php if ($rate['percent'] !== null): ?> + <td class="admin__total-mark"> + <?= $block->escapeHtml($rate['title']) ?> (<?= (float)$rate['percent'] ?>%)<br /> + </td> + <?php else: ?> <td class="admin__total-mark"><?= $block->escapeHtml($rate['title']) ?><br /></td> <?php endif; ?> - <?php if ($isFirst) : ?> - <td rowspan="<?= count($rates) ?>"><?= /* @noEscape */ $block->displayAmount($amount, $baseAmount) ?></td> + <?php if ($isFirst): ?> + <td rowspan="<?= count($rates) ?>"> + <?= /* @noEscape */ $block->displayAmount($amount, $baseAmount) ?> + </td> <?php endif; ?> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'tr#rate-' . $rate->getId()) ?> <?php $isFirst = 0; $isTop = 0; ?> <?php endforeach; ?> <?php endforeach; ?> - <?php else : ?> - <?php foreach ($_fullInfo as $info) : ?> + <?php else: ?> + <?php foreach ($_fullInfo as $info): ?> <?php $percent = $info['percent']; $amount = $info['tax_amount']; $baseAmount = $info['base_tax_amount']; $isFirst = 1; + $infoId = $randomHelper->getRandomString(20); ?> - <tr class="summary-details<?= ($isTop ? ' summary-details-first' : '') ?>" style="display:none;"> - <?php if ($info['percent'] !== null) : ?> - <td class="admin__total-mark"><?= $block->escapeHtml($info['title']) ?> (<?= (float)$info['percent'] ?>%)<br /></td> - <?php else : ?> + <tr id="info-<?= /* @noEscape */ $infoId ?>" + class="summary-details<?= ($isTop ? ' summary-details-first' : '') ?>"> + <?php if ($info['percent'] !== null): ?> + <td class="admin__total-mark"> + <?= $block->escapeHtml($info['title']) ?> (<?= (float)$info['percent'] ?>%)<br /> + </td> + <?php else: ?> <td class="admin__total-mark"><?= $block->escapeHtml($info['title']) ?><br /></td> <?php endif; ?> <td><?= /* @noEscape */ $block->displayAmount($amount, $baseAmount) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'tr#info-' . $infoId) ?> <?php $isFirst = 0; $isTop = 0; diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml index 16643a29a7fbe..a168a89ed5ef4 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml @@ -5,17 +5,22 @@ */ /** @var \Magento\Sales\Block\Adminhtml\Order\View\History $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="order_history_block" class="edit-order-comments"> - <?php if ($block->canAddComment()) : ?> + <?php if ($block->canAddComment()): ?> <div class="order-history-block" id="history_form"> <div class="admin__field"> <label for="history_status" class="admin__field-label"><?= $block->escapeHtml(__('Status')) ?></label> <div class="admin__field-control"> <select name="history[status]" id="history_status" class="admin__control-select"> - <?php foreach ($block->getStatuses() as $_code => $_label) : ?> - <option value="<?= $block->escapeHtmlAttr($_code) ?>"<?php if ($_code == $block->getOrder()->getStatus()) : ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_label) ?></option> + <?php foreach ($block->getStatuses() as $_code => $_label): ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>" + <?php if ($_code == $block->getOrder()->getStatus()): ?> selected="selected" + <?php endif; ?>> + <?= $block->escapeHtml($_label) ?> + </option> <?php endforeach; ?> </select> </div> @@ -37,7 +42,7 @@ <div class="admin__field"> <div class="order-history-comments-options"> <div class="admin__field admin__field-option"> - <?php if ($block->canSendCommentEmail()) : ?> + <?php if ($block->canSendCommentEmail()): ?> <input name="history[is_customer_notified]" type="checkbox" id="history_notify" @@ -69,30 +74,40 @@ <?php endif;?> <ul class="note-list"> - <?php foreach ($block->getOrder()->getStatusHistoryCollection(true) as $_item) : ?> + <?php foreach ($block->getOrder()->getStatusHistoryCollection(true) as $_item): ?> <li class="note-list-item"> - <span class="note-list-date"><?= /* @noEscape */ $block->formatDate($_item->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> - <span class="note-list-time"><?= /* @noEscape */ $block->formatTime($_item->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> + <span class="note-list-date"> + <?= /* @noEscape */ $block->formatDate($_item->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?> + </span> + <span class="note-list-time"> + <?= /* @noEscape */ $block->formatTime($_item->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?> + </span> <span class="note-list-status"><?= $block->escapeHtml($_item->getStatusLabel()) ?></span> <span class="note-list-customer"> <?= $block->escapeHtml(__('Customer')) ?> - <?php if ($block->isCustomerNotificationNotApplicable($_item)) : ?> - <span class="note-list-customer-notapplicable"><?= $block->escapeHtml(__('Notification Not Applicable')) ?></span> - <?php elseif ($_item->getIsCustomerNotified()) : ?> + <?php if ($block->isCustomerNotificationNotApplicable($_item)): ?> + <span class="note-list-customer-notapplicable"> + <?= $block->escapeHtml(__('Notification Not Applicable')) ?> + </span> + <?php elseif ($_item->getIsCustomerNotified()): ?> <span class="note-list-customer-notified"><?= $block->escapeHtml(__('Notified')) ?></span> - <?php else : ?> + <?php else: ?> <span class="note-list-customer-not-notified"><?= $block->escapeHtml(__('Not Notified')) ?></span> <?php endif; ?> </span> - <?php if ($_item->getComment()) : ?> - <div class="note-list-comment"><?= $block->escapeHtml($_item->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?></div> + <?php if ($_item->getComment()): ?> + <div class="note-list-comment"> + <?= $block->escapeHtml($_item->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?> + </div> <?php endif; ?> </li> <?php endforeach; ?> </ul> - <script> + <?php $scriptString = <<<script require(['prototype'], function(){ - if($('order_status'))$('order_status').update('<?= $block->escapeJs($block->escapeHtml($block->getOrder()->getStatusLabel())) ?>'); + if($('order_status'))$('order_status').update('{$block->escapeJs($block->getOrder()->getStatusLabel())}'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml index 390adb7d5cfce..06f0603a21215 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Sales\Block\Adminhtml\Order\View\Tab\Info */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_order = $block->getOrder() ?> @@ -20,14 +21,17 @@ <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')) ?></span> </div> <div class="admin__page-section-content"> - <div class="admin__page-section-item order-payment-method<?= ($_order->getIsVirtual() ? ' order-payment-method-virtual' : '') ?>"> + <div class="admin__page-section-item order-payment-method<?= ($_order->getIsVirtual() ? + ' order-payment-method-virtual' : '') ?>"> <?php /* Payment Method */ ?> <div class="admin__page-section-item-title"> <span class="title"><?= $block->escapeHtml(__('Payment Information')) ?></span> </div> <div class="admin__page-section-item-content"> <div class="order-payment-method-title"><?= $block->getPaymentHtml() ?></div> - <div class="order-payment-currency"><?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?></div> + <div class="order-payment-currency"> + <?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?> + </div> <div class="order-payment-additional"> <?= $block->getChildHtml('order_payment_additional') ?> <?= $block->getChildHtml('payment_additional_info') ?> @@ -72,7 +76,7 @@ <?= $block->getChildHtml('popup_window') ?> -<script> +<?php $scriptString = <<<script require([ "prototype", "Magento_Sales/order/giftoptions_tooltip" @@ -87,7 +91,7 @@ require([ var headerLine = null; var contentLine = null; - $$('#gift_options_data_' + itemId + ' .gift-options-tooltip-content').each(function (element) { + \$$('#gift_options_data_' + itemId + ' .gift-options-tooltip-content').each(function (element) { if (element.down(0)) { headerLine = element.down(0).innerHTML; contentLine = element.down(0).next().innerHTML; @@ -104,4 +108,6 @@ require([ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml index e1f047b372c95..f6b1240402477 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml @@ -58,7 +58,7 @@ </settings> </filterSelect> </filters> - <massaction name="listing_massaction" component="Magento_Ui/js/grid/tree-massactions"> + <massaction name="listing_massaction" component="Magento_Sales/js/grid/tree-massactions"> <action name="cancel"> <settings> <url path="sales/order/massCancel"/> diff --git a/app/code/Magento/Sales/view/adminhtml/web/js/grid/tree-massactions.js b/app/code/Magento/Sales/view/adminhtml/web/js/grid/tree-massactions.js new file mode 100644 index 0000000000000..a2783222afc28 --- /dev/null +++ b/app/code/Magento/Sales/view/adminhtml/web/js/grid/tree-massactions.js @@ -0,0 +1,34 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'mageUtils', + 'Magento_Ui/js/grid/tree-massactions' +], function (_, utils, Massactions) { + 'use strict'; + + return Massactions.extend({ + /** + * Overwrite Default action callback. + * Sends selections data with ids + * via POST request. + * + * @param {Object} action - Action data. + * @param {Object} data - Selections data. + */ + defaultCallback: function (action, data) { + var itemsType = 'selected', + selections = {}; + + selections[itemsType] = data[itemsType]; + _.extend(selections, data.params || {}); + utils.submit({ + url: action.url, + data: selections + }); + } + }); +}); diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index e138112ac3f5a..a329524c58d41 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -84,7 +84,7 @@ define([ }, 10); }; - if (jQuery('#' + this.getAreaId('items')).is(':visible')) { + jQuery.async('#order-items .admin__page-section-title', (function () { this.dataArea.onLoad = this.dataArea.onLoad.wrap(function (proceed) { proceed(); this._parent.itemsArea.setNode($(this._parent.getAreaId('items'))); @@ -93,13 +93,15 @@ define([ this.itemsArea.onLoad = this.itemsArea.onLoad.wrap(function (proceed) { proceed(); - if ($(searchAreaId) && !$(searchAreaId).visible() && !$(searchButtonId)) { + if ($(searchAreaId) && !jQuery('#' + searchAreaId).is(':visible') && !$(searchButtonId)) { this.addControlButton(searchButton); } }); this.areasLoaded(); this.itemsArea.onLoad(); - } + + }).bind(this)); + }).bind(this)); jQuery('#edit_form') @@ -479,6 +481,13 @@ define([ }, switchPaymentMethod: function(method){ + if (this.paymentMethod !== method) { + jQuery('#edit_form') + .off('submitOrder') + .on('submitOrder', function(){ + jQuery(this).trigger('realOrder'); + }); + } jQuery('#edit_form').trigger('changePaymentMethod', [method]); this.setPaymentMethod(method); var data = {}; @@ -1306,11 +1315,16 @@ define([ }, submit: function () { - var $editForm = jQuery('#edit_form'); + var $editForm = jQuery('#edit_form'), + beforeSubmitOrderEvent; if ($editForm.valid()) { $editForm.trigger('processStart'); - $editForm.trigger('submitOrder'); + beforeSubmitOrderEvent = jQuery.Event('beforeSubmitOrder'); + $editForm.trigger(beforeSubmitOrderEvent); + if (beforeSubmitOrderEvent.result !== false) { + $editForm.trigger('submitOrder'); + } } }, diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html index d8a8a0baeca98..f3d133a974082 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html @@ -10,7 +10,7 @@ "var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", "layout handle=\"sales_email_order_creditmemo_items\" creditmemo=$creditmemo order=$order":"Credit Memo Items Grid", -"var billing.name":"Guest Customer Name (Billing)", +"var order_data.customer_name":"Guest Customer Name (Billing)", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", @@ -30,7 +30,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html index ed8f592b59638..64e9a61831956 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html @@ -8,7 +8,7 @@ <!--@vars { "var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", "var store.frontend_name":"Store Frontend Name", @@ -21,7 +21,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html b/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html index c06630fd249ab..b2109fad5478a 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html @@ -7,7 +7,7 @@ <!--@subject {{trans "Invoice for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "layout handle=\"sales_email_order_invoice_items\" invoice=$invoice order=$order":"Invoice Items Grid", @@ -30,7 +30,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html index 289c5113fe285..be3462d0e2fc9 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name invoice" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", @@ -21,7 +21,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html index dc3a8e9f69aca..0529c66a04d8c 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html @@ -8,7 +8,7 @@ <!--@vars { "var formattedBillingAddress|raw":"Billing Address", "var order_data.email_customer_note|escape|nl2br":"Email Order Note", -"var order.billing_address.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var created_at_formatted":"Order Created At (datetime)", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order":"Order Items Grid", @@ -29,7 +29,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.billing_address.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send an email with a link to track your order."}} diff --git a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html index 1ce0d162ed76e..4f1f6862fab03 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", @@ -20,7 +20,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html index 54c7f08506497..85473cb0ee8c6 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html @@ -7,7 +7,7 @@ <!--@subject {{trans "Your %store_name order has shipped" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var comment|escape|nl2br":"Shipment Comment", @@ -31,7 +31,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html index 087cb0ddbf5bc..cf54379d4bb46 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name shipment" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", @@ -21,7 +21,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml b/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml index 0dff0710dd63a..39d6dafe57244 100644 --- a/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form class="form form-orders-search" id="oar-widget-orders-and-returns-form" @@ -23,7 +24,9 @@ </div> </div> <div class="field lastname required"> - <label class="label" for="oar-billing-lastname"><span><?= $block->escapeHtml(__('Billing Last Name')) ?></span></label> + <label class="label" for="oar-billing-lastname"> + <span><?= $block->escapeHtml(__('Billing Last Name')) ?></span> + </label> <div class="control"> <input type="text" class="input-text" id="oar-billing-lastname" name="oar_billing_lastname" @@ -31,7 +34,9 @@ </div> </div> <div class="field find required"> - <label class="label" for="quick-search-type-id"><span><?= $block->escapeHtml(__('Find Order By')) ?></span></label> + <label class="label" for="quick-search-type-id"> + <span><?= $block->escapeHtml(__('Find Order By')) ?></span> + </label> <div class="control"> <select name="oar_type" id="quick-search-type-id" class="select"> @@ -48,13 +53,14 @@ data-validate="{required:true, 'validate-email':true}"/> </div> </div> - <div id="oar-zip" style="display: none;" class="field zip required"> + <div id="oar-zip" class="field zip required"> <label class="label" for="oar_zip"><span><?= $block->escapeHtml(__('Billing ZIP Code')) ?></span></label> <div class="control"> <input type="text" class="input-text" id="oar_zip" name="oar_zip" data-validate="{required:true}"/> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'div#oar-zip') ?> </fieldset> <div class="actions-toolbar"> <div class="primary"> 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/print/shipment.phtml b/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml index 3f12ca1f7b270..90b5f0f289e64 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml @@ -3,26 +3,38 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Sales\Block\Order\PrintOrder\Shipment $block + * @var \Magento\Framework\Escaper $escaper + */ ?> -<?php /* @var $block \Magento\Sales\Block\Order\PrintOrder\Shipment */ ?> <?php $order = $block->getOrder(); ?> -<?php if (!$block->getObjectData($order, 'is_virtual')) : ?> - <?php foreach ($block->getShipmentsCollection() as $shipment) : ?> +<?php if (!$block->getObjectData($order, 'is_virtual')): ?> + <?php foreach ($block->getShipmentsCollection() as $shipment): ?> <div class="order-details-items shipments"> <div class="order-title"> - <strong><?= $block->escapeHtml(__('Shipment #%1', $block->getObjectData($shipment, 'increment_id'))) ?></strong> + <strong> + <?= $escaper->escapeHtml( + __( + 'Shipment #%1', + $block->getObjectData($shipment, 'increment_id') + ) + ) ?> + </strong> </div> <div class="table-wrapper order-items-shipment"> - <table class="data table table-order-items shipment" id="my-shipment-table-<?= (int) $block->getObjectData($shipment, 'id') ?>"> - <caption class="table-caption"><?= $block->escapeHtml(__('Items Invoiced')) ?></caption> + <table class="data table table-order-items shipment" + id="my-shipment-table-<?= (int)$block->getObjectData($shipment, 'id') ?>"> + <caption class="table-caption"><?= $escaper->escapeHtml(__('Items Invoiced')) ?></caption> <thead> - <tr> - <th class="col name"><?= $block->escapeHtml(__('Product Name')) ?></th> - <th class="col sku"><?= $block->escapeHtml(__('SKU')) ?></th> - <th class="col price"><?= $block->escapeHtml(__('Qty Shipped')) ?></th> - </tr> + <tr> + <th class="col name"><?= $escaper->escapeHtml(__('Product Name')) ?></th> + <th class="col sku"><?= $escaper->escapeHtml(__('SKU')) ?></th> + <th class="col price"><?= $escaper->escapeHtml(__('Qty Shipped')) ?></th> + </tr> </thead> - <?php foreach ($block->getShipmentItems($shipment) as $item) : ?> + <?php foreach ($block->getShipmentItems($shipment) as $item): ?> <tbody> <?= $block->getItemHtml($item) ?> </tbody> @@ -31,12 +43,12 @@ </div> <div class="block block-order-details-view"> <div class="block-title"> - <strong><?= $block->escapeHtml(__('Order Information')) ?></strong> + <strong><?= $escaper->escapeHtml(__('Order Information')) ?></strong> </div> <div class="block-content"> <div class="box box-order-shipping-address"> <div class="box-title"> - <strong><?= $block->escapeHtml(__('Shipping Address')) ?></strong> + <strong><?= $escaper->escapeHtml(__('Shipping Address')) ?></strong> </div> <div class="box-content"> <address><?= $block->getShipmentAddressFormattedHtml($shipment) ?></address> @@ -45,25 +57,29 @@ <div class="box box-order-shipping-method"> <div class="box-title"> - <strong><?= $block->escapeHtml(__('Shipping Method')) ?></strong> + <strong><?= $escaper->escapeHtml(__('Shipping Method')) ?></strong> </div> <div class="box-content"> - <?= $block->escapeHtml($block->getObjectData($order, 'shipping_description')) ?> + <?= $escaper->escapeHtml($block->getObjectData($order, 'shipping_description')) ?> <?php $tracks = $block->getShipmentTracks($shipment); - if ($tracks) : ?> + if ($tracks): ?> <dl class="order-tracking"> - <?php foreach ($tracks as $track) : ?> - <dt class="tracking-title"><?= $block->escapeHtml($block->getObjectData($track, 'title')) ?></dt> - <dd class="tracking-content"><?= $block->escapeHtml($block->getObjectData($track, 'number')) ?></dd> + <?php foreach ($tracks as $track): ?> + <dt class="tracking-title"> + <?= $escaper->escapeHtml($block->getObjectData($track, 'title')) ?> + </dt> + <dd class="tracking-content"> + <?= $escaper->escapeHtml($block->getObjectData($track, 'number')) ?> + </dd> <?php endforeach; ?> </dl> <?php endif; ?> </div> </div> - <div class="box box-order-billing-method"> + <div class="box box-order-billing-address"> <div class="box-title"> - <strong><?= $block->escapeHtml(__('Billing Address')) ?></strong> + <strong><?= $escaper->escapeHtml(__('Billing Address')) ?></strong> </div> <div class="box-content"> <address><?= $block->getBillingAddressFormattedHtml($order) ?></address> @@ -72,7 +88,7 @@ <div class="box box-order-billing-method"> <div class="box-title"> - <strong><?= $block->escapeHtml(__('Payment Method')) ?></strong> + <strong><?= $escaper->escapeHtml(__('Payment Method')) ?></strong> </div> <div class="box-content"> <?= $block->getPaymentInfoHtml() ?> 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/Sales/view/frontend/templates/reorder/sidebar.phtml b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml index ba1204fac8ec5..6bdac443f657d 100644 --- a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml @@ -8,12 +8,15 @@ * Last ordered items sidebar * * @var $block \Magento\Sales\Block\Reorder\Sidebar + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="block block-reorder" data-bind="scope: 'lastOrderedItems'"> <div class="block-title no-display" data-bind="css: {'no-display': !lastOrderedItems().items || lastOrderedItems().items.length === 0}"> - <strong id="block-reorder-heading" role="heading" aria-level="2"><?= $block->escapeHtml(__('Recently Ordered')) ?></strong> + <strong id="block-reorder-heading" role="heading" aria-level="2"> + <?= $block->escapeHtml(__('Recently Ordered')) ?> + </strong> </div> <div class="block-content no-display" data-bind="css: {'no-display': !lastOrderedItems().items || lastOrderedItems().items.length === 0}" @@ -33,7 +36,8 @@ data-bind="attr: { id: 'reorder-item-' + id, value: id, - title: is_saleable ? '<?= $block->escapeHtml(__('Add to Cart')) ?>' : '<?= $block->escapeHtml(__('Product is not salable.')) ?>' + title: is_saleable ? '<?= $block->escapeHtml(__('Add to Cart')) ?>' : + '<?= $block->escapeHtml(__('Product is not salable.')) ?>' }, disable: !is_saleable" class="checkbox" data-validate='{"validate-one-checkbox-required-by-name": true}'/> @@ -50,19 +54,21 @@ <div class="actions-toolbar"> <div class="primary" data-bind="visible: isShowAddToCart"> - <button type="submit" title="<?= $block->escapeHtml(__('Add to Cart')) ?>" class="action tocart primary"> + <button type="submit" title="<?= $block->escapeHtml(__('Add to Cart')) ?>" + class="action tocart primary"> <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> </div> <div class="secondary"> - <a class="action view" href="<?= $block->escapeUrl($block->getUrl('customer/account')) ?>#my-orders-table"> + <a class="action view" + href="<?= $block->escapeUrl($block->getUrl('customer/account')) ?>#my-orders-table"> <span><?= $block->escapeHtml(__('View All')) ?></span> </a> </div> </div> </form> </div> - <script> + <?php $scriptString = <<<script require(["jquery", "mage/mage"], function(jQuery){ jQuery('#reorder-validate-detail').mage('validation', { errorPlacement: function(error, element) { @@ -70,7 +76,9 @@ } }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml b/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml index 25926688c6f47..7772e7b9680fd 100644 --- a/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml @@ -4,15 +4,19 @@ * See COPYING.txt for license details. */ -/** @var $block \Magento\Sales\Block\Widget\Guest\Form */ +/** + * @var $block \Magento\Sales\Block\Widget\Guest\Form + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->isEnable()) : ?> +<?php if ($block->isEnable()): ?> <div class="widget block block-orders-returns"> <div class="block-title"> <strong role="heading" aria-level="2"><?= $block->escapeHtml(__('Orders and Returns')) ?></strong> </div> <div class="block-content"> - <form id="oar-widget-orders-and-returns-form" data-mage-init='{"ordersReturns":{},"validation":{}}' action="<?= $block->escapeUrl($block->getActionUrl()) ?>" method="post" + <form id="oar-widget-orders-and-returns-form" data-mage-init='{"ordersReturns":{},"validation":{}}' + action="<?= $block->escapeUrl($block->getActionUrl()) ?>" method="post" class="form form-orders-search" name="guest_post"> <fieldset class="fieldset"> <div class="field find required"> @@ -26,10 +30,13 @@ </div> </div> <div class="field id required"> - <label for="oar-order-id" class="label"><span><?= $block->escapeHtml(__('Order ID')) ?></span></label> + <label for="oar-order-id" class="label"> + <span><?= $block->escapeHtml(__('Order ID')) ?></span> + </label> <div class="control"> - <input type="text" class="input-text" id="oar-order-id" name="oar_order_id" autocomplete="off" + <input type="text" class="input-text" id="oar-order-id" name="oar_order_id" + autocomplete="off" data-validate="{required:true}"> </div> </div> @@ -50,14 +57,17 @@ data-validate="{required:true, 'validate-email':true}"> </div> </div> - <div id="oar-zip" style="display: none;" class="field zip required"> - <label for="oar_zip" class="label"><span><?= $block->escapeHtml(__('Billing ZIP Code')) ?></span></label> + <div id="oar-zip" class="field zip required"> + <label for="oar_zip" class="label"> + <span><?= $block->escapeHtml(__('Billing ZIP Code')) ?></span> + </label> <div class="control"> <input type="text" class="input-text" id="oar_zip" name="oar_zip" data-validate="{required:true}"/> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#oar-zip') ?> </fieldset> <div class="actions-toolbar"> <div class="primary"> diff --git a/app/code/Magento/SalesGraphQl/Model/Order/OrderAddress.php b/app/code/Magento/SalesGraphQl/Model/Order/OrderAddress.php new file mode 100644 index 0000000000000..08e67ee29cbdd --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Order/OrderAddress.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Order; + +use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Class to get the order address details + */ +class OrderAddress +{ + /** + * Get the order Shipping address + * + * @param OrderInterface $order + * @return array|null + */ + public function getOrderShippingAddress( + OrderInterface $order + ): ?array { + $shippingAddress = null; + if ($order->getShippingAddress()) { + $shippingAddress = $this->formatAddressData($order->getShippingAddress()); + } + return $shippingAddress; + } + + /** + * Get the order billing address + * + * @param OrderInterface $order + * @return array|null + */ + public function getOrderBillingAddress( + OrderInterface $order + ): ?array { + $billingAddress = null; + if ($order->getBillingAddress()) { + $billingAddress = $this->formatAddressData($order->getBillingAddress()); + } + return $billingAddress; + } + + /** + * Customer Order address data formatter + * + * @param OrderAddressInterface $orderAddress + * @return array + */ + private function formatAddressData( + OrderAddressInterface $orderAddress + ): array { + return + [ + 'firstname' => $orderAddress->getFirstname(), + 'lastname' => $orderAddress->getLastname(), + 'middlename' => $orderAddress->getMiddlename(), + 'postcode' => $orderAddress->getPostcode(), + 'prefix' => $orderAddress->getPrefix(), + 'suffix' => $orderAddress->getSuffix(), + 'street' => $orderAddress->getStreet(), + 'country_code' => $orderAddress->getCountryId(), + 'city' => $orderAddress->getCity(), + 'company' => $orderAddress->getCompany(), + 'fax' => $orderAddress->getFax(), + 'telephone' => $orderAddress->getTelephone(), + 'vat_id' => $orderAddress->getVatId(), + 'region_id' => $orderAddress->getRegionId(), + 'region' => $orderAddress->getRegion() + ]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Order/OrderPayments.php b/app/code/Magento/SalesGraphQl/Model/Order/OrderPayments.php new file mode 100644 index 0000000000000..991f36663448b --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Order/OrderPayments.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Order; + +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Class to get the order payment details + */ +class OrderPayments +{ + /** + * Get the order payment method + * + * @param OrderInterface $orderModel + * @return array + */ + public function getOrderPaymentMethod(OrderInterface $orderModel): array + { + $orderPayment = $orderModel->getPayment(); + return [ + [ + 'name' => $orderPayment->getAdditionalInformation()['method_title'] ?? '', + 'type' => $orderPayment->getMethod(), + 'additional_data' => [] + ] + ]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php b/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php new file mode 100644 index 0000000000000..a69d9bf58ee8d --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\OrderItem; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; + +/** + * Data provider for order items + */ +class DataProvider +{ + /** + * @var OrderItemRepositoryInterface + */ + private $orderItemRepository; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var OptionsProcessor + */ + private $optionsProcessor; + + /** + * @var int[] + */ + private $orderItemIds = []; + + /** + * @var array + */ + private $orderItemList = []; + + /** + * @param OrderItemRepositoryInterface $orderItemRepository + * @param ProductRepositoryInterface $productRepository + * @param OrderRepositoryInterface $orderRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param OptionsProcessor $optionsProcessor + */ + public function __construct( + OrderItemRepositoryInterface $orderItemRepository, + ProductRepositoryInterface $productRepository, + OrderRepositoryInterface $orderRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + OptionsProcessor $optionsProcessor + ) { + $this->orderItemRepository = $orderItemRepository; + $this->productRepository = $productRepository; + $this->orderRepository = $orderRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->optionsProcessor = $optionsProcessor; + } + + /** + * Add order item id to list for fetching + * + * @param int $orderItemId + */ + public function addOrderItemId(int $orderItemId): void + { + if (!in_array($orderItemId, $this->orderItemIds)) { + $this->orderItemList = []; + $this->orderItemIds[] = $orderItemId; + } + } + + /** + * Get order item by item id + * + * @param int $orderItemId + * @return array + */ + public function getOrderItemById(int $orderItemId): array + { + $orderItems = $this->fetch(); + if (!isset($orderItems[$orderItemId])) { + return []; + } + return $orderItems[$orderItemId]; + } + + /** + * Fetch order items and return in format for GraphQl + * + * @return array + */ + private function fetch() + { + if (empty($this->orderItemIds) || !empty($this->orderItemList)) { + return $this->orderItemList; + } + + $itemSearchCriteria = $this->searchCriteriaBuilder + ->addFilter(OrderItemInterface::ITEM_ID, $this->orderItemIds, 'in') + ->create(); + + $orderItems = $this->orderItemRepository->getList($itemSearchCriteria)->getItems(); + $productList = $this->fetchProducts($orderItems); + $orderList = $this->fetchOrders($orderItems); + + foreach ($orderItems as $orderItem) { + /** @var ProductInterface $associatedProduct */ + $associatedProduct = $productList[$orderItem->getProductId()] ?? null; + /** @var OrderInterface $associatedOrder */ + $associatedOrder = $orderList[$orderItem->getOrderId()]; + $itemOptions = $this->optionsProcessor->getItemOptions($orderItem); + $this->orderItemList[$orderItem->getItemId()] = [ + 'id' => base64_encode($orderItem->getItemId()), + 'associatedProduct' => $associatedProduct, + 'model' => $orderItem, + 'product_name' => $orderItem->getName(), + 'product_sku' => $orderItem->getSku(), + 'product_url_key' => $associatedProduct ? $associatedProduct->getUrlKey() : null, + 'product_type' => $orderItem->getProductType(), + 'status' => $orderItem->getStatus(), + 'discounts' => $this->getDiscountDetails($associatedOrder, $orderItem), + 'product_sale_price' => [ + 'value' => $orderItem->getPrice(), + 'currency' => $associatedOrder->getOrderCurrencyCode() + ], + 'selected_options' => $itemOptions['selected_options'], + 'entered_options' => $itemOptions['entered_options'], + 'quantity_ordered' => $orderItem->getQtyOrdered(), + 'quantity_shipped' => $orderItem->getQtyShipped(), + 'quantity_refunded' => $orderItem->getQtyRefunded(), + 'quantity_invoiced' => $orderItem->getQtyInvoiced(), + 'quantity_canceled' => $orderItem->getQtyCanceled(), + 'quantity_returned' => $orderItem->getQtyReturned() + ]; + } + + return $this->orderItemList; + } + + /** + * Fetch associated products for order items + * + * @param array $orderItems + * @return array + */ + private function fetchProducts(array $orderItems): array + { + $productIds = array_map( + function ($orderItem) { + return $orderItem->getProductId(); + }, + $orderItems + ); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('entity_id', $productIds, 'in') + ->create(); + $products = $this->productRepository->getList($searchCriteria)->getItems(); + $productList = []; + foreach ($products as $product) { + $productList[$product->getId()] = $product; + } + return $productList; + } + + /** + * Fetch associated order for order items + * + * @param array $orderItems + * @return array + */ + private function fetchOrders(array $orderItems): array + { + $orderIds = array_map( + function ($orderItem) { + return $orderItem->getOrderId(); + }, + $orderItems + ); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('entity_id', $orderIds, 'in') + ->create(); + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + + $orderList = []; + foreach ($orders as $order) { + $orderList[$order->getEntityId()] = $order; + } + return $orderList; + } + + /** + * Returns information about an applied discount + * + * @param OrderInterface $associatedOrder + * @param OrderItemInterface $orderItem + * @return array + */ + private function getDiscountDetails(OrderInterface $associatedOrder, OrderItemInterface $orderItem) : array + { + if ($associatedOrder->getDiscountDescription() === null && $orderItem->getDiscountAmount() == 0 + && $associatedOrder->getDiscountAmount() == 0 + ) { + $discounts = []; + } else { + $discounts [] = [ + 'label' => $associatedOrder->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($orderItem->getDiscountAmount()) ?? 0, + 'currency' => $associatedOrder->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/OrderItem/OptionsProcessor.php b/app/code/Magento/SalesGraphQl/Model/OrderItem/OptionsProcessor.php new file mode 100644 index 0000000000000..83b7e0cc46d96 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/OrderItem/OptionsProcessor.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\OrderItem; + +use Magento\Sales\Api\Data\OrderItemInterface; + +/** + * Process order item options to format for GraphQl output + */ +class OptionsProcessor +{ + /** + * Get Order item options. + * + * @param OrderItemInterface $orderItem + * @return array + */ + public function getItemOptions(OrderItemInterface $orderItem): array + { + //build options array + $optionsTypes = ['selected_options' => [], 'entered_options' => []]; + $options = $orderItem->getProductOptions(); + if ($options) { + if (isset($options['options'])) { + $optionsTypes = $this->processOptions($options['options']); + } elseif (isset($options['attributes_info'])) { + $optionsTypes = $this->processAttributesInfo($options['attributes_info']); + } + } + return $optionsTypes; + } + + /** + * Process options data + * + * @param array $options + * @return array + */ + private function processOptions(array $options): array + { + $selectedOptions = []; + $enteredOptions = []; + foreach ($options ?? [] as $option) { + if (isset($option['option_type'])) { + if (in_array($option['option_type'], ['field', 'area', 'file', 'date', 'date_time', 'time'])) { + $selectedOptions[] = [ + 'id' => $option['label'], + 'value' => $option['print_value'] ?? $option['value'], + ]; + } elseif (in_array($option['option_type'], ['drop_down', 'radio', 'checkbox', 'multiple'])) { + $enteredOptions[] = [ + 'id' => $option['label'], + 'value' => $option['print_value'] ?? $option['value'], + ]; + } + } + } + return ['selected_options' => $selectedOptions, 'entered_options' => $enteredOptions]; + } + + /** + * Process attributes info data + * + * @param array $attributesInfo + * @return array + */ + private function processAttributesInfo(array $attributesInfo): array + { + $selectedOptions = []; + foreach ($attributesInfo ?? [] as $option) { + $selectedOptions[] = [ + 'id' => $option['label'], + 'value' => $option['print_value'] ?? $option['value'], + ]; + } + return ['selected_options' => $selectedOptions, 'entered_options' => []]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoComments.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoComments.php new file mode 100644 index 0000000000000..2c9fedf61b502 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoComments.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CreditMemo; + +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\Sales\Api\Data\CreditmemoInterface; + +/** + * Resolve credit memo comments + */ +class CreditMemoComments implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof CreditmemoInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var CreditmemoInterface $creditMemo */ + $creditMemo = $value['model']; + + $comments = []; + foreach ($creditMemo->getComments() as $comment) { + if ($comment->getIsVisibleOnFront()) { + $comments[] = [ + 'message' => $comment->getComment(), + 'timestamp' => $comment->getCreatedAt() + ]; + } + } + + return $comments; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoItems.php new file mode 100644 index 0000000000000..e1cee27e93f87 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoItems.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CreditMemo; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\CreditmemoItemInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\SalesGraphQl\Model\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolve credit memos items data + */ +class CreditMemoItems implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct( + ValueFactory $valueFactory, + OrderItemProvider $orderItemProvider + ) { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof CreditmemoInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (!(($value['order'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"order" value should be specified')); + } + + /** @var CreditmemoInterface $creditMemoModel */ + $creditMemoModel = $value['model']; + /** @var OrderInterface $parentOrderModel */ + $parentOrderModel = $value['order']; + + return $this->valueFactory->create( + $this->getCreditMemoItems($parentOrderModel, $creditMemoModel->getItems()) + ); + } + + /** + * Get credit memo items data as a promise + * + * @param OrderInterface $order + * @param array $creditMemoItems + * @return \Closure + */ + private function getCreditMemoItems(OrderInterface $order, array $creditMemoItems): \Closure + { + $orderItems = []; + foreach ($creditMemoItems as $item) { + $this->orderItemProvider->addOrderItemId((int)$item->getOrderItemId()); + } + + return function () use ($order, $creditMemoItems, $orderItems): array { + foreach ($creditMemoItems as $creditMemoItem) { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$creditMemoItem->getOrderItemId()); + /** @var OrderItemInterface $orderItemModel */ + $orderItemModel = $orderItem['model']; + if (!$orderItemModel->getParentItem()) { + $creditMemoItemData = $this->getCreditMemoItemData($order, $creditMemoItem); + if (!empty($creditMemoItemData)) { + $orderItems[$creditMemoItem->getOrderItemId()] = $creditMemoItemData; + } + } + } + return $orderItems; + }; + } + + /** + * Get credit memo item data + * + * @param OrderInterface $order + * @param CreditmemoItemInterface $creditMemoItem + * @return array + */ + private function getCreditMemoItemData(OrderInterface $order, CreditmemoItemInterface $creditMemoItem): array + { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$creditMemoItem->getOrderItemId()); + return [ + 'id' => base64_encode($creditMemoItem->getEntityId()), + 'product_name' => $creditMemoItem->getName(), + 'product_sku' => $creditMemoItem->getSku(), + 'product_sale_price' => [ + 'value' => $creditMemoItem->getPrice(), + 'currency' => $order->getOrderCurrencyCode() + ], + 'quantity_refunded' => $creditMemoItem->getQty(), + 'model' => $creditMemoItem, + 'product_type' => $orderItem['product_type'], + 'discounts' => $this->formatDiscountDetails($order, $creditMemoItem) + ]; + } + + /** + * Returns formatted information about an applied discount + * + * @param OrderInterface $associatedOrder + * @param CreditmemoItemInterface $creditmemoItem + * @return array + */ + private function formatDiscountDetails( + OrderInterface $associatedOrder, + CreditmemoItemInterface $creditmemoItem + ): array { + if ($associatedOrder->getDiscountDescription() === null + && $creditmemoItem->getDiscountAmount() == 0 + && $associatedOrder->getDiscountAmount() == 0 + ) { + $discounts = []; + } else { + $discounts[] = [ + 'label' => $associatedOrder->getDiscountDescription() ?? _('Discount'), + 'amount' => [ + 'value' => abs($creditmemoItem->getDiscountAmount()) ?? 0, + 'currency' => $associatedOrder->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoTotal.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoTotal.php new file mode 100644 index 0000000000000..5a8f4f7f17ce6 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoTotal.php @@ -0,0 +1,187 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CreditMemo; + +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\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\SalesGraphQl\Model\SalesItem\ShippingTaxCalculator; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Tax\Helper\Data as TaxHelper; + +/** + * Resolve credit memo totals information + */ +class CreditMemoTotal implements ResolverInterface +{ + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var OrderTaxManagementInterface + */ + private $orderTaxManagement; + + /** + * @var ShippingTaxCalculator + */ + private $shippingTaxCalculator; + /** + * @param OrderTaxManagementInterface $orderTaxManagement + * @param TaxHelper $taxHelper + * @param ShippingTaxCalculator $shippingTaxCalculator + */ + public function __construct( + OrderTaxManagementInterface $orderTaxManagement, + TaxHelper $taxHelper, + ShippingTaxCalculator $shippingTaxCalculator + ) { + $this->taxHelper = $taxHelper; + $this->orderTaxManagement = $orderTaxManagement; + $this->shippingTaxCalculator = $shippingTaxCalculator; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof CreditmemoInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (!(($value['order'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"order" value should be specified')); + } + + /** @var OrderInterface $orderModel */ + $orderModel = $value['order']; + /** @var CreditmemoInterface $creditMemo */ + $creditMemo = $value['model']; + $currency = $orderModel->getOrderCurrencyCode(); + $baseCurrency = $orderModel->getBaseCurrencyCode(); + return [ + 'base_grand_total' => ['value' => $creditMemo->getBaseGrandTotal(), 'currency' => $baseCurrency], + 'grand_total' => ['value' => $creditMemo->getGrandTotal(), 'currency' => $currency], + 'subtotal' => ['value' => $creditMemo->getSubtotal(), 'currency' => $currency], + 'total_tax' => ['value' => $creditMemo->getTaxAmount(), 'currency' => $currency], + 'total_shipping' => ['value' => $creditMemo->getShippingAmount(), 'currency' => $currency], + 'discounts' => $this->getDiscountDetails($creditMemo), + 'taxes' => $this->formatTaxes( + $orderModel, + $this->taxHelper->getCalculatedTaxes($creditMemo), + ), + 'shipping_handling' => [ + 'amount_excluding_tax' => [ + 'value' => $creditMemo->getShippingAmount() ?? 0, + 'currency' => $currency + ], + 'amount_including_tax' => [ + 'value' => $creditMemo->getShippingInclTax() ?? 0, + 'currency' => $currency + ], + 'total_amount' => [ + 'value' => $creditMemo->getShippingAmount() ?? 0, + 'currency' => $currency + ], + 'discounts' => $this->getShippingDiscountDetails($creditMemo, $orderModel), + 'taxes' => $this->formatTaxes( + $orderModel, + $this->shippingTaxCalculator->calculateShippingTaxes($orderModel, $creditMemo), + ) + ], + 'adjustment' => [ + 'value' => abs($creditMemo->getAdjustment()), + 'currency' => $currency + ] + ]; + } + + /** + * Return information about an applied discount on shipping + * + * @param CreditmemoInterface $creditmemoModel + * @param OrderInterface $orderModel + * @return array + */ + private function getShippingDiscountDetails(CreditmemoInterface $creditmemoModel, $orderModel): array + { + $creditmemoShippingAmount = (float)$creditmemoModel->getShippingAmount(); + $orderShippingAmount = (float)$orderModel->getShippingAmount(); + $calculatedShippingRatio = (float)$creditmemoShippingAmount != 0 && $orderShippingAmount != 0 ? + ($creditmemoShippingAmount / $orderShippingAmount) : 0; + $orderShippingDiscount = (float)$orderModel->getShippingDiscountAmount(); + $calculatedCreditmemoShippingDiscount = $orderShippingDiscount * $calculatedShippingRatio; + + $shippingDiscounts = []; + if ($calculatedCreditmemoShippingDiscount != 0) { + $shippingDiscounts[] = [ + 'amount' => [ + 'value' => sprintf('%.2f', abs($calculatedCreditmemoShippingDiscount)), + 'currency' => $creditmemoModel->getOrderCurrencyCode() + ] + ]; + } + return $shippingDiscounts; + } + + /** + * Return information about an applied discount + * + * @param CreditmemoInterface $creditmemo + * @return array + */ + private function getDiscountDetails(CreditmemoInterface $creditmemo): array + { + $discounts = []; + if (!($creditmemo->getDiscountDescription() === null && $creditmemo->getDiscountAmount() == 0)) { + $discounts[] = [ + 'label' => $creditmemo->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($creditmemo->getDiscountAmount()), + 'currency' => $creditmemo->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } + + /** + * Format applied taxes + * + * @param OrderInterface $order + * @param array $appliedTaxes + * @return array + */ + private function formatTaxes(OrderInterface $order, array $appliedTaxes): array + { + $taxes = []; + foreach ($appliedTaxes as $appliedTax) { + $appliedTaxesArray = [ + 'rate' => $appliedTax['percent'] ?? 0, + 'title' => $appliedTax['title'] ?? null, + 'amount' => [ + 'value' => $appliedTax['tax_amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $taxes[] = $appliedTaxesArray; + } + return $taxes; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemos.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemos.php new file mode 100644 index 0000000000000..69dbca9d66599 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemos.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Resolve credit memos for order + */ +class CreditMemos implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var OrderInterface $orderModel */ + $orderModel = $value['model']; + + $creditMemos = []; + /** @var CreditmemoInterface $creditMemo */ + foreach ($orderModel->getCreditmemosCollection() as $creditMemo) { + $creditMemos[] = [ + 'id' => base64_encode($creditMemo->getEntityId()), + 'number' => $creditMemo->getIncrementId(), + 'order' => $orderModel, + 'model' => $creditMemo + ]; + } + return $creditMemos; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php new file mode 100644 index 0000000000000..8807dfa390ae8 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php @@ -0,0 +1,165 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\InputException; +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\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\SalesGraphQl\Model\Order\OrderAddress; +use Magento\SalesGraphQl\Model\Order\OrderPayments; +use Magento\SalesGraphQl\Model\Resolver\CustomerOrders\Query\OrderFilter; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Orders data resolver + */ +class CustomerOrders implements ResolverInterface +{ + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var OrderAddress + */ + private $orderAddress; + + /** + * @var OrderPayments + */ + private $orderPayments; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var OrderFilter + */ + private $orderFilter; + + /** + * @param OrderRepositoryInterface $orderRepository + * @param OrderAddress $orderAddress + * @param OrderPayments $orderPayments + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param OrderFilter $orderFilter + */ + public function __construct( + OrderRepositoryInterface $orderRepository, + OrderAddress $orderAddress, + OrderPayments $orderPayments, + SearchCriteriaBuilder $searchCriteriaBuilder, + OrderFilter $orderFilter + ) { + $this->orderRepository = $orderRepository; + $this->orderAddress = $orderAddress; + $this->orderPayments = $orderPayments; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->orderFilter = $orderFilter; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + if ($args['currentPage'] < 1) { + throw new GraphQlInputException(__('currentPage value must be greater than 0.')); + } + if ($args['pageSize'] < 1) { + throw new GraphQlInputException(__('pageSize value must be greater than 0.')); + } + $userId = $context->getUserId(); + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + try { + $searchResult = $this->getSearchResult($args, (int)$userId, (int)$store->getId()); + $maxPages = (int)ceil($searchResult->getTotalCount() / $searchResult->getPageSize()); + } catch (InputException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + return [ + 'total_count' => $searchResult->getTotalCount(), + 'items' => $this->formatOrdersArray($searchResult->getItems()), + 'page_info' => [ + 'page_size' => $searchResult->getPageSize(), + 'current_page' => $searchResult->getCurPage(), + 'total_pages' => $maxPages, + ] + ]; + } + + /** + * Format order models for graphql schema + * + * @param OrderInterface[] $orderModels + * @return array + */ + private function formatOrdersArray(array $orderModels) + { + $ordersArray = []; + foreach ($orderModels as $orderModel) { + $ordersArray[] = [ + 'created_at' => $orderModel->getCreatedAt(), + 'grand_total' => $orderModel->getGrandTotal(), + 'id' => base64_encode($orderModel->getEntityId()), + 'increment_id' => $orderModel->getIncrementId(), + 'number' => $orderModel->getIncrementId(), + 'order_date' => $orderModel->getCreatedAt(), + 'order_number' => $orderModel->getIncrementId(), + 'status' => $orderModel->getStatusLabel(), + 'shipping_method' => $orderModel->getShippingDescription(), + 'shipping_address' => $this->orderAddress->getOrderShippingAddress($orderModel), + 'billing_address' => $this->orderAddress->getOrderBillingAddress($orderModel), + 'payment_methods' => $this->orderPayments->getOrderPaymentMethod($orderModel), + 'model' => $orderModel, + ]; + } + return $ordersArray; + } + + /** + * Get search result from graphql query arguments + * + * @param array $args + * @param int $userId + * @param int $storeId + * @return \Magento\Sales\Api\Data\OrderSearchResultInterface + * @throws InputException + */ + private function getSearchResult(array $args, int $userId, int $storeId) + { + $filterGroups = $this->orderFilter->createFilterGroups($args, $userId, (int)$storeId); + $this->searchCriteriaBuilder->setFilterGroups($filterGroups); + if (isset($args['currentPage'])) { + $this->searchCriteriaBuilder->setCurrentPage($args['currentPage']); + } + if (isset($args['pageSize'])) { + $this->searchCriteriaBuilder->setPageSize($args['pageSize']); + } + return $this->orderRepository->getList($this->searchCriteriaBuilder->create()); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Carrier.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Carrier.php new file mode 100644 index 0000000000000..8fae5c3d19d20 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Carrier.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CustomerOrders; + +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\Sales\Model\Order; +use Magento\Shipping\Model\Config\Source\Allmethods; + +/** + * Resolve shipping carrier for order + */ +class Carrier implements ResolverInterface +{ + /** + * @var Allmethods + */ + private $carrierMethods; + + /** + * @param Allmethods $carrierMethods + */ + public function __construct(Allmethods $carrierMethods) + { + $this->carrierMethods = $carrierMethods; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model']) && !($value['model'] instanceof Order)) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Order $order */ + $order = $value['model']; + $methodCode = $order->getShippingMethod(); + if (null === $methodCode) { + return null; + } + + return $this->findCarrierByMethodCode($methodCode); + } + + /** + * Find carrier name by shipping method code + * + * @param string $methodCode + * @return string + */ + private function findCarrierByMethodCode(string $methodCode): ?string + { + $allCarrierMethods = $this->carrierMethods->toOptionArray(); + + foreach ($allCarrierMethods as $carrierMethods) { + $carrierLabel = $carrierMethods['label']; + $carrierMethodOptions = $carrierMethods['value']; + if (is_array($carrierMethodOptions)) { + foreach ($carrierMethodOptions as $option) { + if ($option['value'] === $methodCode) { + return $carrierLabel; + } + } + } + } + return null; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderFilter.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderFilter.php new file mode 100644 index 0000000000000..b14b05042bb4d --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderFilter.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CustomerOrders\Query; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Api\Search\FilterGroup; + +/** + * Order filter allows to filter collection using 'increment_id' as order number, from the search criteria. + */ +class OrderFilter +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Translator field from graphql to collection field + * + * @var string[] + */ + private $fieldTranslatorArray = [ + 'number' => 'increment_id', + ]; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param FilterBuilder $filterBuilder + * @param FilterGroupBuilder $filterGroupBuilder + * @param string[] $fieldTranslatorArray + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + FilterBuilder $filterBuilder, + FilterGroupBuilder $filterGroupBuilder, + array $fieldTranslatorArray = [] + ) { + $this->filterBuilder = $filterBuilder; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->scopeConfig = $scopeConfig; + $this->fieldTranslatorArray = array_replace($this->fieldTranslatorArray, $fieldTranslatorArray); + } + + /** + * Create filter for filtering the requested categories id's based on url_key, ids, name in the result. + * + * @param array $args + * @param int $userId + * @param int $storeId + * @return FilterGroup[] + */ + public function createFilterGroups( + array $args, + int $userId, + int $storeId + ): array { + $filterGroups = []; + $this->filterGroupBuilder->setFilters( + [$this->filterBuilder->setField('customer_id')->setValue($userId)->setConditionType('eq')->create()] + ); + $filterGroups[] = $this->filterGroupBuilder->create(); + + $this->filterGroupBuilder->setFilters( + [$this->filterBuilder->setField('store_id')->setValue($storeId)->setConditionType('eq')->create()] + ); + $filterGroups[] = $this->filterGroupBuilder->create(); + + if (isset($args['filter'])) { + $filters = []; + foreach ($args['filter'] as $field => $cond) { + if (isset($this->fieldTranslatorArray[$field])) { + $field = $this->fieldTranslatorArray[$field]; + } + foreach ($cond as $condType => $value) { + if ($condType === 'match') { + if (is_array($value)) { + throw new InputException(__('Invalid match filter')); + } + $searchValue = str_replace('%', '', $value); + $filters[] = $this->filterBuilder->setField($field) + ->setValue("%{$searchValue}%") + ->setConditionType('like') + ->create(); + } else { + $filters[] = $this->filterBuilder->setField($field) + ->setValue($value) + ->setConditionType($condType) + ->create(); + } + } + } + + $this->filterGroupBuilder->setFilters($filters); + $filterGroups[] = $this->filterGroupBuilder->create(); + } + return $filterGroups; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceItems.php new file mode 100644 index 0000000000000..1e9d282d80d94 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceItems.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\Invoice; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\InvoiceItemInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\SalesGraphQl\Model\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolver for Invoice Items + */ +class InvoiceItems implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct( + ValueFactory $valueFactory, + OrderItemProvider $orderItemProvider + ) { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof InvoiceInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (!(($value['order'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"order" value should be specified')); + } + + /** @var InvoiceInterface $invoiceModel */ + $invoiceModel = $value['model']; + /** @var OrderInterface $parentOrderModel */ + $parentOrderModel = $value['order']; + + return $this->valueFactory->create( + $this->getInvoiceItems($parentOrderModel, $invoiceModel->getItems()) + ); + } + + /** + * Get invoice items data as promise + * + * @param OrderInterface $order + * @param array $invoiceItems + * @return \Closure + */ + public function getInvoiceItems(OrderInterface $order, array $invoiceItems): \Closure + { + $itemsList = []; + foreach ($invoiceItems as $Item) { + $this->orderItemProvider->addOrderItemId((int)$Item->getOrderItemId()); + } + return function () use ($order, $invoiceItems, $itemsList): array { + foreach ($invoiceItems as $invoiceItem) { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$invoiceItem->getOrderItemId()); + /** @var OrderItemInterface $orderItemModel */ + $orderItemModel = $orderItem['model']; + if (!$orderItemModel->getParentItem()) { + $invoiceItemData = $this->getInvoiceItemData($order, $invoiceItem); + if (!empty($invoiceItemData)) { + $itemsList[$invoiceItem->getOrderItemId()] = $invoiceItemData; + } + } + } + return $itemsList; + }; + } + + /** + * Get formatted invoice item data + * + * @param OrderInterface $order + * @param InvoiceItemInterface $invoiceItem + * @return array + */ + private function getInvoiceItemData(OrderInterface $order, InvoiceItemInterface $invoiceItem): array + { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$invoiceItem->getOrderItemId()); + return [ + 'id' => base64_encode($invoiceItem->getEntityId()), + 'product_name' => $invoiceItem->getName(), + 'product_sku' => $invoiceItem->getSku(), + 'product_sale_price' => [ + 'value' => $invoiceItem->getPrice(), + 'currency' => $order->getOrderCurrencyCode() + ], + 'quantity_invoiced' => $invoiceItem->getQty(), + 'model' => $invoiceItem, + 'product_type' => $orderItem['product_type'], + 'order_item' => $orderItem, + 'discounts' => $this->formatDiscountDetails($order, $invoiceItem) + ]; + } + + /** + * Returns formatted information about an applied discount + * + * @param OrderInterface $associatedOrder + * @param InvoiceItemInterface $invoiceItem + * @return array + */ + private function formatDiscountDetails(OrderInterface $associatedOrder, InvoiceItemInterface $invoiceItem) : array + { + if ($associatedOrder->getDiscountDescription() === null + && $invoiceItem->getDiscountAmount() == 0 + && $associatedOrder->getDiscountAmount() == 0 + ) { + $discounts = []; + } else { + $discounts[] = [ + 'label' => $associatedOrder->getDiscountDescription() ?? _('Discount'), + 'amount' => [ + 'value' => abs($invoiceItem->getDiscountAmount()) ?? 0, + 'currency' => $associatedOrder->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceTotal.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceTotal.php new file mode 100644 index 0000000000000..b77fda9523843 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceTotal.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\Invoice; + +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\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\SalesGraphQl\Model\SalesItem\ShippingTaxCalculator; +use Magento\Tax\Helper\Data as TaxHelper; + +/** + * Resolver for Invoice total + */ +class InvoiceTotal implements ResolverInterface +{ + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var OrderTaxManagementInterface + */ + private $orderTaxManagement; + + /** + * @var ShippingTaxCalculator + */ + private $shippingTaxCalculator; + + /** + * @param OrderTaxManagementInterface $orderTaxManagement + * @param TaxHelper $taxHelper + * @param ShippingTaxCalculator $shippingTaxCalculator + */ + public function __construct( + OrderTaxManagementInterface $orderTaxManagement, + TaxHelper $taxHelper, + ShippingTaxCalculator $shippingTaxCalculator + ) { + $this->taxHelper = $taxHelper; + $this->orderTaxManagement = $orderTaxManagement; + $this->shippingTaxCalculator = $shippingTaxCalculator; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof InvoiceInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (!(($value['order'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"order" value should be specified')); + } + + /** @var OrderInterface $orderModel */ + $orderModel = $value['order']; + /** @var InvoiceInterface $invoiceModel */ + $invoiceModel = $value['model']; + $currency = $orderModel->getOrderCurrencyCode(); + $baseCurrency = $orderModel->getBaseCurrencyCode(); + return [ + 'base_grand_total' => ['value' => $invoiceModel->getBaseGrandTotal(), 'currency' => $baseCurrency], + 'grand_total' => ['value' => $invoiceModel->getGrandTotal(), 'currency' => $currency], + 'subtotal' => ['value' => $invoiceModel->getSubtotal(), 'currency' => $currency], + 'total_tax' => ['value' => $invoiceModel->getTaxAmount(), 'currency' => $currency], + 'total_shipping' => ['value' => $invoiceModel->getShippingAmount(), 'currency' => $currency], + 'discounts' => $this->getDiscountDetails($invoiceModel), + 'taxes' => $this->formatTaxes( + $orderModel, + $this->taxHelper->getCalculatedTaxes($invoiceModel), + ), + 'shipping_handling' => [ + 'amount_excluding_tax' => [ + 'value' => $invoiceModel->getShippingAmount() ?? 0, + 'currency' => $currency + ], + 'amount_including_tax' => [ + 'value' => $invoiceModel->getShippingInclTax() ?? 0, + 'currency' => $currency + ], + 'total_amount' => [ + 'value' => $invoiceModel->getShippingAmount() ?? 0, + 'currency' => $currency + ], + 'discounts' => $this->getShippingDiscountDetails($invoiceModel, $orderModel), + 'taxes' => $this->formatTaxes( + $orderModel, + $this->shippingTaxCalculator->calculateShippingTaxes($orderModel, $invoiceModel), + ) + ] + ]; + } + + /** + * Return information about an applied discount on shipping + * + * @param InvoiceInterface $invoiceModel + * @param OrderInterface $orderModel + * @return array + */ + private function getShippingDiscountDetails(InvoiceInterface $invoiceModel, OrderInterface $orderModel): array + { + $invoiceShippingAmount = (float)$invoiceModel->getShippingAmount(); + $orderShippingAmount = (float)$orderModel->getShippingAmount(); + $calculatedShippingRatioFromOriginal = $invoiceShippingAmount != 0 && $orderShippingAmount != 0 ? + ($invoiceShippingAmount / $orderShippingAmount) : 0; + $orderShippingDiscount = (float)$orderModel->getShippingDiscountAmount(); + $calculatedInvoiceShippingDiscount = $orderShippingDiscount * $calculatedShippingRatioFromOriginal; + $shippingDiscounts = []; + if ($calculatedInvoiceShippingDiscount != 0) { + $shippingDiscounts[] = + [ + 'amount' => [ + 'value' => sprintf('%.2f', abs($calculatedInvoiceShippingDiscount)), + 'currency' => $invoiceModel->getOrderCurrencyCode() + ] + ]; + } + return $shippingDiscounts; + } + + /** + * Return information about an applied discount + * + * @param InvoiceInterface $invoice + * @return array + */ + private function getDiscountDetails(InvoiceInterface $invoice): array + { + $discounts = []; + if (!($invoice->getDiscountDescription() === null && $invoice->getDiscountAmount() == 0)) { + $discounts[] = [ + 'label' => $invoice->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($invoice->getDiscountAmount()), + 'currency' => $invoice->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } + + /** + * Format applied taxes + * + * @param OrderInterface $order + * @param array $appliedTaxes + * @return array + */ + private function formatTaxes(OrderInterface $order, array $appliedTaxes): array + { + $taxes = []; + foreach ($appliedTaxes as $appliedTax) { + $appliedTaxesArray = [ + 'rate' => $appliedTax['percent'] ?? 0, + 'title' => $appliedTax['title'] ?? null, + 'amount' => [ + 'value' => $appliedTax['tax_amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $taxes[] = $appliedTaxesArray; + } + return $taxes; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php new file mode 100644 index 0000000000000..f106752075c25 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\InvoiceInterface; + +/** + * Resolver for Invoice + */ +class Invoices implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var OrderInterface $orderModel */ + $orderModel = $value['model']; + $invoices = []; + /** @var InvoiceInterface $invoice */ + foreach ($orderModel->getInvoiceCollection() as $invoice) { + $invoices[] = [ + 'id' => base64_encode($invoice->getEntityId()), + 'number' => $invoice['increment_id'], + 'model' => $invoice, + 'order' => $orderModel + ]; + } + return $invoices; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem.php new file mode 100644 index 0000000000000..d7819277e56db --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\SalesGraphQl\Model\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolve a single order item + */ +class OrderItem implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct(ValueFactory $valueFactory, OrderItemProvider $orderItemProvider) + { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + $parentItem = $value['model']; + + if (!method_exists($parentItem, 'getOrderItemId')) { + throw new LocalizedException(__('Unable to find associated order item.')); + } + + $orderItemId = $parentItem->getOrderItemId(); + $this->orderItemProvider->addOrderItemId((int)$orderItemId); + + return $this->valueFactory->create(function () use ($parentItem) { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$parentItem->getOrderItemId()); + return empty($orderItem) ? null : $orderItem; + }); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php new file mode 100644 index 0000000000000..f0e768c513cd3 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\SalesGraphQl\Model\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolve order items for order + */ +class OrderItems implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct( + ValueFactory $valueFactory, + OrderItemProvider $orderItemProvider + ) { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + /** @var ContextInterface $context */ + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var OrderInterface $parentOrder */ + $parentOrder = $value['model']; + $orderItemIds = []; + foreach ($parentOrder->getItems() as $item) { + if (!$item->getParentItemId()) { + $orderItemIds[] = (int)$item->getItemId(); + } + $this->orderItemProvider->addOrderItemId((int)$item->getItemId()); + } + $itemsList = []; + foreach ($orderItemIds as $orderItemId) { + $itemsList[] = $this->valueFactory->create( + function () use ($orderItemId) { + return $this->orderItemProvider->getOrderItemById((int)$orderItemId); + } + ); + } + return $itemsList; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php new file mode 100644 index 0000000000000..ab3ace45f336c --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php @@ -0,0 +1,205 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Resolve order totals taxes and discounts for order + */ +class OrderTotal implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var OrderInterface $order */ + $order = $value['model']; + $currency = $order->getOrderCurrencyCode(); + $baseCurrency = $order->getBaseCurrencyCode(); + + return [ + 'base_grand_total' => ['value' => $order->getBaseGrandTotal(), 'currency' => $baseCurrency], + 'grand_total' => ['value' => $order->getGrandTotal(), 'currency' => $currency], + 'subtotal' => ['value' => $order->getSubtotal(), 'currency' => $currency], + 'total_tax' => ['value' => $order->getTaxAmount(), 'currency' => $currency], + 'taxes' => $this->getAppliedTaxesDetails($order), + 'discounts' => $this->getDiscountDetails($order), + 'total_shipping' => ['value' => $order->getShippingAmount(), 'currency' => $currency], + 'shipping_handling' => [ + 'amount_excluding_tax' => [ + 'value' => $order->getShippingAmount(), + 'currency' => $order->getOrderCurrencyCode() + ], + 'amount_including_tax' => [ + 'value' => $order->getShippingInclTax(), + 'currency' => $currency + ], + 'total_amount' => [ + 'value' => $order->getShippingAmount(), + 'currency' => $currency + ], + 'taxes' => $this->getAppliedShippingTaxesDetails($order), + 'discounts' => $this->getShippingDiscountDetails($order), + ] + ]; + } + + /** + * Retrieve applied taxes that apply to the order + * + * @param OrderInterface $order + * @return array + */ + private function getAllAppliedTaxesOnOrders(OrderInterface $order): array + { + $extensionAttributes = $order->getExtensionAttributes(); + $appliedTaxes = $extensionAttributes->getAppliedTaxes() ?? []; + $allAppliedTaxOnOrders = []; + foreach ($appliedTaxes as $taxIndex => $appliedTaxesData) { + $allAppliedTaxOnOrders[$taxIndex] = [ + 'title' => $appliedTaxesData->getDataByKey('title'), + 'percent' => $appliedTaxesData->getDataByKey('percent'), + 'amount' => $appliedTaxesData->getDataByKey('amount'), + ]; + } + return $allAppliedTaxOnOrders; + } + + /** + * Return taxes applied to the current order + * + * @param OrderInterface $order + * @return array + */ + private function getAppliedTaxesDetails(OrderInterface $order): array + { + $allAppliedTaxOnOrders = $this->getAllAppliedTaxesOnOrders($order); + $taxes = []; + foreach ($allAppliedTaxOnOrders as $appliedTaxes) { + $appliedTaxesArray = [ + 'rate' => $appliedTaxes['percent'] ?? 0, + 'title' => $appliedTaxes['title'] ?? null, + 'amount' => [ + 'value' => $appliedTaxes['amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $taxes[] = $appliedTaxesArray; + } + return $taxes; + } + + /** + * Return information about an applied discount + * + * @param OrderInterface $order + * @return array + */ + private function getDiscountDetails(OrderInterface $order): array + { + $orderDiscounts = []; + if (!($order->getDiscountDescription() === null && $order->getDiscountAmount() == 0)) { + $orderDiscounts[] = [ + 'label' => $order->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($order->getDiscountAmount()), + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + } + return $orderDiscounts; + } + + /** + * Retrieve applied shipping taxes on items for the orders + * + * @param OrderInterface $order + * @return array + */ + private function getAppliedShippingTaxesForItems(OrderInterface $order): array + { + $extensionAttributes = $order->getExtensionAttributes(); + $itemAppliedTaxes = $extensionAttributes->getItemAppliedTaxes() ?? []; + $appliedShippingTaxesForItems = []; + foreach ($itemAppliedTaxes as $appliedTaxForItem) { + if ($appliedTaxForItem->getType() === "shipping") { + foreach ($appliedTaxForItem->getAppliedTaxes() ?? [] as $taxLineItem) { + $taxItemIndexTitle = $taxLineItem->getDataByKey('title'); + $appliedShippingTaxesForItems[$taxItemIndexTitle] = [ + 'title' => $taxLineItem->getDataByKey('title'), + 'percent' => $taxLineItem->getDataByKey('percent'), + 'amount' => $taxLineItem->getDataByKey('amount') + ]; + } + } + } + return $appliedShippingTaxesForItems; + } + + /** + * Return taxes applied to the current order + * + * @param OrderInterface $order + * @return array + */ + private function getAppliedShippingTaxesDetails( + OrderInterface $order + ): array { + $appliedShippingTaxesForItems = $this->getAppliedShippingTaxesForItems($order); + $shippingTaxes = []; + foreach ($appliedShippingTaxesForItems as $appliedShippingTaxes) { + $appliedShippingTaxesArray = [ + 'rate' => $appliedShippingTaxes['percent'] ?? 0, + 'title' => $appliedShippingTaxes['title'] ?? null, + 'amount' => [ + 'value' => $appliedShippingTaxes['amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $shippingTaxes[] = $appliedShippingTaxesArray; + } + return $shippingTaxes; + } + + /** + * Return information about an applied discount + * + * @param OrderInterface $order + * @return array + */ + private function getShippingDiscountDetails(OrderInterface $order): array + { + $shippingDiscounts = []; + if (!($order->getDiscountDescription() === null && $order->getShippingDiscountAmount() == 0)) { + $shippingDiscounts[] = + [ + 'label' => $order->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($order->getShippingDiscountAmount()), + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + } + return $shippingDiscounts; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php index 8d81afeab4c90..25a79fa5d3b6c 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php @@ -12,6 +12,7 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Sales\Model\Order; use Magento\Sales\Model\ResourceModel\Order\CollectionFactoryInterface; /** @@ -34,7 +35,7 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc */ public function resolve( Field $field, @@ -51,7 +52,7 @@ public function resolve( $items = []; $orders = $this->collectionFactory->create($context->getUserId()); - /** @var \Magento\Sales\Model\Order $order */ + /** @var Order $order */ foreach ($orders as $order) { $items[] = [ 'id' => $order->getId(), diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php index 8bf4220d1ec3d..70c411c379b62 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php @@ -49,7 +49,7 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc */ public function resolve( Field $field, diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentItems.php new file mode 100644 index 0000000000000..dceb2848bda5b --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentItems.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\Shipment; + +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\Sales\Api\Data\ShipmentInterface; +use Magento\SalesGraphQl\Model\Shipment\ItemProvider; + +/** + * Resolve items included in shipment + */ +class ShipmentItems implements ResolverInterface +{ + /** + * @var ItemProvider + */ + private $shipmentItemProvider; + + /** + * @param ItemProvider $shipmentItemProvider + */ + public function __construct(ItemProvider $shipmentItemProvider) + { + $this->shipmentItemProvider = $shipmentItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!($value['model'] ?? null) instanceof ShipmentInterface) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var ShipmentInterface $shipment */ + $shipment = $value['model']; + + return $this->shipmentItemProvider->getItemData($shipment); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentTracking.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentTracking.php new file mode 100644 index 0000000000000..e6ef0b8442852 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentTracking.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\Shipment; + +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\Sales\Api\Data\ShipmentInterface; + +/** + * Resolve shipment tracking information + */ +class ShipmentTracking implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model']) && !($value['model'] instanceof ShipmentInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var ShipmentInterface $shipment */ + $shipment = $value['model']; + $tracks = $shipment->getTracks(); + + $shipmentTracking = []; + foreach ($tracks as $tracking) { + $shipmentTracking[] = [ + 'title' => $tracking->getTitle(), + 'carrier' => $tracking->getCarrierCode(), + 'number' => $tracking->getTrackNumber(), + 'model' => $tracking + ]; + } + + return $shipmentTracking; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Shipments.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipments.php new file mode 100644 index 0000000000000..8b6aaad09c304 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipments.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Model\Order; + +/** + * Resolve shipment information for order + */ +class Shipments implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model']) && !($value['model'] instanceof Order)) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Order $order */ + $order = $value['model']; + $shipments = $order->getShipmentsCollection()->getItems(); + + if (empty($shipments)) { + //Order does not have any shipments + return []; + } + + $orderShipments = []; + foreach ($shipments as $shipment) { + $orderShipments[] = + [ + 'id' => base64_encode($shipment->getIncrementId()), + 'number' => $shipment->getIncrementId(), + 'comments' => $this->getShipmentComments($shipment), + 'model' => $shipment, + 'order' => $order + ]; + } + return $orderShipments; + } + + /** + * Get comments shipments in proper format + * + * @param ShipmentInterface $shipment + * @return array + */ + private function getShipmentComments(ShipmentInterface $shipment): array + { + $comments = []; + foreach ($shipment->getComments() as $comment) { + if ($comment->getIsVisibleOnFront()) { + $comments[] = [ + 'timestamp' => $comment->getCreatedAt(), + 'message' => $comment->getComment() + ]; + } + } + return $comments; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/SalesItem/ShippingTaxCalculator.php b/app/code/Magento/SalesGraphQl/Model/SalesItem/ShippingTaxCalculator.php new file mode 100644 index 0000000000000..b063918de6ec0 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/SalesItem/ShippingTaxCalculator.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\SalesItem; + +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\EntityInterface; +use Magento\Tax\Api\Data\OrderTaxDetailsItemInterface; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Quote\Model\Quote\Address; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Calculates shipping taxes for sales items (Invoices, Credit memo) + */ +class ShippingTaxCalculator +{ + /** + * @var OrderTaxManagementInterface + */ + private $orderTaxManagement; + + /** + * @param OrderTaxManagementInterface $orderTaxManagement + */ + public function __construct( + OrderTaxManagementInterface $orderTaxManagement + ) { + $this->orderTaxManagement = $orderTaxManagement; + } + + /** + * Calculate shipping taxes for sales item + * + * @param OrderInterface $order + * @param EntityInterface $salesItem + * @return array + * @throws NoSuchEntityException + */ + public function calculateShippingTaxes( + OrderInterface $order, + EntityInterface $salesItem + ): array { + $orderTaxDetails = $this->orderTaxManagement->getOrderTaxDetails($order->getId()); + $taxClassBreakdown = []; + // Apply any taxes for shipping + $shippingTaxAmount = $salesItem->getShippingTaxAmount(); + $originalShippingTaxAmount = $order->getShippingTaxAmount(); + if ($shippingTaxAmount && $originalShippingTaxAmount && + $shippingTaxAmount != 0 && (float)$originalShippingTaxAmount + ) { + //An invoice or credit memo can have a different qty than its order + $shippingRatio = $shippingTaxAmount / $originalShippingTaxAmount; + $itemTaxDetails = $orderTaxDetails->getItems(); + foreach ($itemTaxDetails as $itemTaxDetail) { + //Aggregate taxable items associated with shipping + if ($itemTaxDetail->getType() == Address::TYPE_SHIPPING) { + $taxClassBreakdown = $this->aggregateTaxes($taxClassBreakdown, $itemTaxDetail, $shippingRatio); + } + } + } + return $taxClassBreakdown; + } + + /** + * Accumulates the pre-calculated taxes for each tax class + * + * This method accepts and returns the '$taxClassBreakdown' array with format: + * array( + * $index => array( + * 'tax_amount' => $taxAmount, + * 'base_tax_amount' => $baseTaxAmount, + * 'title' => $title, + * 'percent' => $percent + * ) + * ) + * + * @param array $taxClassBreakdown + * @param OrderTaxDetailsItemInterface $itemTaxDetail + * @param float $taxRatio + * @return array + */ + private function aggregateTaxes( + array $taxClassBreakdown, + OrderTaxDetailsItemInterface $itemTaxDetail, + float $taxRatio + ): array { + $itemAppliedTaxes = $itemTaxDetail->getAppliedTaxes(); + foreach ($itemAppliedTaxes as $itemAppliedTax) { + $taxAmount = $itemAppliedTax->getAmount() * $taxRatio; + $baseTaxAmount = $itemAppliedTax->getBaseAmount() * $taxRatio; + if (0 == $taxAmount && 0 == $baseTaxAmount) { + continue; + } + $taxCode = $itemAppliedTax->getCode(); + if (!isset($taxClassBreakdown[$taxCode])) { + $taxClassBreakdown[$taxCode]['title'] = $itemAppliedTax->getTitle(); + $taxClassBreakdown[$taxCode]['percent'] = $itemAppliedTax->getPercent(); + $taxClassBreakdown[$taxCode]['tax_amount'] = $taxAmount; + $taxClassBreakdown[$taxCode]['base_tax_amount'] = $baseTaxAmount; + } else { + $taxClassBreakdown[$taxCode]['tax_amount'] += $taxAmount; + $taxClassBreakdown[$taxCode]['base_tax_amount'] += $baseTaxAmount; + } + } + return $taxClassBreakdown; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Shipment/Item/FormatterInterface.php b/app/code/Magento/SalesGraphQl/Model/Shipment/Item/FormatterInterface.php new file mode 100644 index 0000000000000..61ea89b5a81e6 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Shipment/Item/FormatterInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Shipment\Item; + +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\ShipmentItemInterface; + +/** + * Format shipment items for GraphQl output + */ +interface FormatterInterface +{ + /** + * Format a shipment item for GraphQl + * + * @param ShipmentInterface $shipment + * @param ShipmentItemInterface $item + * @return array|null + */ + public function formatShipmentItem(ShipmentInterface $shipment, ShipmentItemInterface $item): ?array; +} diff --git a/app/code/Magento/SalesGraphQl/Model/Shipment/Item/ShipmentItemFormatter.php b/app/code/Magento/SalesGraphQl/Model/Shipment/Item/ShipmentItemFormatter.php new file mode 100644 index 0000000000000..e8ba6e5f784ec --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Shipment/Item/ShipmentItemFormatter.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Shipment\Item; + +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\ShipmentItemInterface; + +/** + * Format shipment item for GraphQl output + */ +class ShipmentItemFormatter implements FormatterInterface +{ + /** + * @inheritDoc + */ + public function formatShipmentItem(ShipmentInterface $shipment, ShipmentItemInterface $item): ?array + { + $order = $shipment->getOrder(); + return [ + 'id' => base64_encode($item->getEntityId()), + 'product_name' => $item->getName(), + 'product_sku' => $item->getSku(), + 'product_sale_price' => [ + 'value' => $item->getPrice(), + 'currency' => $order->getOrderCurrencyCode() + ], + 'product_type' => $item->getOrderItem()->getProductType(), + 'quantity_shipped' => $item->getQty(), + 'model' => $item, + ]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Shipment/ItemProvider.php b/app/code/Magento/SalesGraphQl/Model/Shipment/ItemProvider.php new file mode 100644 index 0000000000000..49f8e3b119da2 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Shipment/ItemProvider.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Shipment; + +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\ShipmentItemInterface; +use Magento\SalesGraphQl\Model\Shipment\Item\FormatterInterface; + +/** + * Get shipment item data + */ +class ItemProvider +{ + /** + * @var FormatterInterface[] + */ + private $formatters; + + /** + * @param FormatterInterface[] $formatters + */ + public function __construct(array $formatters = []) + { + $this->formatters = $formatters; + } + + /** + * Get item data for shipment + * + * @param ShipmentInterface $shipment + * @return array + */ + public function getItemData(ShipmentInterface $shipment): array + { + $shipmentItems = []; + + foreach ($shipment->getItems() as $shipmentItem) { + $formattedItem = $this->formatItem($shipment, $shipmentItem); + if ($formattedItem) { + $shipmentItems[] = $formattedItem; + } + } + return $shipmentItems; + } + + /** + * Format individual shipment item + * + * @param ShipmentInterface $shipment + * @param ShipmentItemInterface $shipmentItem + * @return array|null + */ + private function formatItem(ShipmentInterface $shipment, ShipmentItemInterface $shipmentItem): ?array + { + $orderItem = $shipmentItem->getOrderItem(); + $formatter = $this->formatters[$orderItem->getProductType()] ?? $this->formatters['default']; + + return $formatter->formatShipmentItem($shipment, $shipmentItem); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/TypeResolver/CreditMemoItem.php b/app/code/Magento/SalesGraphQl/Model/TypeResolver/CreditMemoItem.php new file mode 100644 index 0000000000000..8fab97153231b --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/TypeResolver/CreditMemoItem.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Resolve concrete type for CreditMemoItemInterface + */ +class CreditMemoItem implements TypeResolverInterface +{ + /** + * @var array + */ + private $productTypeMap; + + /** + * @param array $productTypeMap + */ + public function __construct($productTypeMap = []) + { + $this->productTypeMap = $productTypeMap; + } + + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (!isset($data['product_type'])) { + throw new GraphQlInputException(__('Missing key %1 in sales item data', ['product_type'])); + } + if (isset($this->productTypeMap[$data['product_type']])) { + return $this->productTypeMap[$data['product_type']]; + } + return $this->productTypeMap['default']; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/TypeResolver/InvoiceItem.php b/app/code/Magento/SalesGraphQl/Model/TypeResolver/InvoiceItem.php new file mode 100644 index 0000000000000..e4ceab02fbbe9 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/TypeResolver/InvoiceItem.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Resolve concrete type for InvoiceItemInterface + */ +class InvoiceItem implements TypeResolverInterface +{ + /** + * @var array + */ + private $productTypeMap; + + /** + * @param array $productTypeMap + */ + public function __construct($productTypeMap = []) + { + $this->productTypeMap = $productTypeMap; + } + + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (!isset($data['product_type'])) { + throw new GraphQlInputException(__('Missing key %1 in sales item data', ['product_type'])); + } + if (isset($this->productTypeMap[$data['product_type']])) { + return $this->productTypeMap[$data['product_type']]; + } + return $this->productTypeMap['default']; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/TypeResolver/OrderItem.php b/app/code/Magento/SalesGraphQl/Model/TypeResolver/OrderItem.php new file mode 100644 index 0000000000000..851a0daf2f50d --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/TypeResolver/OrderItem.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Resolve concrete type for OrderItemInterface + */ +class OrderItem implements TypeResolverInterface +{ + /** + * @var array + */ + private $productTypeMap; + + /** + * @param array $productTypeMap + */ + public function __construct(array $productTypeMap = []) + { + $this->productTypeMap = $productTypeMap; + } + + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (!isset($data['product_type'])) { + throw new GraphQlInputException(__('Missing key %1 in sales item data', ['product_type'])); + } + if (isset($this->productTypeMap[$data['product_type']])) { + return $this->productTypeMap[$data['product_type']]; + } + + return $this->productTypeMap['default']; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/TypeResolver/ShipmentItem.php b/app/code/Magento/SalesGraphQl/Model/TypeResolver/ShipmentItem.php new file mode 100644 index 0000000000000..fd72a8729af27 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/TypeResolver/ShipmentItem.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Resolve concrete type of ShipmentItemInterface + */ +class ShipmentItem implements TypeResolverInterface +{ + /** + * @var array + */ + private $productTypeMap; + + /** + * @param array $productTypeMap + */ + public function __construct(array $productTypeMap = []) + { + $this->productTypeMap = $productTypeMap; + } + + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (!isset($data['product_type'])) { + throw new GraphQlInputException(__('Missing key %1 in sales item data', ['product_type'])); + } + if (isset($this->productTypeMap[$data['product_type']])) { + return $this->productTypeMap[$data['product_type']]; + } + + return $this->productTypeMap['default']; + } +} diff --git a/app/code/Magento/SalesGraphQl/composer.json b/app/code/Magento/SalesGraphQl/composer.json index 8e9d95836e189..b85d8c0f852da 100644 --- a/app/code/Magento/SalesGraphQl/composer.json +++ b/app/code/Magento/SalesGraphQl/composer.json @@ -6,7 +6,12 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-sales": "*", - "magento/module-graph-ql": "*" + "magento/module-store": "*", + "magento/module-catalog": "*", + "magento/module-tax": "*", + "magento/module-quote": "*", + "magento/module-graph-ql": "*", + "magento/module-shipping": "*" }, "license": [ "OSL-3.0", diff --git a/app/code/Magento/SalesGraphQl/etc/graphql/di.xml b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..b40d8e9331bbb --- /dev/null +++ b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml @@ -0,0 +1,45 @@ +<?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\SalesGraphQl\Model\TypeResolver\OrderItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="default" xsi:type="string">OrderItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\InvoiceItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="default" xsi:type="string">InvoiceItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\CreditMemoItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="default" xsi:type="string">CreditMemoItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\ShipmentItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="default" xsi:type="string">ShipmentItem</item> + </argument> + </arguments> + </type> + <preference for="Magento\SalesGraphQl\Model\Shipment\Item\FormatterInterface" type="Magento\SalesGraphQl\Model\Shipment\Item\ShipmentItemFormatter"/> + <type name="Magento\SalesGraphQl\Model\Shipment\ItemProvider"> + <arguments> + <argument name="formatters" xsi:type="array"> + <item name="default" xsi:type="object">Magento\SalesGraphQl\Model\Shipment\Item\ShipmentItemFormatter\Proxy</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index f823c25cf2d9f..8b9d58e48d4b1 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -2,20 +2,7 @@ # See COPYING.txt for license details. type Query { - customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @doc(description: "List of customer orders") @cache(cacheable: false) -} - -type CustomerOrder @doc(description: "Order mapping fields") { - id: Int - increment_id: String @deprecated(reason: "Use the order_number instead.") - order_number: String! @doc(description: "The order number") - created_at: String - grand_total: Float - status: String -} - -type CustomerOrders { - items: [CustomerOrder] @doc(description: "Array of orders") + customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @deprecated(reason: "Use orders from customer instead") @cache(cacheable: false) } type Mutation { @@ -33,6 +20,227 @@ type CheckoutUserInputError @doc(description:"An error encountered while adding code: CheckoutUserInputErrorCodes! @doc(description: "Checkout-specific error code") } +type Customer { + orders ( + filter: CustomerOrdersFilterInput @doc(description: "Defines the filter to use for searching customer orders"), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1"), + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. The default value is 20"), + ): CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CustomerOrders") @cache(cacheable: false) +} + +input CustomerOrdersFilterInput @doc(description: "Identifies the filter to use for filtering orders.") { + number: FilterStringTypeInput @doc(description: "Filters by order number.") +} + +type CustomerOrders @doc(description: "The collection of orders that match the conditions defined in the filter") { + items: [CustomerOrder]! @doc(description: "An array of customer orders") + page_info: SearchResultPageInfo @doc(description: "An object that includes the current_page, page_info, and page_size values specified in the query") + total_count: Int @doc(description: "The total count of customer orders") +} + +type CustomerOrder @doc(description: "Contains details about each of the customer's orders") { + id: ID! @doc(description: "Unique identifier for the order") + order_date: String! @doc(description: "The date the order was placed") + status: String! @doc(description: "The current status of the order") + number: String! @doc(description: "The order number") + items: [OrderItemInterface] @doc(description: "An array containing the items purchased in this order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItems") + total: OrderTotal @doc(description: "Contains details about the calculated totals for this order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderTotal") + invoices: [Invoice]! @doc(description: "A list of invoices for the order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoices") + shipments: [OrderShipment] @doc(description: "A list of shipments for the order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Shipments") + credit_memos: [CreditMemo] @doc(description: "A list of credit memos") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemos") + payment_methods: [PaymentMethod] @doc(description: "Payment details for the order") + shipping_address: OrderAddress @doc(description: "The shipping address for the order") + billing_address: OrderAddress @doc(description: "The billing address for the order") + carrier: String @doc(description: "The shipping carrier for the order delivery") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CustomerOrders\\Carrier") + shipping_method: String @doc(description: "The delivery method for the order") + comments: [CommentItem] @doc(description: "Comments about the order") + increment_id: String @deprecated(reason: "Use the id attribute instead") + order_number: String! @deprecated(reason: "Use the number attribute instead") + created_at: String @deprecated(reason: "Use the order_date attribute instead") + grand_total: Float @deprecated(reason: "Use the totals.grand_total attribute instead") +} + +type OrderAddress @doc(description: "OrderAddress contains detailed information about an order's billing and shipping addresses"){ + firstname: String! @doc(description: "The first name of the person associated with the shipping/billing address") + lastname: String! @doc(description: "The family name of the person associated with the shipping/billing address") + middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") + region: String @doc(description: "The state or province name") + region_id: ID @doc(description: "The unique ID for a pre-defined region") + country_code: CountryCodeEnum @doc(description: "The customer's country") + street: [String!]! @doc(description: "An array of strings that define the street number and name") + company: String @doc(description: "The customer's company") + telephone: String! @doc(description: "The telephone number") + fax: String @doc(description: "The fax number") + postcode: String @doc(description: "The customer's order ZIP or postal code") + city: String! @doc(description: "The city or town") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") +} + +interface OrderItemInterface @doc(description: "Order item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\OrderItem") { + id: ID! @doc(description: "The unique identifier of the order item") + product_name: String @doc(description: "The name of the base product") + product_sku: String! @doc(description: "The SKU of the base product") + product_url_key: String @doc(description: "URL key of the base product") + product_type: String @doc(description: "The type of product, such as simple, configurable, etc.") + status: String @doc(description: "The status of the order item") + product_sale_price: Money! @doc(description: "The sale price of the base product, including selected options") + discounts: [Discount] @doc(description: "The final discount information for the product") + selected_options: [OrderItemOption] @doc(description: "The selected options for the base product, such as color or size") + entered_options: [OrderItemOption] @doc(description: "The entered option for the base product, such as a logo or image") + quantity_ordered: Float @doc(description: "The number of units ordered for this item") + quantity_shipped: Float @doc(description: "The number of shipped items") + quantity_refunded: Float @doc(description: "The number of refunded items") + quantity_invoiced: Float @doc(description: "The number of invoiced items") + quantity_canceled: Float @doc(description: "The number of canceled items") + quantity_returned: Float @doc(description: "The number of returned items") +} + +type OrderItem implements OrderItemInterface { +} + +type OrderItemOption @doc(description: "Represents order item options like selected or entered") { + id: String! @doc(description: "The name of the option") + value: String! @doc(description: "The value of the option") +} + +type TaxItem @doc(description: "The tax item details") { + amount: Money! @doc(description: "The amount of tax applied to the item") + title: String! @doc(description: "A title that describes the tax") + rate: Float! @doc(description: "The rate used to calculate the tax") +} + +type OrderTotal @doc(description: "Contains details about the sales total amounts used to calculate the final price") { + subtotal: Money! @doc(description: "The subtotal of the order, excluding shipping, discounts, and taxes") + discounts: [Discount] @doc(description: "The applied discounts to the order") + total_tax: Money! @doc(description: "The amount of tax applied to the order") + taxes: [TaxItem] @doc(description: "The order tax details") + grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes") + base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency") + total_shipping: Money! @doc(description: "The shipping amount for the order") + shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the order") +} + +type Invoice @doc(description: "Invoice details") { + id: ID! @doc(description: "The ID of the invoice, used for API purposes") + number: String! @doc(description: "Sequential invoice number") + total: InvoiceTotal @doc(description: "Invoice total amount details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoice\\InvoiceTotal") + items: [InvoiceItemInterface] @doc(description: "Invoiced product details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoice\\InvoiceItems") + comments: [CommentItem] @doc(description: "Comments on the invoice") +} + +interface InvoiceItemInterface @doc(description: "Invoice item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\InvoiceItem") { + id: ID! @doc(description: "The unique ID of the invoice item") + order_item: OrderItemInterface @doc(description: "Contains details about an individual order item") + product_name: String @doc(description: "The name of the base product") + product_sku: String! @doc(description: "The SKU of the base product") + product_sale_price: Money! @doc(description: "The sale price for the base product including selected options") + discounts: [Discount] @doc(description: "Contains information about the final discount amount for the base product, including discounts on options") + quantity_invoiced: Float @doc(description: "The number of invoiced items") +} + +type InvoiceItem implements InvoiceItemInterface { +} + +type InvoiceTotal @doc(description: "Contains price details from an invoice"){ + subtotal: Money! @doc(description: "The subtotal of the invoice, excluding shipping, discounts, and taxes") + discounts: [Discount] @doc(description: "The applied discounts to the invoice") + total_tax: Money! @doc(description: "The amount of tax applied to the invoice") + taxes: [TaxItem] @doc(description: "The invoice tax details") + grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes") + base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency") + total_shipping: Money! @doc(description: "The shipping amount for the invoice") + shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the invoice") +} + +type ShippingHandling @doc(description: "The Shipping handling details") { + total_amount: Money! @doc(description: "The total amount for shipping") + amount_including_tax: Money @doc(description: "The shipping amount, including tax") + amount_excluding_tax: Money @doc(description: "The shipping amount, excluding tax") + taxes: [TaxItem] @doc(description: "Contains details about taxes applied for shipping") + discounts: [ShippingDiscount] @doc(description: "The applied discounts to the shipping") +} + +type ShippingDiscount @doc(description:"Defines an individual shipping discount. This discount can be applied to shipping.") { + amount: Money! @doc(description:"The amount of the discount") +} + +type OrderShipment @doc(description: "Order shipment details") { + id: ID! @doc(description: "The unique ID of the shipment") + number: String! @doc(description: "The sequential credit shipment number") + tracking: [ShipmentTracking] @doc(description: "Contains shipment tracking details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Shipment\\ShipmentTracking") + items: [ShipmentItemInterface] @doc(description: "Contains items included in the shipment") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Shipment\\ShipmentItems") + comments: [CommentItem] @doc(description: "Comments added to the shipment") +} + +type CommentItem @doc(description: "Comment item details") { + timestamp: String! @doc(description: "The timestamp of the comment") + message: String! @doc(description: "The text of the message") +} + +interface ShipmentItemInterface @doc(description: "Order shipment item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\ShipmentItem"){ + id: ID! @doc(description: "Shipment item unique identifier") + order_item: OrderItemInterface @doc(description: "Associated order item") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItem") + product_name: String @doc(description: "Name of the base product") + product_sku: String! @doc(description: "SKU of the base product") + product_sale_price: Money! @doc(description: "Sale price for the base product") + quantity_shipped: Float! @doc(description: "Number of shipped items") +} + +type ShipmentItem implements ShipmentItemInterface { +} + +type ShipmentTracking @doc(description: "Order shipment tracking details") { + title: String! @doc(description: "The shipment tracking title") + carrier: String! @doc(description: "The shipping carrier for the order delivery") + number: String @doc(description: "The tracking number of the order shipment") +} + +type PaymentMethod @doc(description: "Contains details about the payment method used to pay for the order") { + name: String! @doc(description: "The label that describes the payment method") + type: String! @doc(description: "The payment method code that indicates how the order was paid for") + additional_data: [KeyValue] @doc(description: "Additional data per payment method type") +} + +type CreditMemo @doc(description: "Credit memo details") { + id: ID! @doc(description: "The unique ID of the credit memo, used for API purposes") + number: String! @doc(description: "The sequential credit memo number") + items: [CreditMemoItemInterface] @doc(description: "An array containing details about refunded items") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemo\\CreditMemoItems") + total: CreditMemoTotal @doc(description: "Contains details about the total refunded amount") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemo\\CreditMemoTotal") + comments: [CommentItem] @doc(description: "Comments on the credit memo") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemo\\CreditMemoComments") +} + +interface CreditMemoItemInterface @doc(description: "Credit memo item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\CreditMemoItem") { + id: ID! @doc(description: "The unique ID of the credit memo item, used for API purposes") + order_item: OrderItemInterface @doc(description: "The order item the credit memo is applied to") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItem") + product_name: String @doc(description: "The name of the base product") + product_sku: String! @doc(description: "SKU of the base product") + product_sale_price: Money! @doc(description: "The sale price for the base product, including selected options") + discounts: [Discount] @doc(description: "Contains information about the final discount amount for the base product, including discounts on options") + quantity_refunded: Float @doc(description: "The number of refunded items") +} + +type CreditMemoItem implements CreditMemoItemInterface { +} + +type CreditMemoTotal @doc(description: "Credit memo price details") { + subtotal: Money! @doc(description: "The subtotal of the invoice, excluding shipping, discounts, and taxes") + discounts: [Discount] @doc(description: "The applied discounts to the credit memo") + total_tax: Money! @doc(description: "The amount of tax applied to the credit memo") + taxes: [TaxItem] @doc(description: "The credit memo tax details") + grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes") + base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency") + total_shipping: Money! @doc(description: "The shipping amount for the credit memo") + shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the credit memo") + adjustment: Money! @doc(description: "An adjustment manually applied to the order") +} + +type KeyValue @doc(description: "The key-value type") { + name: String @doc(description: "The name part of the name/value pair") + value: String @doc(description: "The value part of the name/value pair") +} + enum CheckoutUserInputErrorCodes { REORDER_NOT_AVAILABLE PRODUCT_NOT_FOUND diff --git a/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php b/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php index 3f2ba38fa5a55..2739226c5fb5a 100644 --- a/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php +++ b/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php @@ -12,6 +12,7 @@ * Class ReturnProcessor * * @api + * @since 100.0.0 */ class ReturnProcessor { @@ -68,6 +69,7 @@ public function __construct( * @param array $returnToStockItems * @param bool $isAutoReturn * @return void + * @since 100.0.0 */ public function execute( CreditmemoInterface $creditmemo, diff --git a/app/code/Magento/SalesRule/Api/Data/CouponInterface.php b/app/code/Magento/SalesRule/Api/Data/CouponInterface.php index bd44ea829fe66..2ab731f2f7974 100644 --- a/app/code/Magento/SalesRule/Api/Data/CouponInterface.php +++ b/app/code/Magento/SalesRule/Api/Data/CouponInterface.php @@ -110,7 +110,7 @@ public function setTimesUsed($timesUsed); * Get expiration date * * @return string|null - * @deprecated Coupon expiration must follow sales rule expiration date. + * @deprecated 101.1.3 Coupon expiration must follow sales rule expiration date. */ public function getExpirationDate(); @@ -119,7 +119,7 @@ public function getExpirationDate(); * * @param string $expirationDate * @return $this - * @deprecated Coupon expiration must follow sales rule expiration date. + * @deprecated 101.1.3 Coupon expiration must follow sales rule expiration date. */ public function setExpirationDate($expirationDate); 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/Model/Coupon.php b/app/code/Magento/SalesRule/Model/Coupon.php index a8c77c6ceeec8..070ce89c1d474 100644 --- a/app/code/Magento/SalesRule/Model/Coupon.php +++ b/app/code/Magento/SalesRule/Model/Coupon.php @@ -207,7 +207,7 @@ public function setTimesUsed($timesUsed) * Get expiration date * * @return string|null - * @deprecated Coupon expiration must follow sales rule expiration date. + * @deprecated 101.1.3 Coupon expiration must follow sales rule expiration date. */ public function getExpirationDate() { @@ -219,7 +219,7 @@ public function getExpirationDate() * * @param string $expirationDate * @return $this - * @deprecated Coupon expiration must follow sales rule expiration date. + * @deprecated 101.1.3 Coupon expiration must follow sales rule expiration date. */ public function setExpirationDate($expirationDate) { diff --git a/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php b/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php new file mode 100644 index 0000000000000..0ee2ee09cad57 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon\Quote; + +use Magento\Quote\Api\Data\CartInterface; +use Magento\SalesRule\Model\Coupon\Usage\Processor as CouponUsageProcessor; +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfo; +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfoFactory; + +/** + * Updates the coupon usages from quote + */ +class UpdateCouponUsages +{ + /** + * @var CouponUsageProcessor + */ + private $couponUsageProcessor; + + /** + * @var UpdateInfoFactory + */ + private $updateInfoFactory; + + /** + * @param CouponUsageProcessor $couponUsageProcessor + * @param UpdateInfoFactory $updateInfoFactory + */ + public function __construct( + CouponUsageProcessor $couponUsageProcessor, + UpdateInfoFactory $updateInfoFactory + ) { + $this->couponUsageProcessor = $couponUsageProcessor; + $this->updateInfoFactory = $updateInfoFactory; + } + + /** + * Executes the current command + * + * @param CartInterface $quote + * @param bool $increment + * @return void + */ + public function execute(CartInterface $quote, bool $increment): void + { + if (!$quote->getAppliedRuleIds()) { + return; + } + + /** @var UpdateInfo $updateInfo */ + $updateInfo = $this->updateInfoFactory->create(); + $updateInfo->setAppliedRuleIds(explode(',', $quote->getAppliedRuleIds())); + $updateInfo->setCouponCode((string)$quote->getCouponCode()); + $updateInfo->setCustomerId((int)$quote->getCustomerId()); + $updateInfo->setIsIncrement($increment); + + $this->couponUsageProcessor->process($updateInfo); + } +} diff --git a/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php b/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php index 3236c80e1b7ed..1645f205d1e55 100644 --- a/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php +++ b/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php @@ -8,56 +8,39 @@ namespace Magento\SalesRule\Model\Coupon; use Magento\Sales\Api\Data\OrderInterface; -use Magento\SalesRule\Model\Coupon; -use Magento\SalesRule\Model\ResourceModel\Coupon\Usage; -use Magento\SalesRule\Model\Rule\CustomerFactory; -use Magento\SalesRule\Model\RuleFactory; +use Magento\SalesRule\Model\Coupon\Usage\Processor as CouponUsageProcessor; +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfo; +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfoFactory; /** - * Updates the coupon usages. + * Updates the coupon usages */ class UpdateCouponUsages { /** - * @var RuleFactory + * @var CouponUsageProcessor */ - private $ruleFactory; + private $couponUsageProcessor; /** - * @var RuleFactory + * @var UpdateInfoFactory */ - private $ruleCustomerFactory; + private $updateInfoFactory; /** - * @var Coupon - */ - private $coupon; - - /** - * @var Usage - */ - private $couponUsage; - - /** - * @param RuleFactory $ruleFactory - * @param CustomerFactory $ruleCustomerFactory - * @param Coupon $coupon - * @param Usage $couponUsage + * @param CouponUsageProcessor $couponUsageProcessor + * @param UpdateInfoFactory $updateInfoFactory */ public function __construct( - RuleFactory $ruleFactory, - CustomerFactory $ruleCustomerFactory, - Coupon $coupon, - Usage $couponUsage + CouponUsageProcessor $couponUsageProcessor, + UpdateInfoFactory $updateInfoFactory ) { - $this->ruleFactory = $ruleFactory; - $this->ruleCustomerFactory = $ruleCustomerFactory; - $this->coupon = $coupon; - $this->couponUsage = $couponUsage; + $this->couponUsageProcessor = $couponUsageProcessor; + $this->updateInfoFactory = $updateInfoFactory; } /** - * Executes the current command. + * Executes the current command * * @param OrderInterface $subject * @param bool $increment @@ -68,86 +51,16 @@ public function execute(OrderInterface $subject, bool $increment): OrderInterfac if (!$subject || !$subject->getAppliedRuleIds()) { return $subject; } - // lookup rule ids - $ruleIds = explode(',', $subject->getAppliedRuleIds()); - $ruleIds = array_unique($ruleIds); - $customerId = (int)$subject->getCustomerId(); - // use each rule (and apply to customer, if applicable) - foreach ($ruleIds as $ruleId) { - if (!$ruleId) { - continue; - } - $this->updateRuleUsages($increment, (int)$ruleId, $customerId); - } - $this->updateCouponUsages($subject, $increment, $customerId); - - return $subject; - } - /** - * Update the number of rule usages. - * - * @param bool $increment - * @param int $ruleId - * @param int $customerId - */ - private function updateRuleUsages(bool $increment, int $ruleId, int $customerId) - { - /** @var \Magento\SalesRule\Model\Rule $rule */ - $rule = $this->ruleFactory->create(); - $rule->load($ruleId); - if ($rule->getId()) { - $rule->loadCouponCode(); - if ($increment || $rule->getTimesUsed() > 0) { - $rule->setTimesUsed($rule->getTimesUsed() + ($increment ? 1 : -1)); - $rule->save(); - } - if ($customerId) { - $this->updateCustomerRuleUsages($increment, $ruleId, $customerId); - } - } - } + /** @var UpdateInfo $updateInfo */ + $updateInfo = $this->updateInfoFactory->create(); + $updateInfo->setAppliedRuleIds(explode(',', $subject->getAppliedRuleIds())); + $updateInfo->setCouponCode((string)$subject->getCouponCode()); + $updateInfo->setCustomerId((int)$subject->getCustomerId()); + $updateInfo->setIsIncrement($increment); - /** - * Update the number of rule usages per customer. - * - * @param bool $increment - * @param int $ruleId - * @param int $customerId - */ - private function updateCustomerRuleUsages(bool $increment, int $ruleId, int $customerId): void - { - /** @var \Magento\SalesRule\Model\Rule\Customer $ruleCustomer */ - $ruleCustomer = $this->ruleCustomerFactory->create(); - $ruleCustomer->loadByCustomerRule($customerId, $ruleId); - if ($ruleCustomer->getId()) { - if ($increment || $ruleCustomer->getTimesUsed() > 0) { - $ruleCustomer->setTimesUsed($ruleCustomer->getTimesUsed() + ($increment ? 1 : -1)); - } - } elseif ($increment) { - $ruleCustomer->setCustomerId($customerId)->setRuleId($ruleId)->setTimesUsed(1); - } - $ruleCustomer->save(); - } + $this->couponUsageProcessor->process($updateInfo); - /** - * Update the number of coupon usages. - * - * @param OrderInterface $subject - * @param bool $increment - * @param int $customerId - */ - private function updateCouponUsages(OrderInterface $subject, bool $increment, int $customerId): void - { - $this->coupon->load($subject->getCouponCode(), 'code'); - if ($this->coupon->getId()) { - if ($increment || $this->coupon->getTimesUsed() > 0) { - $this->coupon->setTimesUsed($this->coupon->getTimesUsed() + ($increment ? 1 : -1)); - $this->coupon->save(); - } - if ($customerId) { - $this->couponUsage->updateCustomerCouponTimesUsed($customerId, $this->coupon->getId(), $increment); - } - } + return $subject; } } diff --git a/app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php b/app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php new file mode 100644 index 0000000000000..90a456d5ff833 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon\Usage; + +use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\ResourceModel\Coupon\Usage; +use Magento\SalesRule\Model\Rule\CustomerFactory; +use Magento\SalesRule\Model\RuleFactory; + +/** + * Processor to update coupon usage + */ +class Processor +{ + /** + * @var RuleFactory + */ + private $ruleFactory; + + /** + * @var RuleFactory + */ + private $ruleCustomerFactory; + + /** + * @var Coupon + */ + private $coupon; + + /** + * @var Usage + */ + private $couponUsage; + + /** + * @param RuleFactory $ruleFactory + * @param CustomerFactory $ruleCustomerFactory + * @param Coupon $coupon + * @param Usage $couponUsage + */ + public function __construct( + RuleFactory $ruleFactory, + CustomerFactory $ruleCustomerFactory, + Coupon $coupon, + Usage $couponUsage + ) { + $this->ruleFactory = $ruleFactory; + $this->ruleCustomerFactory = $ruleCustomerFactory; + $this->coupon = $coupon; + $this->couponUsage = $couponUsage; + } + + /** + * Update coupon usage + * + * @param UpdateInfo $updateInfo + */ + public function process(UpdateInfo $updateInfo): void + { + if (empty($updateInfo->getAppliedRuleIds())) { + return; + } + + if (!empty($updateInfo->getCouponCode())) { + $this->updateCouponUsages($updateInfo); + } + $isIncrement = $updateInfo->isIncrement(); + $customerId = $updateInfo->getCustomerId(); + // use each rule (and apply to customer, if applicable) + foreach (array_unique($updateInfo->getAppliedRuleIds()) as $ruleId) { + if (!(int)$ruleId) { + continue; + } + $this->updateRuleUsages($isIncrement, (int)$ruleId); + if ($customerId) { + $this->updateCustomerRuleUsages($isIncrement, (int)$ruleId, $customerId); + } + } + } + + /** + * Update the number of coupon usages + * + * @param UpdateInfo $updateInfo + */ + private function updateCouponUsages(UpdateInfo $updateInfo): void + { + $isIncrement = $updateInfo->isIncrement(); + $this->coupon->load($updateInfo->getCouponCode(), 'code'); + if ($this->coupon->getId()) { + if ($updateInfo->isIncrement() || $this->coupon->getTimesUsed() > 0) { + $this->coupon->setTimesUsed($this->coupon->getTimesUsed() + ($isIncrement ? 1 : -1)); + $this->coupon->save(); + } + if ($updateInfo->getCustomerId()) { + $this->couponUsage->updateCustomerCouponTimesUsed( + $updateInfo->getCustomerId(), + $this->coupon->getId(), + $isIncrement + ); + } + } + } + + /** + * Update the number of rule usages + * + * @param bool $isIncrement + * @param int $ruleId + */ + private function updateRuleUsages(bool $isIncrement, int $ruleId): void + { + $rule = $this->ruleFactory->create(); + $rule->load($ruleId); + if ($rule->getId()) { + $rule->loadCouponCode(); + if ($isIncrement || $rule->getTimesUsed() > 0) { + $rule->setTimesUsed($rule->getTimesUsed() + ($isIncrement ? 1 : -1)); + $rule->save(); + } + } + } + + /** + * Update the number of rule usages per customer + * + * @param bool $isIncrement + * @param int $ruleId + * @param int $customerId + */ + private function updateCustomerRuleUsages(bool $isIncrement, int $ruleId, int $customerId): void + { + $ruleCustomer = $this->ruleCustomerFactory->create(); + $ruleCustomer->loadByCustomerRule($customerId, $ruleId); + if ($ruleCustomer->getId()) { + if ($isIncrement || $ruleCustomer->getTimesUsed() > 0) { + $ruleCustomer->setTimesUsed($ruleCustomer->getTimesUsed() + ($isIncrement ? 1 : -1)); + } + } elseif ($isIncrement) { + $ruleCustomer->setCustomerId($customerId)->setRuleId($ruleId)->setTimesUsed(1); + } + $ruleCustomer->save(); + } +} diff --git a/app/code/Magento/SalesRule/Model/Coupon/Usage/UpdateInfo.php b/app/code/Magento/SalesRule/Model/Coupon/Usage/UpdateInfo.php new file mode 100644 index 0000000000000..328093ca1af0e --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/Usage/UpdateInfo.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon\Usage; + +use Magento\Framework\DataObject; + +/** + * Coupon usages info to update + */ +class UpdateInfo extends DataObject +{ + private const APPLIED_RULE_IDS_KEY = 'applied_rule_ids'; + private const COUPON_CODE_KEY = 'coupon_code'; + private const CUSTOMER_ID_KEY = 'customer_id'; + private const IS_INCREMENT_KEY = 'is_increment'; + + /** + * Get applied rule ids + * + * @return array + */ + public function getAppliedRuleIds(): array + { + return (array)$this->getData(self::APPLIED_RULE_IDS_KEY); + } + + /** + * Set applied rule ids + * + * @param array $value + * @return void + */ + public function setAppliedRuleIds(array $value): void + { + $this->setData(self::APPLIED_RULE_IDS_KEY, $value); + } + + /** + * Get coupon code + * + * @return string + */ + public function getCouponCode(): string + { + return (string)$this->getData(self::COUPON_CODE_KEY); + } + + /** + * Set coupon code + * + * @param string $value + * @return void + */ + public function setCouponCode(string $value): void + { + $this->setData(self::COUPON_CODE_KEY, $value); + } + + /** + * Get customer id + * + * @return int|null + */ + public function getCustomerId(): ?int + { + return $this->getData(self::CUSTOMER_ID_KEY) !== null + ? (int) $this->getData(self::CUSTOMER_ID_KEY) + : null; + } + + /** + * Set customer id + * + * @param int|null $value + * @return void + */ + public function setCustomerId(?int $value): void + { + $this->setData(self::CUSTOMER_ID_KEY, $value); + } + + /** + * Get update mode: increment - true, decrement - false + * + * @return bool + */ + public function isIncrement(): bool + { + return (bool)$this->getData(self::IS_INCREMENT_KEY); + } + + /** + * Set update mode: increment - true, decrement - false + * + * @param bool $value + * @return void + */ + public function setIsIncrement(bool $value): void + { + $this->setData(self::IS_INCREMENT_KEY, $value); + } +} diff --git a/app/code/Magento/SalesRule/Model/CouponRepository.php b/app/code/Magento/SalesRule/Model/CouponRepository.php index 4c557832fa8d6..f32fbc3d12134 100644 --- a/app/code/Magento/SalesRule/Model/CouponRepository.php +++ b/app/code/Magento/SalesRule/Model/CouponRepository.php @@ -197,7 +197,7 @@ public function deleteById($couponId) * * @param FilterGroup $filterGroup * @param Collection $collection - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return void */ protected function addFilterGroupToCollection( @@ -219,7 +219,7 @@ protected function addFilterGroupToCollection( /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/SalesRule/Model/Quote/Discount.php b/app/code/Magento/SalesRule/Model/Quote/Discount.php index a580a8f9d2eaa..a32fe249920b1 100644 --- a/app/code/Magento/SalesRule/Model/Quote/Discount.php +++ b/app/code/Magento/SalesRule/Model/Quote/Discount.php @@ -6,37 +6,53 @@ namespace Magento\SalesRule\Model\Quote; use Magento\Framework\App\ObjectManager; -use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address\Total; +use Magento\Quote\Model\Quote\Address\Total\AbstractTotal; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\SalesRule\Api\Data\DiscountDataInterface; use Magento\SalesRule\Api\Data\DiscountDataInterfaceFactory; +use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; +use Magento\SalesRule\Model\Data\RuleDiscount; +use Magento\SalesRule\Model\Discount\PostProcessorFactory; +use Magento\SalesRule\Model\Validator; +use Magento\Store\Model\StoreManagerInterface; /** * Discount totals calculation model. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Discount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal +class Discount extends AbstractTotal { const COLLECTOR_TYPE_CODE = 'discount'; /** * Discount calculation object * - * @var \Magento\SalesRule\Model\Validator + * @var Validator */ protected $calculator; /** * Core event manager proxy * - * @var \Magento\Framework\Event\ManagerInterface + * @var ManagerInterface */ protected $eventManager = null; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Magento\Framework\Pricing\PriceCurrencyInterface + * @var PriceCurrencyInterface */ protected $priceCurrency; @@ -51,18 +67,18 @@ class Discount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal private $discountDataInterfaceFactory; /** - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\SalesRule\Model\Validator $validator - * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param ManagerInterface $eventManager + * @param StoreManagerInterface $storeManager + * @param Validator $validator + * @param PriceCurrencyInterface $priceCurrency * @param RuleDiscountInterfaceFactory|null $discountInterfaceFactory * @param DiscountDataInterfaceFactory|null $discountDataInterfaceFactory */ public function __construct( - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\SalesRule\Model\Validator $validator, - \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, + ManagerInterface $eventManager, + StoreManagerInterface $storeManager, + Validator $validator, + PriceCurrencyInterface $priceCurrency, RuleDiscountInterfaceFactory $discountInterfaceFactory = null, DiscountDataInterfaceFactory $discountDataInterfaceFactory = null ) { @@ -80,17 +96,17 @@ public function __construct( /** * Collect address discount amount * - * @param \Magento\Quote\Model\Quote $quote - * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment - * @param \Magento\Quote\Model\Quote\Address\Total $total + * @param Quote $quote + * @param ShippingAssignmentInterface $shippingAssignment + * @param Total $total * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function collect( - \Magento\Quote\Model\Quote $quote, - \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment, - \Magento\Quote\Model\Quote\Address\Total $total + Quote $quote, + ShippingAssignmentInterface $shippingAssignment, + Total $total ) { parent::collect($quote, $shippingAssignment, $total); @@ -122,7 +138,7 @@ public function collect( $address->getExtensionAttributes()->setDiscounts([]); $addressDiscountAggregator = []; - /** @var \Magento\Quote\Model\Quote\Item $item */ + /** @var Item $item */ foreach ($items as $item) { if ($item->getNoDiscount() || !$this->calculator->canApplyDiscount($item)) { $item->setDiscountAmount(0); @@ -147,7 +163,6 @@ public function collect( if ($item->getHasChildren() && $item->isChildrenCalculated()) { $this->calculator->process($item); - $this->distributeDiscount($item); foreach ($item->getChildren() as $child) { $eventArgs['item'] = $child; $this->eventManager->dispatch('sales_quote_address_discount_item', $eventArgs); @@ -175,13 +190,13 @@ public function collect( /** * Aggregate item discount information to total data and related properties * - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\Quote\Model\Quote\Address\Total $total + * @param AbstractItem $item + * @param Total $total * @return $this */ protected function aggregateItemDiscount( - \Magento\Quote\Model\Quote\Item\AbstractItem $item, - \Magento\Quote\Model\Quote\Address\Total $total + AbstractItem $item, + Total $total ) { $total->addTotalAmount($this->getCode(), -$item->getDiscountAmount()); $total->addBaseTotalAmount($this->getCode(), -$item->getBaseDiscountAmount()); @@ -191,10 +206,12 @@ protected function aggregateItemDiscount( /** * Distribute discount at parent item to children items * - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * @param AbstractItem $item * @return $this + * @deprecated No longer used. + * @see \Magento\SalesRule\Model\RulesApplier::applyRule() */ - protected function distributeDiscount(\Magento\Quote\Model\Quote\Item\AbstractItem $item) + protected function distributeDiscount(AbstractItem $item) { $parentBaseRowTotal = $item->getBaseRowTotal(); $keys = [ @@ -230,12 +247,12 @@ protected function distributeDiscount(\Magento\Quote\Model\Quote\Item\AbstractIt /** * Add discount total information to address * - * @param \Magento\Quote\Model\Quote $quote - * @param \Magento\Quote\Model\Quote\Address\Total $total + * @param Quote $quote + * @param Total $total * @return array|null * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Quote\Address\Total $total) + public function fetch(Quote $quote, Total $total) { $result = null; $amount = $total->getDiscountAmount(); @@ -254,25 +271,25 @@ public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Qu /** * Aggregates discount per rule * - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\Quote\Api\Data\AddressInterface $address + * @param AbstractItem $item + * @param AddressInterface $address * @param array $addressDiscountAggregator * @return void */ private function aggregateDiscountPerRule( - \Magento\Quote\Model\Quote\Item\AbstractItem $item, - \Magento\Quote\Api\Data\AddressInterface $address, + AbstractItem $item, + AddressInterface $address, array &$addressDiscountAggregator ) { $discountBreakdown = $item->getExtensionAttributes()->getDiscounts(); if ($discountBreakdown) { foreach ($discountBreakdown as $value) { - /* @var \Magento\SalesRule\Api\Data\DiscountDataInterface $discount */ + /* @var DiscountDataInterface $discount */ $discount = $value->getDiscountData(); $ruleLabel = $value->getRuleLabel(); $ruleID = $value->getRuleID(); if (isset($addressDiscountAggregator[$ruleID])) { - /** @var \Magento\SalesRule\Model\Data\RuleDiscount $cartDiscount */ + /** @var RuleDiscount $cartDiscount */ $cartDiscount = $addressDiscountAggregator[$ruleID]; $discountData = $cartDiscount->getDiscountData(); $discountData->setBaseAmount($discountData->getBaseAmount()+$discount->getBaseAmount()); @@ -294,12 +311,12 @@ private function aggregateDiscountPerRule( 'rule' => $ruleLabel, 'rule_id' => $ruleID, ]; - /** @var \Magento\SalesRule\Model\Data\RuleDiscount $cartDiscount */ + /** @var RuleDiscount $cartDiscount */ $cartDiscount = $this->discountInterfaceFactory->create(['data' => $data]); $addressDiscountAggregator[$ruleID] = $cartDiscount; } } } - $address->getExtensionAttributes()->setDiscounts(array_values($addressDiscountAggregator)); + $address->getExtensionAttributes()->setDiscounts(array_values($addressDiscountAggregator)); } } diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/BuyXGetY.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/BuyXGetY.php index 114d2e6784b72..eaa2433e63ed0 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/BuyXGetY.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/BuyXGetY.php @@ -3,19 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\SalesRule\Model\Rule\Action\Discount; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\SalesRule\Model\Rule; + class BuyXGetY extends AbstractDiscount { /** - * @param \Magento\SalesRule\Model\Rule $rule - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * Calculate discount data for BuyXGetY action. + * + * @param Rule $rule + * @param AbstractItem $item * @param float $qty - * @return \Magento\SalesRule\Model\Rule\Action\Discount\Data + * @return Data */ - public function calculate($rule, $item, $qty) + public function calculate($rule, $item, $qty): Data { - /** @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData */ $discountData = $this->discountFactory->create(); $itemPrice = $this->validator->getItemPrice($item); diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php index b4585bb047c44..1569c9551aa46 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php @@ -190,7 +190,7 @@ public function calculate($rule, $item, $qty) /** * Set information about usage cart fixed rule by quote address * - * @deprecated should be removed as it is not longer used + * @deprecated 101.2.0 should be removed as it is not longer used * @param int $ruleId * @param int $itemId * @return void @@ -203,7 +203,7 @@ protected function setCartFixedRuleUsedForAddress($ruleId, $itemId) /** * Retrieve information about usage cart fixed rule by quote address * - * @deprecated should be removed as it is not longer used + * @deprecated 101.2.0 should be removed as it is not longer used * @param int $ruleId * @return int|null */ diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php index 6ade7a064e849..35e7e62144611 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php @@ -89,6 +89,7 @@ public function collectValidatedAttributes($productCollection) /** * @inheritdoc + * @since 101.0.6 */ protected function _isValid($entity) { diff --git a/app/code/Magento/SalesRule/Model/RuleRepository.php b/app/code/Magento/SalesRule/Model/RuleRepository.php index 2cff0d64dba01..2016ae0dde1c7 100644 --- a/app/code/Magento/SalesRule/Model/RuleRepository.php +++ b/app/code/Magento/SalesRule/Model/RuleRepository.php @@ -184,7 +184,7 @@ public function deleteById($id) * * @param FilterGroup $filterGroup * @param Collection $collection - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return void */ protected function addFilterGroupToCollection( @@ -206,7 +206,7 @@ protected function addFilterGroupToCollection( /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php index 270732c8e0278..ede889c79fb9d 100644 --- a/app/code/Magento/SalesRule/Model/RulesApplier.php +++ b/app/code/Magento/SalesRule/Model/RulesApplier.php @@ -16,9 +16,7 @@ use Magento\SalesRule\Api\Data\DiscountDataInterfaceFactory; /** - * Class RulesApplier - * - * @package Magento\SalesRule\Model\Validator + * Rule applier model */ class RulesApplier { @@ -115,7 +113,6 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) if (!$this->validatorUtility->canProcessRule($rule, $address)) { continue; } - if (!$skipValidation && !$rule->getActions()->validate($item)) { if (!$this->childrenValidationLocator->isChildrenValidationRequired($item)) { continue; @@ -189,8 +186,22 @@ public function addDiscountDescription($address, $rule) */ protected function applyRule($item, $rule, $address, $couponCode) { - $discountData = $this->getDiscountData($item, $rule, $address); - $this->setDiscountData($discountData, $item); + if ($item->getChildren() && $item->isChildrenCalculated()) { + $cloneItem = clone $item; + /** + * validate without children + */ + $applyAll = $rule->getActions()->validate($cloneItem); + foreach ($item->getChildren() as $childItem) { + if ($applyAll || $rule->getActions()->validate($childItem)) { + $discountData = $this->getDiscountData($childItem, $rule, $address); + $this->setDiscountData($discountData, $childItem); + } + } + } else { + $discountData = $this->getDiscountData($item, $rule, $address); + $this->setDiscountData($discountData, $item); + } $this->maintainAddressCouponCode($address, $rule, $couponCode); $this->addDiscountDescription($address, $rule); diff --git a/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php b/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php index b698190997d7e..7f355a62c4631 100644 --- a/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php +++ b/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php @@ -19,7 +19,7 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement { /** * @var \Magento\SalesRule\Model\CouponFactory - * @deprecated + * @deprecated 101.1.2 */ protected $couponFactory; @@ -30,7 +30,7 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement /** * @var \Magento\SalesRule\Model\ResourceModel\Coupon\CollectionFactory - * @deprecated + * @deprecated 101.1.2 */ protected $collectionFactory; @@ -41,7 +41,7 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement /** * @var \Magento\SalesRule\Model\Spi\CouponResourceInterface - * @deprecated + * @deprecated 101.1.2 */ protected $resourceModel; diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index 0fc0b062c7887..cc0333480f7b0 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -7,6 +7,7 @@ namespace Magento\SalesRule\Model; use Magento\Framework\App\ObjectManager; +use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\SalesRule\Helper\CartFixedDiscount; @@ -280,6 +281,13 @@ public function process(AbstractItem $item) $item->setDiscountAmount(0); $item->setBaseDiscountAmount(0); $item->setDiscountPercent(0); + if ($item->getChildren() && $item->isChildrenCalculated()) { + foreach ($item->getChildren() as $child) { + $child->setDiscountAmount(0); + $child->setBaseDiscountAmount(0); + $child->setDiscountPercent(0); + } + } $itemPrice = $this->getItemPrice($item); if ($itemPrice < 0) { @@ -319,7 +327,7 @@ public function processShippingAmount(Address $address) $quote = $address->getQuote(); $appliedRuleIds = []; foreach ($this->_getRules($address) as $rule) { - /* @var \Magento\SalesRule\Model\Rule $rule */ + /* @var Rule $rule */ if (!$rule->getApplyToShipping() || !$this->validatorUtility->canProcessRule($rule, $address)) { continue; } @@ -328,28 +336,28 @@ public function processShippingAmount(Address $address) $baseDiscountAmount = 0; $rulePercent = min(100, $rule->getDiscountAmount()); switch ($rule->getSimpleAction()) { - case \Magento\SalesRule\Model\Rule::TO_PERCENT_ACTION: + case Rule::TO_PERCENT_ACTION: $rulePercent = max(0, 100 - $rule->getDiscountAmount()); // break is intentionally omitted // no break - case \Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION: + case Rule::BY_PERCENT_ACTION: $discountAmount = ($shippingAmount - $address->getShippingDiscountAmount()) * $rulePercent / 100; $baseDiscountAmount = ($baseShippingAmount - $address->getBaseShippingDiscountAmount()) * $rulePercent / 100; $discountPercent = min(100, $address->getShippingDiscountPercent() + $rulePercent); $address->setShippingDiscountPercent($discountPercent); break; - case \Magento\SalesRule\Model\Rule::TO_FIXED_ACTION: + case Rule::TO_FIXED_ACTION: $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore()); $discountAmount = $shippingAmount - $quoteAmount; $baseDiscountAmount = $baseShippingAmount - $rule->getDiscountAmount(); break; - case \Magento\SalesRule\Model\Rule::BY_FIXED_ACTION: + case Rule::BY_FIXED_ACTION: $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore()); $discountAmount = $quoteAmount; $baseDiscountAmount = $rule->getDiscountAmount(); break; - case \Magento\SalesRule\Model\Rule::CART_FIXED_ACTION: + case Rule::CART_FIXED_ACTION: $cartRules = $address->getCartFixedRules(); $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore()); $isAppliedToShipping = (int) $rule->getApplyToShipping(); @@ -385,6 +393,12 @@ public function processShippingAmount(Address $address) } $address->setCartFixedRules($cartRules); break; + case Rule::BUY_X_GET_Y_ACTION: + $allQtyDiscount = $this->getDiscountQtyAllItemsBuyXGetYAction($quote, $rule); + $quoteAmount = $address->getBaseShippingAmount() / $quote->getItemsQty() * $allQtyDiscount; + $discountAmount = $this->priceCurrency->convert($quoteAmount, $quote->getStore()); + $baseDiscountAmount = $quoteAmount; + break; } $discountAmount = min($address->getShippingDiscountAmount() + $discountAmount, $shippingAmount); @@ -426,9 +440,9 @@ public function initTotals($items, Address $address) return $this; } - /** @var \Magento\SalesRule\Model\Rule $rule */ + /** @var Rule $rule */ foreach ($this->_getRules($address) as $rule) { - if (\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION == $rule->getSimpleAction() + if (Rule::CART_FIXED_ACTION == $rule->getSimpleAction() && $this->validatorUtility->canProcessRule($rule, $address) ) { $ruleTotalItemsPrice = 0; @@ -481,6 +495,40 @@ private function isValidItemForRule(AbstractItem $item, Rule $rule) return true; } + /** + * Return discount Qty for all items at Buy_X_Get_Y_Action + * + * @param Quote $quote + * @param Rule $rule + * @return float + */ + private function getDiscountQtyAllItemsBuyXGetYAction(Quote $quote, Rule $rule): float + { + $discountAllQty = 0; + foreach ($quote->getItems() as $item) { + $qty = $item->getQty(); + + $discountStep = $rule->getDiscountStep(); + $discountAmount = $rule->getDiscountAmount(); + if (!$discountStep || $discountAmount > $discountStep) { + continue; + } + $buyAndDiscountQty = $discountStep + $discountAmount; + + $fullRuleQtyPeriod = floor($qty / $buyAndDiscountQty); + $freeQty = $qty - $fullRuleQtyPeriod * $buyAndDiscountQty; + + $discountQty = $fullRuleQtyPeriod * $discountAmount; + if ($freeQty > $discountStep) { + $discountQty += $freeQty - $discountStep; + } + + $discountAllQty += $discountQty; + } + + return $discountAllQty; + } + /** * Return item price * @@ -564,7 +612,7 @@ public function prepareDescription($address, $separator = ', ') public function sortItemsByPriority($items, Address $address = null) { $itemsSorted = []; - /** @var $rule \Magento\SalesRule\Model\Rule */ + /** @var $rule Rule */ foreach ($this->_getRules($address) as $rule) { foreach ($items as $itemKey => $itemValue) { if ($rule->getActions()->validate($itemValue)) { diff --git a/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php b/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php index 2d771e4560fcf..1d416fbcf4f52 100644 --- a/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php +++ b/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php @@ -45,9 +45,10 @@ public function execute(Observer $observer) $event = $observer->getEvent(); /** @var OrderInterface $order */ $order = $event->getData(self::EVENT_KEY_ORDER); - - if ($order->getCustomerId()) { - $this->updateCouponUsages->execute($order, true); + if (!$order->getCustomerId()) { + return; } + + $this->updateCouponUsages->execute($order, true); } } diff --git a/app/code/Magento/SalesRule/Observer/CouponUsagesDecrement.php b/app/code/Magento/SalesRule/Observer/CouponUsagesDecrement.php new file mode 100644 index 0000000000000..d0c7199405879 --- /dev/null +++ b/app/code/Magento/SalesRule/Observer/CouponUsagesDecrement.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Observer; + +use Magento\Framework\Event\Observer as EventObserver; +use Magento\Framework\Event\ObserverInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\SalesRule\Model\Coupon\Quote\UpdateCouponUsages; + +/** + * Decrement number of coupon usages after error of placing order + */ +class CouponUsagesDecrement implements ObserverInterface +{ + /** + * @var UpdateCouponUsages + */ + private $updateCouponUsages; + + /** + * @param UpdateCouponUsages $updateCouponUsages + */ + public function __construct(UpdateCouponUsages $updateCouponUsages) + { + $this->updateCouponUsages = $updateCouponUsages; + } + + /** + * @inheritdoc + */ + public function execute(EventObserver $observer) + { + /** @var CartInterface $quote */ + $quote = $observer->getQuote(); + $this->updateCouponUsages->execute($quote, false); + } +} diff --git a/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php b/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php index 87a7c2ed1bd38..3be801a288479 100644 --- a/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php +++ b/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php @@ -49,11 +49,13 @@ public function __construct( */ public function afterCancel(OrderService $subject, bool $result, $orderId): bool { - $order = $this->orderRepository->get($orderId); - if ($result) { - $this->updateCouponUsages->execute($order, false); + if (!$result) { + return $result; } + $order = $this->orderRepository->get($orderId); + $this->updateCouponUsages->execute($order, false); + return $result; } } diff --git a/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php b/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php index 14bbb5fce02a5..66a32f37eee2f 100644 --- a/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php +++ b/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php @@ -7,12 +7,13 @@ namespace Magento\SalesRule\Plugin; -use Magento\Sales\Api\Data\OrderInterface; -use Magento\Sales\Model\Service\OrderService; -use Magento\SalesRule\Model\Coupon\UpdateCouponUsages; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteManagement; +use Magento\SalesRule\Model\Coupon\Quote\UpdateCouponUsages; /** - * Increments number of coupon usages after placing order. + * Increments number of coupon usages before placing order */ class CouponUsagesIncrement { @@ -24,24 +25,28 @@ class CouponUsagesIncrement /** * @param UpdateCouponUsages $updateCouponUsages */ - public function __construct( - UpdateCouponUsages $updateCouponUsages - ) { + public function __construct(UpdateCouponUsages $updateCouponUsages) + { $this->updateCouponUsages = $updateCouponUsages; } /** - * Increments number of coupon usages after placing order. + * Increments number of coupon usages before placing order * - * @param OrderService $subject - * @param OrderInterface $result - * @return OrderInterface + * @param QuoteManagement $subject + * @param Quote $quote + * @param array $orderData + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws NoSuchEntityException */ - public function afterPlace(OrderService $subject, OrderInterface $result): OrderInterface + public function beforeSubmit(QuoteManagement $subject, Quote $quote, $orderData = []) { - $this->updateCouponUsages->execute($result, true); + /* if coupon code has been canceled then need to notify the customer */ + if (!$quote->getCouponCode() && $quote->dataHasChangedFor('coupon_code')) { + throw new NoSuchEntityException(__("The coupon code isn't valid. Verify the code and try again.")); + } - return $result; + $this->updateCouponUsages->execute($quote, true); } } diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml new file mode 100644 index 0000000000000..85437650efc35 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.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="AdminCartPriceRuleDeleteAllActionGroup"> + <annotations> + <description>Open Cart Price Rule grid and delete all rules one by one. Need to avoid interference with other tests that test cart price rules.</description> + </annotations> + + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="goToAdminCartPriceRuleGridPage"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <helper class="\Magento\Rule\Test\Mftf\Helper\RuleHelper" method="deleteAllRulesOneByOne" stepKey="deleteAllRulesOneByOne"> + <argument name="firstNotEmptyRow">{{AdminDataGridTableSection.firstNotEmptyRow}}</argument> + <argument name="modalAcceptButton">{{AdminConfirmationModalSection.ok}}</argument> + <argument name="deleteButton">{{AdminMainActionsSection.delete}}</argument> + <argument name="successMessageContainer">{{AdminMessagesSection.success}}</argument> + <argument name="successMessage">You deleted the rule.</argument> + </helper> + <waitForElementVisible selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="waitDataGridEmptyMessageAppears"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillActionsActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillActionsActionGroup.xml new file mode 100644 index 0000000000000..391a11cd7f1dc --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillActionsActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCartPriceRuleFillActionsActionGroup"> + <annotations> + <description>Fill Cart Price Rule actions fields: Apply, Discount Amount, Discard subsequent rules.</description> + </annotations> + <arguments> + <argument name="apply" type="string" defaultValue="{{ApiSalesRule.simple_action}}"/> + <argument name="discountAmount" type="string" defaultValue="{{ApiSalesRule.discount_amount}}"/> + <argument name="discardSubsequentRules" type="string" defaultValue="1"/> + </arguments> + + <conditionalClick selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" dependentSelector="{{AdminCartPriceRulesFormSection.actionsHeaderOpen}}" visible="false" stepKey="clickToExpandActions"/> + <scrollTo selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="scrollToActionsFieldset"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.apply}}" stepKey="waitActionsFieldsetFullyOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="{{apply}}" stepKey="fillDiscountType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="{{discountAmount}}" stepKey="fillDiscountAmount"/> + <pressKey selector="{{AdminCartPriceRulesFormSection.discountAmount}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::TAB]" stepKey="pressTab"/> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.discardSubsequentRulesLabel}}" dependentSelector="{{AdminCartPriceRulesFormSection.discardSubsequentRulesByStatus(discardSubsequentRules)}}" visible="false" stepKey="fillDiscardSubsequentRules"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillMainInfoActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillMainInfoActionGroup.xml new file mode 100644 index 0000000000000..4624278d7f3f4 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillMainInfoActionGroup.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCartPriceRuleFillMainInfoActionGroup"> + <annotations> + <description>Fill Cart Price Rule main info fields: Name, Description, Active (1/0), Priority.</description> + </annotations> + <arguments> + <argument name="name" type="string" defaultValue="{{ApiSalesRule.name}}"/> + <argument name="description" type="string" defaultValue="{{ApiSalesRule.description}}"/> + <argument name="active" type="string" defaultValue="1"/> + <argument name="websites" type="string" defaultValue="'Main Website'"/> + <argument name="groups" type="string" defaultValue="'NOT LOGGED IN','General','Wholesale','Retailer'"/> + <argument name="fromDate" type="string" defaultValue=""/> + <argument name="toDate" type="string" defaultValue=""/> + <argument name="priority" type="string" defaultValue=""/> + </arguments> + + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{name}}" stepKey="fillName"/> + <fillField selector="{{AdminCartPriceRulesFormSection.description}}" userInput="{{description}}" stepKey="fillDescription"/> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.isActive}}" dependentSelector="{{AdminCartPriceRulesFormSection.activeByStatus(active)}}" visible="false" stepKey="fillActive"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" parameterArray="[{{websites}}]" stepKey="selectSpecifiedWebsites"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" parameterArray="[{{groups}}]" stepKey="selectSpecifiedCustomerGroups"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{{fromDate}}" stepKey="fillFromDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.toDate}}" userInput="{{toDate}}" stepKey="fillToDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.priority}}" userInput="{{priority}}" stepKey="fillPriority"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleSaveActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleSaveActionGroup.xml new file mode 100644 index 0000000000000..a94d5b4cf4889 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleSaveActionGroup.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="AdminCartPriceRuleSaveActionGroup"> + <annotations> + <description>Clicks Save and Apply on a Admin Cart Price Rule creation/edit page. Validates that the Success Message is present.</description> + </annotations> + + <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForElementVisible selector="{{AdminMainActionsSection.save}}" stepKey="waitForSaveButton"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveRule"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="checkSuccessSaveMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index b164cdde33248..df126f05819d0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -10,7 +10,7 @@ <section name="AdminCartPriceRulesFormSection"> <element name="save" type="button" selector="#save" timeout="30"/> <element name="saveAndContinue" type="button" selector="#save_and_continue" timeout="30"/> - <element name="delete" type="button" selector="#delete" timeout="30"/> + <element name="delete" type="button" selector="button#delete" timeout="30"/> <element name="modalAcceptButton" type="button" selector="button.action-accept" timeout="30"/> <!-- Rule Information (the main form on the page) --> @@ -18,6 +18,8 @@ <element name="ruleName" type="input" selector="input[name='name']"/> <element name="description" type="textarea" selector="//div[@class='admin__field-control']/textarea[@name='description']"/> <element name="active" type="checkbox" selector="//div[@class='admin__actions-switch']/input[@name='is_active']/../label"/> + <element name="isActive" type="text" selector="input[name='is_active']+label"/> + <element name="activeByStatus" type="text" selector="div.admin__actions-switch input[name='is_active'][value='{{value}}']+label" parameterized="true"/> <element name="websites" type="multiselect" selector="select[name='website_ids']"/> <element name="websitesOptions" type="select" selector="[name='website_ids'] option"/> <element name="customerGroups" type="multiselect" selector="select[name='customer_group_ids']"/> @@ -84,6 +86,8 @@ <element name="discountStep" type="input" selector="input[name='discount_step']"/> <element name="applyToShippingAmount" type="checkbox" selector="//div[@class='admin__actions-switch']/input[@name='apply_to_shipping']/../label"/> <element name="discardSubsequentRules" type="checkbox" selector="//div[@class='admin__actions-switch']/input[@name='stop_rules_processing']/../label"/> + <element name="discardSubsequentRulesLabel" type="text" selector="div.admin__actions-switch input[name='stop_rules_processing']+label"/> + <element name="discardSubsequentRulesByStatus" type="text" selector="div.admin__actions-switch input[name='stop_rules_processing'][value='{{value}}']+label" parameterized="true"/> <element name="addRewardPoints" type="input" selector="input[name='extension_attributes[reward_points_delta]']"/> <element name="freeShipping" type="select" selector="//select[@name='simple_free_shipping']"/> 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/AdminCartRulesAppliedForProductInCartTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml index 1433d660d3535..61c80f32b6546 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml @@ -50,8 +50,7 @@ </after> <!--Start creating a bundle product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml new file mode 100644 index 0000000000000..c65aa9980666f --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.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="AdminCreateBuyXGetYFreeWithApplyShippingAmountTest" extends="AdminCreateBuyXGetYFreeTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Admin should be able to create a cart price rule of type Buy X get Y free enable 'Apply to Shipping Amount' "/> + <description value="Use cart price rule of type Buy X get Y free with enable 'Apply to Shipping Amount'"/> + <severity value="MAJOR"/> + <group value="SalesRule"/> + </annotations> + + <remove keyForRemoval="verifyStorefront"/> + <click selector="{{AdminCartPriceRulesFormSection.applyDiscountToShippingLabel}}" stepKey="enabledApplyDiscountToShipping" after="fillDiscountStep"/> + <actionGroup ref="VerifyDiscountAmountActionGroup" stepKey="verifyStorefrontDiscount" after="fillProductFieldsInAdmin"> + <argument name="productUrl" value="{{_defaultProduct.urlKey}}.html"/> + <argument name="quantity" value="2"/> + <argument name="expectedDiscount" value="-$128.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml index 221f80b887fe5..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"/> @@ -83,9 +82,7 @@ <!-- Spot check the storefront --> <amOnPage url="$$product.custom_attributes[url_key]$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyCoupon"> <argument name="coupon" value="_defaultCoupon"/> @@ -95,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/AdminCreateCartPriceRuleForCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml index e2a65685bd97e..557a585858868 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml @@ -75,9 +75,7 @@ <!-- Spot check the storefront --> <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyCoupon"> <argument name="coupon" value="_defaultCoupon"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index 9f4168575595a..e18a9eaadcd23 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -79,9 +79,7 @@ <!-- Spot check the storefront --> <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> <conditionalClick selector="{{StorefrontSalesRuleCartCouponSection.couponHeader}}" dependentSelector="{{StorefrontSalesRuleCartCouponSection.discountBlockActive}}" visible="false" stepKey="clickCouponHeader"/> <waitForElementVisible selector="{{StorefrontSalesRuleCartCouponSection.couponField}}" stepKey="waitForCouponField" /> 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/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml index bc608c0e06086..ad1ff69a60901 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml @@ -126,15 +126,11 @@ <!-- Add the first product to the cart --> <amOnPage url="$$createConfigChildProduct1.sku$$.html" stepKey="goToProductPage1"/> <waitForPageLoad stepKey="waitForProductPageLoad1"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart1"/> - <waitForPageLoad stepKey="waitForAddToCart1"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Add the second product to the cart --> <amOnPage url="$$createConfigChildProduct2.sku$$.html" stepKey="goToProductPage2"/> <waitForPageLoad stepKey="waitForProductPageLoad2"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart2"/> - <waitForPageLoad stepKey="waitForAddToCart2"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage2"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"/> <!--View and edit cart--> <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="clickViewAndEditCartFromMiniCart"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index b77cfaf02d232..c2aeca657db3b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -97,8 +97,7 @@ <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" stepKey="selectFlatShippingMethod"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml index 51e25d3a7e255..eef5dadfbe5d8 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml @@ -66,9 +66,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Should not see the discount yet because we have not set country --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml index 420bc37d5c1b2..69097e3269fcb 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml @@ -70,9 +70,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Should not see the discount yet because we have not filled in postcode --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml index 279747f87d66d..18057965c28e1 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml @@ -68,9 +68,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Should not see the discount yet because we have only 1 item in our cart --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> @@ -81,9 +79,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage2"/> <waitForPageLoad stepKey="waitForProductPageLoad2"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity2"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart2"/> - <waitForPageLoad stepKey="waitForAddToCart2"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage2"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"/> <!-- Now we should see the discount because we have more than 1 item --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage2"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml index a3f32c0781a52..c13b74b6990d0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml @@ -66,9 +66,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Should not see the discount yet because we have not filled in postcode --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml index 39ac14315110e..97b75ae772f08 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml @@ -66,9 +66,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Should not see the discount yet because we have not exceeded $200 --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> @@ -79,9 +77,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage2"/> <waitForPageLoad stepKey="waitForProductPageLoad2"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity2"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart2"/> - <waitForPageLoad stepKey="waitForAddToCart2"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage2"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"/> <!-- Now we should see the discount because we exceeded $200 --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage2"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml index 9b5f8fbb2912d..1178ca2cfb328 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml @@ -34,8 +34,7 @@ <createData entity="defaultTaxRule" stepKey="initialTaxRule"/> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> <!-- Add tax rule with 20% tax rate --> @@ -57,20 +56,22 @@ <createData entity="SimpleProduct2" stepKey="createSimpleProductThird"> <field key="price">5.50</field> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Removed created Data --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Delete the tax rate that were created --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> <argument name="name" value="{{SimpleTaxNYRate.state}}-{{SimpleTaxNYRate.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml index 85a30b3a3a2b4..6b634fa37da2c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml @@ -81,7 +81,7 @@ <argument name="actionValue" value="$$createCategory.id$$"/> </actionGroup> <!-- 2: Go to frontend and add an item from both CAT1 and CAT2 to your cart --> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontend"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontend"/> <!-- 3: Open configurable product 1 and add all his child products to cart --> <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct1.custom_attributes[url_key]$$)}}" stepKey="amOnConfigurableProductPage"/> <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect('$$createConfigProductAttributeCreateConfigurableProduct1.attribute[frontend_labels][0][label]$$')}}" userInput="$$createConfigProductAttributeOption1CreateConfigurableProduct1.option[store_labels][0][label]$$" stepKey="selectOption"/> diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php index b8f879611e51c..bcebeae94be86 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php @@ -228,157 +228,6 @@ public function testCollectItemHasParent() ); } - /** - * @dataProvider collectItemHasChildrenDataProvider - */ - public function testCollectItemHasChildren($childItemData, $parentData, $expectedChildData) - { - $childItems = []; - foreach ($childItemData as $itemId => $itemData) { - $item = $this->objectManager->getObject(Item::class)->setData($itemData); - $childItems[$itemId] = $item; - } - - $itemWithChildren = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->setMethods( - [ - 'getNoDiscount', - 'getParentItem', - 'getHasChildren', - 'isChildrenCalculated', - 'getChildren', - 'getExtensionAttributes', - ] - ) - ->getMock(); - $itemExtension = $this->getMockBuilder( - ExtensionAttributesInterface::class - )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); - $itemExtension->method('getDiscounts')->willReturn([]); - $itemExtension->expects($this->any()) - ->method('setDiscounts') - ->willReturn([]); - $itemWithChildren->expects( - $this->any() - )->method('getExtensionAttributes')->willReturn($itemExtension); - $itemWithChildren->expects($this->once())->method('getNoDiscount')->willReturn(false); - $itemWithChildren->expects($this->once())->method('getParentItem')->willReturn(false); - $itemWithChildren->expects($this->once())->method('getHasChildren')->willReturn(true); - $itemWithChildren->expects($this->once())->method('isChildrenCalculated')->willReturn(true); - $itemWithChildren->expects($this->any())->method('getChildren')->willReturn($childItems); - foreach ($parentData as $key => $value) { - $itemWithChildren->setData($key, $value); - } - - $this->validatorMock->expects($this->any())->method('canApplyDiscount')->willReturn(true); - $this->validatorMock->expects($this->once())->method('sortItemsByPriority') - ->with([$itemWithChildren], $this->addressMock) - ->willReturnArgument(0); - $this->validatorMock->expects($this->any())->method('canApplyRules')->willReturn(true); - - $storeMock = $this->getMockBuilder(Store::class) - ->disableOriginalConstructor() - ->setMethods(['getStore']) - ->getMock(); - $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); - - $quoteMock = $this->getMockBuilder(Quote::class) - ->disableOriginalConstructor() - ->getMock(); - $this->addressMock->expects($this->any())->method('getQuote')->willReturn($quoteMock); - $this->addressMock->expects($this->any())->method('getShippingAmount')->willReturn(true); - - $this->shippingAssignmentMock->expects($this->any())->method('getItems')->willReturn([$itemWithChildren]); - $totalMock = $this->createMock(Total::class); - - $this->assertInstanceOf( - Discount::class, - $this->discount->collect($quoteMock, $this->shippingAssignmentMock, $totalMock) - ); - - foreach ($expectedChildData as $itemId => $expectedItemData) { - $childItem = $childItems[$itemId]; - foreach ($expectedItemData as $key => $value) { - $this->assertEquals($value, $childItem->getData($key), 'Incorrect value for ' . $key); - } - } - } - - /** - * @return array - */ - public function collectItemHasChildrenDataProvider() - { - $data = [ - // 3 items, each $100, testing that discount are distributed to item correctly - [ - 'child_item_data' => [ - 'item1' => [ - 'base_row_total' => 0, - ] - ], - 'parent_item_data' => [ - 'discount_amount' => 20, - 'base_discount_amount' => 10, - 'original_discount_amount' => 40, - 'base_original_discount_amount' => 20, - 'base_row_total' => 0, - ], - 'expected_child_item_data' => [ - 'item1' => [ - 'discount_amount' => 0, - 'base_discount_amount' => 0, - 'original_discount_amount' => 0, - 'base_original_discount_amount' => 0, - ] - ], - ], - [ - // 3 items, each $100, testing that discount are distributed to item correctly - 'child_item_data' => [ - 'item1' => [ - 'base_row_total' => 100, - ], - 'item2' => [ - 'base_row_total' => 100, - ], - 'item3' => [ - 'base_row_total' => 100, - ], - ], - 'parent_item_data' => [ - 'discount_amount' => 20, - 'base_discount_amount' => 10, - 'original_discount_amount' => 40, - 'base_original_discount_amount' => 20, - 'base_row_total' => 300, - ], - 'expected_child_item_data' => [ - 'item1' => [ - 'discount_amount' => 6.67, - 'base_discount_amount' => 3.33, - 'original_discount_amount' => 13.33, - 'base_original_discount_amount' => 6.67, - ], - 'item2' => [ - 'discount_amount' => 6.66, - 'base_discount_amount' => 3.34, - 'original_discount_amount' => 13.34, - 'base_original_discount_amount' => 6.66, - ], - 'item3' => [ - 'discount_amount' => 6.67, - 'base_discount_amount' => 3.33, - 'original_discount_amount' => 13.33, - 'base_original_discount_amount' => 6.67, - ], - ], - ], - ]; - return $data; - } - public function testCollectItemHasNoChildren() { $itemWithChildren = $this->getMockBuilder(Item::class) diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php index b0d3a203977ef..22627717a47f2 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php @@ -9,6 +9,7 @@ use Magento\Backend\Helper\Data; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ProductCategoryList; use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Model\ResourceModel\Product; use Magento\Directory\Model\CurrencyFactory; @@ -34,6 +35,7 @@ */ class ProductTest extends TestCase { + const STUB_CATEGORY_ID = 5; /** @var SalesRuleProduct */ protected $model; @@ -70,6 +72,9 @@ class ProductTest extends TestCase /** @var Select|MockObject */ protected $selectMock; + /** @var MockObject|ProductCategoryList */ + private $productCategoryListMock; + /** * Setup the test */ @@ -138,6 +143,10 @@ protected function setUp(): void $this->collectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); + $this->productCategoryListMock = $this->getMockBuilder(ProductCategoryList::class) + ->disableOriginalConstructor() + ->onlyMethods(['getCategoryIds']) + ->getMock(); $this->format = new Format( $this->getMockBuilder(ScopeResolverInterface::class) ->disableOriginalConstructor() @@ -158,7 +167,9 @@ protected function setUp(): void $this->productRepositoryMock, $this->productMock, $this->collectionMock, - $this->format + $this->format, + [], + $this->productCategoryListMock ); } @@ -228,28 +239,22 @@ public function testValidateCategoriesIgnoresVisibility(): void ->setMethods(['getAttribute', 'getId', 'setQuoteItemQty', 'setQuoteItemPrice']) ->getMock(); $product - ->expects($this->any()) ->method('setQuoteItemQty') ->willReturnSelf(); $product - ->expects($this->any()) ->method('setQuoteItemPrice') ->willReturnSelf(); /* @var AbstractItem|MockObject $item */ $item = $this->getMockBuilder(AbstractItem::class) ->disableOriginalConstructor() - ->setMethods(['getProduct']) + ->onlyMethods(['getProduct']) ->getMockForAbstractClass(); $item->expects($this->any()) ->method('getProduct') ->willReturn($product); $this->model->setAttribute('category_ids'); - - $this->selectMock - ->expects($this->once()) - ->method('where') - ->with($this->logicalNot($this->stringContains('visibility')), $this->anything(), $this->anything()); - + $this->productCategoryListMock->method('getCategoryIds') + ->willReturn([self::STUB_CATEGORY_ID]); $this->model->validate($item); } diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php index 199bcde93bc64..df3b1227e5386 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php @@ -7,6 +7,7 @@ namespace Magento\SalesRule\Test\Unit\Model; +use Magento\Catalog\Model\Product; use Magento\Framework\Api\ExtensionAttributesInterface; use Magento\Framework\Event\Manager; use Magento\Quote\Model\Quote; @@ -169,6 +170,10 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren, ->method('validate') ->with($item) ->willReturn(!$isContinue); + $product = $this->createPartialMock(Product::class, []); + $item->expects($this->atLeastOnce()) + ->method('getProduct') + ->willReturn($product); } if (!$isContinue || !$isChildren) { @@ -247,7 +252,7 @@ protected function getPreparedItem() */ $item = $this->getMockBuilder(Item::class) ->addMethods(['setDiscountAmount', 'setBaseDiscountAmount', 'setDiscountPercent', 'setAppliedRuleIds']) - ->onlyMethods(['getAddress', 'getChildren', 'getExtensionAttributes']) + ->onlyMethods(['getAddress', 'getChildren', 'getExtensionAttributes', 'getProduct']) ->disableOriginalConstructor() ->getMock(); $itemExtension = $this->getMockBuilder( diff --git a/app/code/Magento/SalesRule/etc/di.xml b/app/code/Magento/SalesRule/etc/di.xml index c4bc9c3a6decb..05bd801c3b99f 100644 --- a/app/code/Magento/SalesRule/etc/di.xml +++ b/app/code/Magento/SalesRule/etc/di.xml @@ -191,6 +191,8 @@ </type> <type name="Magento\Sales\Model\Service\OrderService"> <plugin name="coupon_uses_decrement_plugin" type="Magento\SalesRule\Plugin\CouponUsagesDecrement" /> + </type> + <type name="\Magento\Quote\Model\QuoteManagement"> <plugin name="coupon_uses_increment_plugin" type="Magento\SalesRule\Plugin\CouponUsagesIncrement" sortOrder="20"/> </type> <preference diff --git a/app/code/Magento/SalesRule/etc/events.xml b/app/code/Magento/SalesRule/etc/events.xml index c55c37de71aac..0c8335b0a6716 100644 --- a/app/code/Magento/SalesRule/etc/events.xml +++ b/app/code/Magento/SalesRule/etc/events.xml @@ -39,4 +39,7 @@ <event name="sales_quote_collect_totals_before"> <observer name="salesrule_sales_quote_collect_totals_before" instance="\Magento\SalesRule\Observer\QuoteResetAppliedRulesObserver" /> </event> + <event name="sales_model_service_quote_submit_failure"> + <observer name="sales_rule_decrement_coupon_usage_quote_submit_failure" instance="\Magento\SalesRule\Observer\CouponUsagesDecrement" /> + </event> </config> diff --git a/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml b/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml index 64918c24cdc61..5a81d2fd94e14 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml +++ b/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script require([ 'jquery', "uiRegistry", @@ -41,15 +43,15 @@ function generateCouponCodes(idPrefix, generateUrl, grid) { var elements = $(idPrefix + 'information_fieldset').select('input', 'select', 'textarea'); elements = elements.concat( - $$('#rule_uses_per_coupon'), - $$('#rule_uses_per_customer'), - $$('#rule_to_date') + \$$('#rule_uses_per_coupon'), + \$$('#rule_uses_per_customer'), + \$$('#rule_to_date') ); var params = Form.serializeElements(elements, true); params.form_key = FORM_KEY; - if ($$('#'+idPrefix + 'information_fieldset .messages')) { - $$('#'+idPrefix + 'information_fieldset .messages')[0].update(); + if (\$$('#'+idPrefix + 'information_fieldset .messages')) { + \$$('#'+idPrefix + 'information_fieldset .messages')[0].update(); } if ($('messages')) { $('messages').update(); @@ -71,8 +73,8 @@ function generateCouponCodes(idPrefix, generateUrl, grid) { couponCodesGrid.reload(); } if (response && response.messages) { - if ($$('#'+idPrefix + 'information_fieldset .messages')) { - $$('#'+idPrefix + 'information_fieldset .messages')[0].update(response.messages); + if (\$$('#'+idPrefix + 'information_fieldset .messages')) { + \$$('#'+idPrefix + 'information_fieldset .messages')[0].update(response.messages); } else if ($('messages')) { $('messages').update(response.messages); } @@ -94,4 +96,6 @@ window.validateCouponGenerate = validateCouponGenerate; window.generateCouponCodes = generateCouponCodes; window.refreshCouponCodesGrid = refreshCouponCodesGrid; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/SalesRule/view/frontend/requirejs-config.js b/app/code/Magento/SalesRule/view/frontend/requirejs-config.js index 13b701c6fe65a..484020a573f07 100644 --- a/app/code/Magento/SalesRule/view/frontend/requirejs-config.js +++ b/app/code/Magento/SalesRule/view/frontend/requirejs-config.js @@ -8,6 +8,12 @@ var config = { mixins: { 'Magento_Checkout/js/action/select-payment-method': { 'Magento_SalesRule/js/action/select-payment-method-mixin': true + }, + 'Magento_Checkout/js/model/shipping-save-processor': { + 'Magento_SalesRule/js/model/shipping-save-processor-mixin': true + }, + 'Magento_Checkout/js/action/place-order': { + 'Magento_SalesRule/js/model/place-order-mixin': true } } } diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/model/place-order-mixin.js b/app/code/Magento/SalesRule/view/frontend/web/js/model/place-order-mixin.js new file mode 100644 index 0000000000000..da4de3fa19c5e --- /dev/null +++ b/app/code/Magento/SalesRule/view/frontend/web/js/model/place-order-mixin.js @@ -0,0 +1,42 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/utils/wrapper', + 'Magento_Checkout/js/model/quote', + 'Magento_SalesRule/js/model/coupon', + 'Magento_Checkout/js/action/get-totals' +], function ($, wrapper, quote, coupon, getTotalsAction) { + 'use strict'; + + return function (placeOrderAction) { + return wrapper.wrap(placeOrderAction, function (originalAction, paymentData, messageContainer) { + var result; + + $.when( + result = originalAction(paymentData, messageContainer) + ).fail( + function () { + var deferred = $.Deferred(), + + /** + * Update coupon form + */ + updateCouponCallback = function () { + if (quote.totals() && !quote.totals()['coupon_code']) { + coupon.setCouponCode(''); + coupon.setIsApplied(false); + } + }; + + getTotalsAction([], deferred); + $.when(deferred).done(updateCouponCallback); + } + ); + + return result; + }); + }; +}); diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js b/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js new file mode 100644 index 0000000000000..193acb8eed2f4 --- /dev/null +++ b/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js @@ -0,0 +1,34 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'mage/utils/wrapper', + 'Magento_Checkout/js/model/quote', + 'Magento_SalesRule/js/model/coupon' +], function (wrapper, quote, coupon) { + 'use strict'; + + return function (shippingSaveProcessor) { + shippingSaveProcessor.saveShippingInformation = wrapper.wrapSuper( + shippingSaveProcessor.saveShippingInformation, + function (type) { + var updateCouponCallback; + + /** + * Update coupon form + */ + updateCouponCallback = function () { + if (quote.totals() && !quote.totals()['coupon_code']) { + coupon.setCouponCode(''); + coupon.setIsApplied(false); + } + }; + + return this._super(type).done(updateCouponCallback); + } + ); + + return shippingSaveProcessor; + }; +}); diff --git a/app/code/Magento/SalesSequence/Model/Sequence/DeleteByStore.php b/app/code/Magento/SalesSequence/Model/Sequence/DeleteByStore.php index e86cc8b1b2e6d..d0c516b47577b 100644 --- a/app/code/Magento/SalesSequence/Model/Sequence/DeleteByStore.php +++ b/app/code/Magento/SalesSequence/Model/Sequence/DeleteByStore.php @@ -58,7 +58,7 @@ public function execute($storeId): void $metadataIds = $this->getMetadataIdsByStoreId($storeId); $profileIds = $this->getProfileIdsByMetadataIds($metadataIds); - $this->appResource->getConnection()->delete( + $this->appResource->getConnection('sales')->delete( $this->appResource->getTableName('sales_sequence_profile'), ['profile_id IN (?)' => $profileIds] ); @@ -70,7 +70,7 @@ public function execute($storeId): void continue; } - $this->appResource->getConnection()->dropTable( + $this->appResource->getConnection('sales')->dropTable( $metadata->getSequenceTable() ); $this->resourceMetadata->delete($metadata); @@ -85,7 +85,7 @@ public function execute($storeId): void */ private function getMetadataIdsByStoreId($storeId) { - $connection = $this->appResource->getConnection(); + $connection = $this->appResource->getConnection('sales'); $bind = ['store_id' => $storeId]; $select = $connection->select()->from( $this->appResource->getTableName('sales_sequence_meta'), @@ -105,7 +105,7 @@ private function getMetadataIdsByStoreId($storeId) */ private function getProfileIdsByMetadataIds(array $metadataIds) { - $connection = $this->appResource->getConnection(); + $connection = $this->appResource->getConnection('sales'); $select = $connection->select() ->from( $this->appResource->getTableName('sales_sequence_profile'), diff --git a/app/code/Magento/SalesSequence/Test/Unit/Model/Sequence/DeleteByStoreTest.php b/app/code/Magento/SalesSequence/Test/Unit/Model/Sequence/DeleteByStoreTest.php index 57093c8851c89..29d60ef7e6aa5 100644 --- a/app/code/Magento/SalesSequence/Test/Unit/Model/Sequence/DeleteByStoreTest.php +++ b/app/code/Magento/SalesSequence/Test/Unit/Model/Sequence/DeleteByStoreTest.php @@ -110,6 +110,7 @@ static function ($tableName) { } ); $this->resourceMock->method('getConnection') + ->with('sales') ->willReturn($this->connectionMock); $this->connectionMock ->method('select') diff --git a/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php b/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php index 57a61fecae5ca..76159dc8320e1 100644 --- a/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php +++ b/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php @@ -7,63 +7,83 @@ namespace Magento\SampleData\Console\Command; use Composer\Console\Application; +use Composer\Console\ApplicationFactory; +use Exception; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\SampleData\Model\Dependency; use Magento\Setup\Model\PackagesAuth; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\ArrayInputFactory; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Command for deployment of Sample Data + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SampleDataDeployCommand extends Command { const OPTION_NO_UPDATE = 'no-update'; /** - * @var \Magento\Framework\Filesystem + * @var Filesystem */ private $filesystem; /** - * @var \Magento\SampleData\Model\Dependency + * @var Dependency */ private $sampleDataDependency; /** - * @var \Symfony\Component\Console\Input\ArrayInputFactory + * @var ArrayInputFactory * @deprecated 100.1.0 */ private $arrayInputFactory; /** - * @var \Composer\Console\ApplicationFactory + * @var ApplicationFactory */ private $applicationFactory; /** - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\SampleData\Model\Dependency $sampleDataDependency - * @param \Symfony\Component\Console\Input\ArrayInputFactory $arrayInputFactory - * @param \Composer\Console\ApplicationFactory $applicationFactory + * @var Json + */ + private $serializer; + + /** + * @param Filesystem $filesystem + * @param Dependency $sampleDataDependency + * @param ArrayInputFactory $arrayInputFactory + * @param ApplicationFactory $applicationFactory + * @param Json $serializer */ public function __construct( - \Magento\Framework\Filesystem $filesystem, - \Magento\SampleData\Model\Dependency $sampleDataDependency, - \Symfony\Component\Console\Input\ArrayInputFactory $arrayInputFactory, - \Composer\Console\ApplicationFactory $applicationFactory + Filesystem $filesystem, + Dependency $sampleDataDependency, + ArrayInputFactory $arrayInputFactory, + ApplicationFactory $applicationFactory, + Json $serializer ) { $this->filesystem = $filesystem; $this->sampleDataDependency = $sampleDataDependency; $this->arrayInputFactory = $arrayInputFactory; $this->applicationFactory = $applicationFactory; + $this->serializer = $serializer; parent::__construct(); } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { @@ -79,15 +99,42 @@ protected function configure() } /** - * {@inheritdoc} + * @inheritdoc + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws FileSystemException + * @throws LocalizedException */ protected function execute(InputInterface $input, OutputInterface $output) { - $rootJson = json_decode($this->filesystem->getDirectoryRead(DirectoryList::ROOT)->readFile("composer.json")); - if (!isset($rootJson->version)) { - // @codingStandardsIgnoreLine - $output->writeln('<info>' . 'Git installations must deploy sample data from GitHub; see https://devdocs.magento.com/guides/v2.3/install-gde/install/sample-data-after-clone.html for more information.' . '</info>'); - return; + $rootJson = $this->serializer->unserialize( + $this->filesystem->getDirectoryRead( + DirectoryList::ROOT + )->readFile("composer.json") + ); + if (!isset($rootJson['version'])) { + $magentoProductPackage = array_filter( + $rootJson['require'], + function ($package) { + return false !== strpos($package, 'magento/product-'); + }, + ARRAY_FILTER_USE_KEY + ); + $version = reset($magentoProductPackage); + $output->writeln( + '<info>' . + // @codingStandardsIgnoreLine + 'We don\'t recommend to remove the "version" field from your composer.json; see https://getcomposer.org/doc/02-libraries.md#library-versioning for more information.' . + '</info>' + ); + $restoreVersion = new ArrayInput([ + 'command' => 'config', + 'setting-key' => 'version', + 'setting-value' => [$version], + '--quiet' => 1 + ]); } $this->updateMemoryLimit(); $this->createAuthFile(); @@ -109,6 +156,12 @@ protected function execute(InputInterface $input, OutputInterface $output) /** @var Application $application */ $application = $this->applicationFactory->create(); $application->setAutoExit(false); + if (!empty($restoreVersion)) { + $result = $application->run($restoreVersion, clone $output); + if ($result === 0) { + $output->writeln('<info>The field "version" has been restored.</info>'); + } + } $result = $application->run($commandInput, $output); if ($result !== 0) { $output->writeln( @@ -116,9 +169,15 @@ protected function execute(InputInterface $input, OutputInterface $output) . '</info>' ); $application->resetComposer(); + + return Cli::RETURN_FAILURE; } + + return Cli::RETURN_SUCCESS; } else { $output->writeln('<info>' . 'There is no sample data for current set of modules.' . '</info>'); + + return Cli::RETURN_FAILURE; } } @@ -128,7 +187,7 @@ protected function execute(InputInterface $input, OutputInterface $output) * We create auth.json with correct permissions instead of relying on Composer. * * @return void - * @throws \Exception + * @throws LocalizedException */ private function createAuthFile() { @@ -137,30 +196,51 @@ private function createAuthFile() if (!$directory->isExist(PackagesAuth::PATH_TO_AUTH_FILE)) { try { $directory->writeFile(PackagesAuth::PATH_TO_AUTH_FILE, '{}'); - } catch (\Exception $e) { - $message = 'Error in writing Auth file ' - . $directory->getAbsolutePath(PackagesAuth::PATH_TO_AUTH_FILE) - . '. Please check permissions for writing.'; - throw new \Exception($message); + } catch (Exception $e) { + throw new LocalizedException(__( + 'Error in writing Auth file %1. Please check permissions for writing.', + $directory->getAbsolutePath(PackagesAuth::PATH_TO_AUTH_FILE) + )); } } } /** + * Updates PHP memory limit + * + * @throws InvalidArgumentException * @return void */ private function updateMemoryLimit() { if (function_exists('ini_set')) { - @ini_set('display_errors', 1); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $result = ini_set('display_errors', 1); + if ($result === false) { + $error = error_get_last(); + throw new InvalidArgumentException(__( + 'Failed to set ini option display_errors to value 1. %1', + $error['message'] + )); + } $memoryLimit = trim(ini_get('memory_limit')); if ($memoryLimit != -1 && $this->getMemoryInBytes($memoryLimit) < 756 * 1024 * 1024) { - @ini_set('memory_limit', '756M'); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $result = ini_set('memory_limit', '756M'); + if ($result === false) { + $error = error_get_last(); + throw new InvalidArgumentException(__( + 'Failed to set ini option memory_limit to 756M. %1', + $error['message'] + )); + } } } } /** + * Retrieves the memory size in bytes + * * @param string $value * @return int */ diff --git a/app/code/Magento/SampleData/README.md b/app/code/Magento/SampleData/README.md index c71439b929013..e0666ba73fe24 100644 --- a/app/code/Magento/SampleData/README.md +++ b/app/code/Magento/SampleData/README.md @@ -11,7 +11,7 @@ You can deploy sample data from one of the following sources: * From the Magento composer repository, optionally using Magento CLI * From the Magento GitHub repository -If your Magento code base was cloned from the `master` branch, you can use either source of the sample data. If it was cloned from the `develop` branch, use the GitHub repository and choose to get sample data modules from the `develop` branch. +If your Magento code base was cloned from the mainline branch, you can use either source of the sample data. If it was cloned from the `develop` branch, use the GitHub repository and choose to get sample data modules from the `develop` branch. ### Deploy Sample Data from Composer Repository @@ -46,7 +46,7 @@ Each package corresponds to a sample data module. The complete list of available To deploy sample data from the GitHub repository: -1. Clone sample data from `https://github.com/magento/magento2-sample-data`. If your Magento instance was cloned from the `master` branch, choose the `master` branch when cloning sample data; choose the `develop` branch if Magento was cloned from `develop`. +1. Clone sample data from `https://github.com/magento/magento2-sample-data`. If your Magento instance was cloned from the mainline branch, choose the mainline branch when cloning sample data; choose the `develop` branch if Magento was cloned from `develop`. 2. Link the sample data and your Magento instance by running: `# php -f <sample-data_clone_dir>/dev/tools/build-sample-data.php -- --ce-source="<path_to_your_magento_instance>"` ## Install Sample Data diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php index 51235dbffc417..3bf664ea6b0d2 100644 --- a/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php @@ -20,10 +20,22 @@ use Symfony\Component\Console\Input\ArrayInputFactory; /** + * Common class for tests + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractSampleDataCommandTest extends TestCase { + /* + * Expected arguments for `composer config` to set missing field "version" + */ + private const STUB_EXPECTED_COMPOSER_CONFIG = [ + 'command' => 'config', + 'setting-key' => 'version', + 'setting-value' => ['0.0.1'], + '--quiet' => 1 + ]; + /** * @var ReadInterface|MockObject */ @@ -60,8 +72,10 @@ abstract class AbstractSampleDataCommandTest extends TestCase protected $applicationFactoryMock; /** - * @return void + * @var int */ + private $appRunResult; + protected function setUp(): void { $this->directoryReadMock = $this->getMockForAbstractClass(ReadInterface::class); @@ -74,47 +88,84 @@ protected function setUp(): void } /** - * @param array $sampleDataPackages Array in form [package_name => version_constraint] - * @param string $pathToComposerJson Fake path to composer.json - * @param int $appRunResult Composer exit code + * @param array $sampleDataPackages Array in form [package_name => version_constraint] + * @param string $pathToComposerJson Fake path to composer.json + * @param int $appRunResult Composer exit code + * @param array $composerJsonContent Content of the composer.json * @param array $additionalComposerArgs Additional arguments that composer expects */ protected function setupMocks( $sampleDataPackages, $pathToComposerJson, $appRunResult, + $composerJsonContent = [], $additionalComposerArgs = [] ) { - $this->directoryReadMock->expects($this->any())->method('getAbsolutePath')->willReturn($pathToComposerJson); - $this->directoryReadMock->expects($this->any())->method('readFile')->with('composer.json')->willReturn( - '{"version": "0.0.1"}' - ); - $this->filesystemMock->expects($this->any())->method('getDirectoryRead')->with(DirectoryList::ROOT)->willReturn( - $this->directoryReadMock - ); - $this->sampleDataDependencyMock->expects($this->any())->method('getSampleDataPackages')->willReturn( - $sampleDataPackages - ); + $this->appRunResult = $appRunResult; + $this->directoryReadMock->expects($this->any()) + ->method('getAbsolutePath') + ->willReturn($pathToComposerJson); + $this->directoryReadMock->expects($this->any()) + ->method('readFile') + ->with('composer.json') + ->willReturn(json_encode($composerJsonContent)); + $this->filesystemMock->expects($this->any()) + ->method('getDirectoryRead') + ->with(DirectoryList::ROOT) + ->willReturn($this->directoryReadMock); + $this->sampleDataDependencyMock->expects($this->any()) + ->method('getSampleDataPackages') + ->willReturn($sampleDataPackages); $this->arrayInputFactoryMock->expects($this->never())->method('create'); - $this->applicationMock->expects($this->any()) - ->method('run') - ->with( - new ArrayInput( - array_merge( - $this->expectedComposerArguments( - $sampleDataPackages, - $pathToComposerJson + if (!array_key_exists('version', $composerJsonContent)) { + $this->applicationMock->expects($this->any()) + ->method('run') + ->withConsecutive( + [ + 'input' => new ArrayInput( + self::STUB_EXPECTED_COMPOSER_CONFIG ), - $additionalComposerArgs - ) - ), - $this->anything() - ) - ->willReturn($appRunResult); + 'output' => $this->anything() + ], + [ + 'input' => new ArrayInput( + array_merge( + $this->expectedComposerArgumentsSampleDataCommands( + $sampleDataPackages, + $pathToComposerJson + ), + $additionalComposerArgs + ) + ), + 'output' => $this->anything() + ] + )->willReturnOnConsecutiveCalls( + $this->returnValue(0), + $this->returnValue($appRunResult) + ); + } else { + $this->applicationMock->expects($this->any()) + ->method('run') + ->with( + new ArrayInput( + array_merge( + $this->expectedComposerArgumentsSampleDataCommands( + $sampleDataPackages, + $pathToComposerJson + ), + $additionalComposerArgs + ) + ), + $this->anything() + ) + ->willReturn($appRunResult); + } if (($appRunResult !== 0) && !empty($sampleDataPackages)) { - $this->applicationMock->expects($this->once())->method('resetComposer')->willReturnSelf(); + $this->applicationMock->expects($this->any()) + ->method('resetComposer') + ->willReturnSelf(); } $this->applicationFactoryMock->expects($this->any()) @@ -123,14 +174,14 @@ protected function setupMocks( } /** - * Expected arguments for composer based on sample data packages and composer.json path + * Expected arguments for composer based on sample data command * * @param array $sampleDataPackages * @param string $pathToComposerJson * @return array */ - abstract protected function expectedComposerArguments( + abstract protected function expectedComposerArgumentsSampleDataCommands( array $sampleDataPackages, string $pathToComposerJson - ) : array; + ): array; } diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php index 45db83403b4f5..a1186d6015871 100644 --- a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php @@ -7,26 +7,54 @@ namespace Magento\SampleData\Test\Unit\Console\Command; +use Exception; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Serialize\Serializer\Json; use Magento\SampleData\Console\Command\SampleDataDeployCommand; use Magento\Setup\Model\PackagesAuth; +use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\Console\Tester\CommandTester; class SampleDataDeployCommandTest extends AbstractSampleDataCommandTest { /** + * @var Json|MockObject + */ + private $serializerMock; + + protected function setUp(): void + { + parent::setUp(); + $this->serializerMock = $this->createMock(Json::class); + } + + /** + * Sets mock for unserialization composer content + * @param array $composerJsonContent + * @return void + */ + protected function setupMockForSerializer(array $composerJsonContent): void + { + $this->serializerMock->expects($this->any()) + ->method('unserialize') + ->will($this->returnValue($composerJsonContent)); + } + + /** + * Sets mocks for auth file + * * @param bool $authExist True to test with existing auth.json, false without + * @return void */ - protected function setupMocksForAuthFile($authExist) + protected function setupMocksForAuthFile(bool $authExist): void { $this->directoryWriteMock->expects($this->once()) ->method('isExist') ->with(PackagesAuth::PATH_TO_AUTH_FILE) ->willReturn($authExist); - $this->directoryWriteMock->expects($authExist ? $this->never() : $this->once())->method('writeFile')->with( - PackagesAuth::PATH_TO_AUTH_FILE, - '{}' - ); + $this->directoryWriteMock->expects($authExist ? $this->never() : $this->once()) + ->method('writeFile') + ->with(PackagesAuth::PATH_TO_AUTH_FILE, '{}'); $this->filesystemMock->expects($this->once()) ->method('getDirectoryWrite') ->with(DirectoryList::COMPOSER_HOME) @@ -34,18 +62,30 @@ protected function setupMocksForAuthFile($authExist) } /** - * @param array $sampleDataPackages - * @param int $appRunResult - int 0 if everything went fine, or an error code - * @param string $expectedMsg - * @param bool $authExist - * @return void + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param array $composerJsonContent + * @param string $expectedMsg + * @param bool $authExist + * @return void * * @dataProvider processDataProvider */ - public function testExecute(array $sampleDataPackages, $appRunResult, $expectedMsg, $authExist) - { - $this->setupMocks($sampleDataPackages, '/path/to/composer.json', $appRunResult); + public function testExecute( + array $sampleDataPackages, + int $appRunResult, + array $composerJsonContent, + string $expectedMsg, + bool $authExist + ): void { + $this->setupMocks( + $sampleDataPackages, + '/path/to/composer.json', + $appRunResult, + $composerJsonContent + ); $this->setupMocksForAuthFile($authExist); + $this->setupMockForSerializer($composerJsonContent); $commandTester = $this->createCommandTester(); $commandTester->execute([]); @@ -53,23 +93,31 @@ public function testExecute(array $sampleDataPackages, $appRunResult, $expectedM } /** - * @param array $sampleDataPackages - * @param int $appRunResult - int 0 if everything went fine, or an error code - * @param string $expectedMsg - * @param bool $authExist - * @return void + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param array $composerJsonContent + * @param string $expectedMsg + * @param bool $authExist + * @return void * * @dataProvider processDataProvider */ - public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult, $expectedMsg, $authExist) - { + public function testExecuteWithNoUpdate( + array $sampleDataPackages, + int $appRunResult, + array $composerJsonContent, + string $expectedMsg, + bool $authExist + ): void { $this->setupMocks( $sampleDataPackages, '/path/to/composer.json', $appRunResult, + $composerJsonContent, ['--no-update' => 1] ); $this->setupMocksForAuthFile($authExist); + $this->setupMockForSerializer($composerJsonContent); $commandInput = ['--no-update' => 1]; $commandTester = $this->createCommandTester(); @@ -79,14 +127,20 @@ public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult } /** + * Data provider + * * @return array */ - public function processDataProvider() + public function processDataProvider(): array { return [ 'No sample data found' => [ 'sampleDataPackages' => [], 'appRunResult' => 1, + 'composerJsonContent' => [ + 'require' => ["magento/product-community-edition" => "0.0.1"], + 'version' => '0.0.1' + ], 'expectedMsg' => 'There is no sample data for current set of modules.' . PHP_EOL, 'authExist' => true, ], @@ -95,15 +149,36 @@ public function processDataProvider() 'magento/module-cms-sample-data' => '1.0.0-beta', ], 'appRunResult' => 1, + 'composerJsonContent' => [ + 'require' => ["magento/product-community-edition" => "0.0.1"], + 'version' => '0.0.1' + ], 'expectedMsg' => 'There is an error during sample data deployment. Composer file will be reverted.' . PHP_EOL, 'authExist' => false, ], + 'Successful sample data installation without field "version"' => [ + 'sampleDataPackages' => [ + 'magento/module-cms-sample-data' => '1.0.0-beta', + ], + 'appRunResult' => 0, + 'composerJsonContent' => [ + 'require' => ["magento/product-community-edition" => "0.0.1"] + ], + // @codingStandardsIgnoreLine + 'expectedMsg' => 'We don\'t recommend to remove the "version" field from your composer.json; see https://getcomposer.org/doc/02-libraries.md#library-versioning for more information.' + . PHP_EOL . 'The field "version" has been restored.' . PHP_EOL, + 'authExist' => true, + ], 'Successful sample data installation' => [ 'sampleDataPackages' => [ 'magento/module-cms-sample-data' => '1.0.0-beta', ], 'appRunResult' => 0, + 'composerJsonContent' => [ + 'require' => ["magento/product-community-edition" => "0.0.1"], + 'version' => '0.0.1' + ], 'expectedMsg' => '', 'authExist' => true, ], @@ -113,7 +188,7 @@ public function processDataProvider() /** * @return void */ - public function testExecuteWithException() + public function testExecuteWithException(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage( @@ -122,12 +197,17 @@ public function testExecuteWithException() $this->directoryReadMock->expects($this->once()) ->method('readFile') ->with('composer.json') - ->willReturn('{"version": "0.0.1"}'); + ->willReturn('{"require": {"magento/product-community-edition": "0.0.1"}, "version": "0.0.1"}'); + $this->serializerMock->expects($this->any()) + ->method('unserialize') + ->will($this->returnValue([ + 'require' => ["magento/product-community-edition" => "0.0.1"], + 'version' => '0.0.1' + ])); $this->filesystemMock->expects($this->once()) ->method('getDirectoryRead') ->with(DirectoryList::ROOT) ->willReturn($this->directoryReadMock); - $this->directoryWriteMock->expects($this->once()) ->method('isExist') ->with(PackagesAuth::PATH_TO_AUTH_FILE) @@ -135,7 +215,7 @@ public function testExecuteWithException() $this->directoryWriteMock->expects($this->once()) ->method('writeFile') ->with(PackagesAuth::PATH_TO_AUTH_FILE, '{}') - ->willThrowException(new \Exception('Something went wrong...')); + ->willThrowException(new Exception('Something went wrong...')); $this->directoryWriteMock->expects($this->once()) ->method('getAbsolutePath') ->with(PackagesAuth::PATH_TO_AUTH_FILE) @@ -153,15 +233,15 @@ public function testExecuteWithException() */ private function createCommandTester(): CommandTester { - $commandTester = new CommandTester( + return new CommandTester( new SampleDataDeployCommand( $this->filesystemMock, $this->sampleDataDependencyMock, $this->arrayInputFactoryMock, - $this->applicationFactoryMock + $this->applicationFactoryMock, + $this->serializerMock ) ); - return $commandTester; } /** @@ -169,10 +249,10 @@ private function createCommandTester(): CommandTester * @param $pathToComposerJson * @return array */ - protected function expectedComposerArguments( + protected function expectedComposerArgumentsSampleDataCommands( array $sampleDataPackages, string $pathToComposerJson - ) : array { + ): array { return [ 'command' => 'require', '--working-dir' => $pathToComposerJson, diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php index cbb562ff10f25..9883100ce5c49 100644 --- a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php @@ -10,20 +10,32 @@ use Magento\SampleData\Console\Command\SampleDataRemoveCommand; use Symfony\Component\Console\Tester\CommandTester; +/** + * Tests for command `sampledata:remove` + */ class SampleDataRemoveCommandTest extends AbstractSampleDataCommandTest { - /** - * @param array $sampleDataPackages - * @param int $appRunResult - int 0 if everything went fine, or an error code - * @param string $expectedMsg - * @return void + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param array $composerJsonContent + * @param string $expectedMsg + * @return void * * @dataProvider processDataProvider */ - public function testExecute(array $sampleDataPackages, $appRunResult, $expectedMsg) - { - $this->setupMocks($sampleDataPackages, '/path/to/composer.json', $appRunResult); + public function testExecute( + array $sampleDataPackages, + int $appRunResult, + array $composerJsonContent, + string $expectedMsg + ): void { + $this->setupMocks( + $sampleDataPackages, + '/path/to/composer.json', + $appRunResult, + $composerJsonContent + ); $commandTester = $this->createCommandTester(); $commandTester->execute([]); @@ -31,19 +43,25 @@ public function testExecute(array $sampleDataPackages, $appRunResult, $expectedM } /** - * @param array $sampleDataPackages - * @param int $appRunResult - int 0 if everything went fine, or an error code - * @param string $expectedMsg - * @return void + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param array $composerJsonContent + * @param string $expectedMsg + * @return void * * @dataProvider processDataProvider */ - public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult, $expectedMsg) - { + public function testExecuteWithNoUpdate( + array $sampleDataPackages, + int $appRunResult, + array $composerJsonContent, + string $expectedMsg + ): void { $this->setupMocks( $sampleDataPackages, '/path/to/composer.json', $appRunResult, + $composerJsonContent, ['--no-update' => 1] ); $commandInput = ['--no-update' => 1]; @@ -55,32 +73,51 @@ public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult } /** + * Data provider + * * @return array */ - public function processDataProvider() + public function processDataProvider(): array { return [ - 'No sample data found' => [ - 'sampleDataPackages' => [], + 'No sample data found in require' => [ + 'sampleDataPackages' => [ + 'magento/module-cms-sample-data' => '1.0.0-beta', + ], 'appRunResult' => 1, - 'expectedMsg' => 'There is no sample data for current set of modules.' . PHP_EOL, + 'composerJsonContent' => [ + "require" => [ + "magento/product-community-edition" => "0.0.1", + ], + "version" => "0.0.1" + ], + 'expectedMsg' => 'There is an error during remove sample data.' . PHP_EOL, ], - 'Successful sample data installation' => [ + 'Successful sample data removing' => [ 'sampleDataPackages' => [ 'magento/module-cms-sample-data' => '1.0.0-beta', ], 'appRunResult' => 0, + 'composerJsonContent' => [ + "require" => [ + "magento/product-community-edition" => "0.0.1", + "magento/module-cms-sample-data" => "1.0.0-beta", + ], + "version" => "0.0.1" + ], 'expectedMsg' => '', ], ]; } /** + * Creates command tester + * * @return CommandTester */ private function createCommandTester(): CommandTester { - $commandTester = new CommandTester( + return new CommandTester( new SampleDataRemoveCommand( $this->filesystemMock, $this->sampleDataDependencyMock, @@ -88,15 +125,16 @@ private function createCommandTester(): CommandTester $this->applicationFactoryMock ) ); - return $commandTester; } /** + * Returns expected arguments for command `composer remove` + * * @param $sampleDataPackages * @param $pathToComposerJson * @return array */ - protected function expectedComposerArguments( + protected function expectedComposerArgumentsSampleDataCommands( array $sampleDataPackages, string $pathToComposerJson ) : array { diff --git a/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php b/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php index a73edcce99760..3fa8fa9d417f3 100644 --- a/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php +++ b/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php @@ -8,11 +8,13 @@ use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; /** - * Class DeleteButton + * Delete Synonyms Group Button Class */ class DeleteButton extends GenericButton implements ButtonProviderInterface { /** + * Delete Button Data + * * @return array */ public function getButtonData() @@ -24,7 +26,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __('Are you sure you want to delete this synonym group?') - . '\', \'' . $this->getDeleteUrl() . '\')', + . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})', 'sort_order' => 20, ]; } @@ -32,6 +34,8 @@ public function getButtonData() } /** + * Delete Url + * * @return string */ public function getDeleteUrl() diff --git a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php index 9d8b612cefadf..06d15d4d7124e 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php @@ -6,10 +6,12 @@ namespace Magento\Search\Controller\Adminhtml\Synonyms; +use Magento\Framework\App\Action\HttpPostActionInterface; + /** * Delete Controller */ -class Delete extends \Magento\Backend\App\Action +class Delete extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Search/Model/AdapterFactory.php b/app/code/Magento/Search/Model/AdapterFactory.php index 917603ce57dc3..f6d2013bd4886 100644 --- a/app/code/Magento/Search/Model/AdapterFactory.php +++ b/app/code/Magento/Search/Model/AdapterFactory.php @@ -17,7 +17,7 @@ class AdapterFactory * Scope configuration * * @var \Magento\Framework\App\Config\ScopeConfigInterface - * @deprecated since it is not used anymore + * @deprecated 101.0.0 since it is not used anymore */ protected $scopeConfig; @@ -32,13 +32,13 @@ class AdapterFactory * Config path * * @var string - * @deprecated since it is not used anymore + * @deprecated 101.0.0 since it is not used anymore */ protected $path; /** * Config Scope - * @deprecated since it is not used anymore + * @deprecated 101.0.0 since it is not used anymore */ protected $scope; diff --git a/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php b/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php index 2fc71fc6a6d73..8d3db36e35dec 100644 --- a/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php +++ b/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php @@ -168,6 +168,7 @@ public function setPopularQueryFilter($storeIds = null) * @param int $storeId * @param int $maxCountCacheableSearchTerms * @return bool + * @since 101.1.0 */ public function isTopSearchResult(string $term, int $storeId, int $maxCountCacheableSearchTerms):bool { diff --git a/app/code/Magento/Search/Model/Search/PageSizeProvider.php b/app/code/Magento/Search/Model/Search/PageSizeProvider.php index 5572bac6addc3..ae2a8ca954d63 100644 --- a/app/code/Magento/Search/Model/Search/PageSizeProvider.php +++ b/app/code/Magento/Search/Model/Search/PageSizeProvider.php @@ -10,6 +10,7 @@ /** * Returns max page size by search engine name * @api + * @since 101.0.0 */ class PageSizeProvider { @@ -39,6 +40,7 @@ public function __construct( * Returns max_page_size depends on engine * * @return integer + * @since 101.0.0 */ public function getMaxPageSize() : int { diff --git a/app/code/Magento/Search/Model/SearchEngine/Validator.php b/app/code/Magento/Search/Model/SearchEngine/Validator.php index f4fc8a9a62e0e..264e7c69dd520 100644 --- a/app/code/Magento/Search/Model/SearchEngine/Validator.php +++ b/app/code/Magento/Search/Model/SearchEngine/Validator.php @@ -22,7 +22,7 @@ class Validator implements ValidatorInterface /** * @var array */ - private $engineBlacklist = ['mysql' => 'MySQL']; + private $excludedEngineList = ['mysql' => 'MySQL']; /** * @var ValidatorInterface[] @@ -32,16 +32,16 @@ class Validator implements ValidatorInterface /** * @param ScopeConfigInterface $scopeConfig * @param array $engineValidators - * @param array $engineBlacklist + * @param array $excludedEngineList */ public function __construct( ScopeConfigInterface $scopeConfig, array $engineValidators = [], - array $engineBlacklist = [] + array $excludedEngineList = [] ) { $this->scopeConfig = $scopeConfig; $this->engineValidators = $engineValidators; - $this->engineBlacklist = array_merge($this->engineBlacklist, $engineBlacklist); + $this->excludedEngineList = array_merge($this->excludedEngineList, $excludedEngineList); } /** @@ -51,9 +51,9 @@ public function validate(): array { $errors = []; $currentEngine = $this->scopeConfig->getValue('catalog/search/engine'); - if (isset($this->engineBlacklist[$currentEngine])) { - $blacklistedEngine = $this->engineBlacklist[$currentEngine]; - $errors[] = "Your current search engine, '{$blacklistedEngine}', is not supported." + if (isset($this->excludedEngineList[$currentEngine])) { + $excludedEngine = $this->excludedEngineList[$currentEngine]; + $errors[] = "Your current search engine, '{$excludedEngine}', is not supported." . " You must install a supported search engine before upgrading." . " See the System Upgrade Guide for more information."; } diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminFillNewSearchSynonymsActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminFillNewSearchSynonymsActionGroup.xml new file mode 100644 index 0000000000000..ae4128e4f5d9a --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminFillNewSearchSynonymsActionGroup.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="AdminFillNewSearchSynonymsActionGroup"> + <annotations> + <description>Fills the search synonyms form field.</description> + </annotations> + <arguments> + <argument name="scope_id" type="string"/> + <argument name="synonyms" type="string"/> + </arguments> + + <selectOption selector="{{AdminSearchSynonymsNewSection.scope}}" userInput="{{scope_id}}" stepKey="selectScope"/> + <fillField selector="{{AdminSearchSynonymsNewSection.synonyms}}" userInput="{{synonyms}}" stepKey="fillSynonyms"/> + <checkOption selector="{{AdminSearchSynonymsNewSection.merge}}" stepKey="checkCheckbox"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminNavigateToNewSearchSynonymsPageActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminNavigateToNewSearchSynonymsPageActionGroup.xml new file mode 100644 index 0000000000000..6c14aba36a139 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminNavigateToNewSearchSynonymsPageActionGroup.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="AdminNavigateToNewSearchSynonymsPageActionGroup"> + <click stepKey="clickNewSynonymsGroupButton" selector="{{AdminSearchSynonymsGridSection.add}}"/> + <waitForPageLoad stepKey="waitForNewSearchSynonymsPageLoaded"/> + </actionGroup> +</actionGroups> 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/ActionGroup/StoreFrontQuickSearchActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontQuickSearchActionGroup.xml index aec874e7b6d85..840d8439e3d63 100644 --- a/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontQuickSearchActionGroup.xml +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontQuickSearchActionGroup.xml @@ -16,5 +16,6 @@ <fillField stepKey="fillSearchField" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="{{query}}"/> <waitForElementVisible selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="waitForSubmitButton"/> <click stepKey="clickSearchButton" selector="{{StorefrontQuickSearchSection.searchButton}}"/> + <waitForPageLoad stepKey="waitForSearchResults"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/Data/SearchSynonymsData.xml b/app/code/Magento/Search/Test/Mftf/Data/SearchSynonymsData.xml new file mode 100644 index 0000000000000..e8242b5694739 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Data/SearchSynonymsData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminSearchSynonyms" type="SearchSynonyms"> + <data key="pageTitle">Search Synonyms</data> + <data key="title">Search Synonyms</data> + <data key="dataUiId">magento-search-search-synonyms</data> + </entity> +</entities> diff --git a/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsGridSection.xml b/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsGridSection.xml new file mode 100644 index 0000000000000..fe97fe7a5663d --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsGridSection.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="AdminSearchSynonymsGridSection"> + <element name="add" type="button" selector=".page-actions-buttons #add"/> + </section> +</sections> diff --git a/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsNewSection.xml b/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsNewSection.xml new file mode 100644 index 0000000000000..73a39a25325f7 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsNewSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminSearchSynonymsNewSection"> + <element name="save" type="button" selector="//button[@id='save']"/> + <element name="resetButton" type="button" selector="//button[@id='reset']"/> + <element name="scope" type="select" selector="//select[@name='scope_id']"/> + <element name="synonyms" type="textarea" selector="//textarea[@name='synonyms']"/> + <element name="merge" type="checkbox" selector="//input[@name='mergeOnConflict']"/> + </section> +</sections> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml index 82ec95b24d3ca..189b8962957a2 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml @@ -38,8 +38,7 @@ </after> <!-- Create Simple Product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="adminProductIndexPageAdd"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="adminProductIndexPageAdd"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProductPage"> <argument name="product" value="SimpleProduct"/> </actionGroup> 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/AdminNewSearchSynonymsFormResetTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminNewSearchSynonymsFormResetTest.xml new file mode 100644 index 0000000000000..24a5bf16704ff --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminNewSearchSynonymsFormResetTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminNewSearchSynonymsFormResetTest"> + <annotations> + <features value="Search"/> + <stories value="Reset new search synonyms group form"/> + <title value="Admin reset new search synonyms group form"/> + <description value="When admin users reset button on new search synonyms form all fields should be set to default values"/> + <testCaseId value="MC-36382"/> + <severity value="AVERAGE"/> + <group value="Search"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSearchSynonymsPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminSearchSynonyms.dataUiId}}"/> + </actionGroup> + + <actionGroup ref="AdminNavigateToNewSearchSynonymsPageActionGroup" stepKey="navigateToNewSearchSynonymsPage"/> + + <actionGroup ref="AdminFillNewSearchSynonymsActionGroup" stepKey="fillNewSearchSynonyms"> + <argument name="scope_id" value="1:1"/> + <argument name="synonyms" value="Test Synonyms"/> + </actionGroup> + + <click selector="{{AdminSearchSynonymsNewSection.resetButton}}" stepKey="clickResetButton"/> + + <grabValueFrom selector="{{AdminSearchSynonymsNewSection.scope}}" stepKey="grabScopeValue"/> + <assertEquals stepKey="assertScopeDefaultValue"> + <expectedResult type="string">0:0</expectedResult> + <actualResult type="string">$grabScopeValue</actualResult> + </assertEquals> + + <grabValueFrom selector="{{AdminSearchSynonymsNewSection.synonyms}}" stepKey="grabSynonymsValue"/> + <assertEmpty stepKey="assertSynonymsDefaultValue"> + <actualResult type="string">$grabSynonymsValue</actualResult> + </assertEmpty> + + <grabValueFrom selector="{{AdminSearchSynonymsNewSection.merge}}" stepKey="grabMergeValue"/> + <assertEquals stepKey="assertMergeDefaultValue"> + <expectedResult type="string">false</expectedResult> + <actualResult type="string">$grabMergeValue</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontUsingElasticSearchWithWeightAttributeTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontUsingElasticSearchWithWeightAttributeTest.xml index 18f623288621d..b082014c7b120 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontUsingElasticSearchWithWeightAttributeTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontUsingElasticSearchWithWeightAttributeTest.xml @@ -49,7 +49,9 @@ <!-- Step 3 --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> <!-- Step 4 --> - <magentoCLI command="cache:clean" arguments="full_page" stepKey="clearFPC"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="clearFPC"> + <argument name="tags" value="full_page"/> + </actionGroup> <!-- Step 5 --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> <!-- Step 6 --> 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 3bfa777ac27d8..8c468cce91829 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml @@ -24,15 +24,18 @@ <createData entity="SimpleProductWithDescription" stepKey="simpleProduct"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- 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}}"/> @@ -56,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 93a3c8ca8e4a2..fb1f35730fd80 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml @@ -26,16 +26,19 @@ <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete create 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"> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml index ebe3b6c129721..1558f9aa5342b 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml @@ -26,8 +26,12 @@ <createData entity="ApiProductWithDescription" stepKey="product"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> @@ -35,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 e72f614593cfe..19c12843c23a2 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml @@ -26,8 +26,12 @@ <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> @@ -35,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/Test/Unit/Model/SearchEngine/ValidatorTest.php b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php index c91c0fce9dd47..cc272ccb60162 100644 --- a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php @@ -34,7 +34,7 @@ protected function setUp(): void [ 'scopeConfig' => $this->scopeConfigMock, 'engineValidators' => ['otherEngine' => $this->otherEngineValidatorMock], - 'engineBlacklist' => ['badEngine' => 'Bad Engine'] + 'excludedEngineList' => ['badEngine' => 'Bad Engine'] ] ); } @@ -54,7 +54,7 @@ public function testValidateValid() $this->assertEquals($expectedErrors, $this->validator->validate()); } - public function testValidateBlacklist() + public function testValidateExcludedList() { $this->scopeConfigMock ->expects($this->once()) diff --git a/app/code/Magento/Search/Test/Unit/Ui/Component/Listing/Column/SynonymActionsTest.php b/app/code/Magento/Search/Test/Unit/Ui/Component/Listing/Column/SynonymActionsTest.php index 4613ef3d45ede..729a8a7e737fd 100644 --- a/app/code/Magento/Search/Test/Unit/Ui/Component/Listing/Column/SynonymActionsTest.php +++ b/app/code/Magento/Search/Test/Unit/Ui/Component/Listing/Column/SynonymActionsTest.php @@ -112,6 +112,7 @@ public function testPrepareDataSourceWithItems() self::STUB_SYNONYM_GROUP_ID ) ], + 'post' => true ], 'edit' => [ 'href' => sprintf( diff --git a/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php b/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php index 2fd569642375e..191726bd2689b 100644 --- a/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php +++ b/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php @@ -64,6 +64,7 @@ public function prepareDataSource(array $dataSource) 'title' => __('Delete'), 'message' => __('Are you sure you want to delete synonym group with id: %1?', $item['group_id']) ], + 'post' => true ]; $item[$name]['edit'] = [ 'href' => $this->urlBuilder->getUrl(self::SYNONYM_URL_PATH_EDIT, ['group_id' => $item['group_id']]), diff --git a/app/code/Magento/Search/ViewModel/ConfigProvider.php b/app/code/Magento/Search/ViewModel/ConfigProvider.php new file mode 100644 index 0000000000000..be3366e62e965 --- /dev/null +++ b/app/code/Magento/Search/ViewModel/ConfigProvider.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\ViewModel; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * View model for search + */ +class ConfigProvider implements ArgumentInterface +{ + /** + * Suggestions settings config paths + */ + private const SEARCH_SUGGESTION_ENABLED = 'catalog/search/search_suggestion_enabled'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Is Search Suggestions Allowed + * + * @return bool + */ + public function isSuggestionsAllowed(): bool + { + return $this->scopeConfig->isSetFlag( + self::SEARCH_SUGGESTION_ENABLED, + ScopeInterface::SCOPE_STORE + ); + } +} diff --git a/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_form.xml b/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_form.xml index b084a0ad16aaa..cf2b0704dc15b 100644 --- a/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_form.xml +++ b/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_form.xml @@ -63,14 +63,14 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">block</item> - <item name="default" xsi:type="number">0</item> + <item name="default" xsi:type="string">0:0</item> </item> </argument> <settings> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> </validation> - <dataType>int</dataType> + <dataType>text</dataType> <tooltip> <link>https://docs.magento.com/m2/ce/user_guide/stores/websites-stores-views.html</link> <description translate="true">You can adjust the scope of this synonym group by selecting an option from the list.</description> diff --git a/app/code/Magento/Search/view/frontend/layout/default.xml b/app/code/Magento/Search/view/frontend/layout/default.xml index 0cb18adedd952..69c99f979d51b 100644 --- a/app/code/Magento/Search/view/frontend/layout/default.xml +++ b/app/code/Magento/Search/view/frontend/layout/default.xml @@ -8,7 +8,11 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="header-wrapper"> - <block class="Magento\Framework\View\Element\Template" name="top.search" as="topSearch" template="Magento_Search::form.mini.phtml" /> + <block class="Magento\Framework\View\Element\Template" name="top.search" as="topSearch" template="Magento_Search::form.mini.phtml"> + <arguments> + <argument name="configProvider" xsi:type="object">Magento\Search\ViewModel\ConfigProvider</argument> + </arguments> + </block> </referenceContainer> <referenceBlock name="footer_links"> <block class="Magento\Framework\View\Element\Html\Link\Current" ifconfig="catalog/seo/search_terms" name="search-term-popular-link"> diff --git a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml index 35f3876599731..80e720e2c2fe2 100644 --- a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml +++ b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml @@ -9,7 +9,9 @@ <?php /** @var $block \Magento\Framework\View\Element\Template */ /** @var $helper \Magento\Search\Helper\Data */ +/** @var $configProvider \Magento\Search\ViewModel\ConfigProvider */ $helper = $this->helper(\Magento\Search\Helper\Data::class); +$configProvider = $block->getData('configProvider'); ?> <div class="block block-search"> <div class="block block-title"><strong><?= $block->escapeHtml(__('Search')) ?></strong></div> @@ -22,12 +24,14 @@ $helper = $this->helper(\Magento\Search\Helper\Data::class); </label> <div class="control"> <input id="search" - data-mage-init='{"quickSearch":{ - "formSelector":"#search_mini_form", - "url":"<?= $block->escapeUrl($helper->getSuggestUrl())?>", - "destinationSelector":"#search_autocomplete", - "minSearchLength":"<?= $block->escapeHtml($helper->getMinQueryLength()) ?>"} - }' + <?php if ($configProvider->isSuggestionsAllowed()):?> + data-mage-init='{"quickSearch":{ + "formSelector":"#search_mini_form", + "url":"<?= $block->escapeUrl($helper->getSuggestUrl())?>", + "destinationSelector":"#search_autocomplete", + "minSearchLength":"<?= $block->escapeHtml($helper->getMinQueryLength()) ?>"} + }' + <?php endif;?> type="text" name="<?= $block->escapeHtmlAttr($helper->getQueryParamName()) ?>" value="<?= /* @noEscape */ $helper->getEscapedQueryText() ?>" diff --git a/app/code/Magento/Search/view/frontend/templates/term.phtml b/app/code/Magento/Search/view/frontend/templates/term.phtml index b06ebcfe66966..51f40e8247ccf 100644 --- a/app/code/Magento/Search/view/frontend/templates/term.phtml +++ b/app/code/Magento/Search/view/frontend/templates/term.phtml @@ -3,19 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Search\Block\Term $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if (count($block->getTerms()) > 0) : ?> +<?php if (count($block->getTerms()) > 0): ?> <ul class="search-terms"> - <?php foreach ($block->getTerms() as $_term) : ?> - <li class="item"> - <a href="<?= $block->escapeUrl($block->getSearchUrl($_term)) ?>" - style="font-size:<?= /* @noEscape */ $_term->getRatio()*70+75 ?>%;"> + <?php foreach ($block->getTerms() as $_term): ?> + <li id="term-<?= /* @noEscape */ $_term->getId() ?>" class="item"> + <a href="<?= $block->escapeUrl($block->getSearchUrl($_term)) ?>"> <?= $block->escapeHtml($_term->getQueryText()) ?> </a> </li> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "font-size:" . ($_term->getRatio()*70+75) . "%;", + 'li#term-' . $_term->getId() + ) ?> <?php endforeach; ?> </ul> -<?php else : ?> +<?php else: ?> <div class="message notice"> <div><?= $block->escapeHtml(__('There are no search terms available.')) ?></div> </div> diff --git a/app/code/Magento/Search/view/frontend/web/js/form-mini.js b/app/code/Magento/Search/view/frontend/web/js/form-mini.js index 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/Security/Model/Plugin/Auth.php b/app/code/Magento/Security/Model/Plugin/Auth.php index 833b4e4c1b774..b388ef6115867 100644 --- a/app/code/Magento/Security/Model/Plugin/Auth.php +++ b/app/code/Magento/Security/Model/Plugin/Auth.php @@ -35,6 +35,8 @@ public function __construct( } /** + * Add warning message if other sessions terminated + * * @param \Magento\Backend\Model\Auth $authModel * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -43,11 +45,13 @@ public function afterLogin(\Magento\Backend\Model\Auth $authModel) { $this->sessionsManager->processLogin(); if ($this->sessionsManager->getCurrentSession()->isOtherSessionsTerminated()) { - $this->messageManager->addWarning(__('All other open sessions for this account were terminated.')); + $this->messageManager->addWarningMessage(__('All other open sessions for this account were terminated.')); } } /** + * Handle logout process + * * @param \Magento\Backend\Model\Auth $authModel * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml index a75f65dffeca3..83e3479c753e4 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml @@ -18,8 +18,6 @@ <testCaseId value="MC-14382" /> <group value="security"/> <group value="mtf_migrated"/> - <!-- skip due to MQE-1964 --> - <group value="skip"/> </annotations> <before> <!-- Log in to Admin Panel --> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml index 3d04f3eed4daf..3fffbcd480761 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml @@ -18,8 +18,6 @@ <testCaseId value="MC-14384" /> <group value="security"/> <group value="mtf_migrated"/> - <!-- skip due to MQE-1964 --> - <group value="skip"/> </annotations> <before> <!-- Log in to Admin Panel --> @@ -41,7 +39,7 @@ <argument name="message" value="The password entered for the current user is invalid. Verify the password and try again." /> <argument name="messageType" value="error" /> </actionGroup> - + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillFieldSecondAttempt"> <argument name="role" value="roleAdministrator" /> <argument name="currentAdminPassword" value="{{_ENV.MAGENTO_ADMIN_PASSWORD}}INVALID" /> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml similarity index 97% rename from app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml rename to app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml index 3fb798521fb45..1421b589d5669 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml @@ -9,7 +9,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminLoginAdminUserWithInvalidExpiration"> + <test name="AdminLoginAdminUserWithInvalidExpirationTest"> <annotations> <features value="Security"/> <stories value="Try to login as a user with an invalid expiration date."/> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml similarity index 97% rename from app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml rename to app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml index 5d12650351bc0..9a9ae8f3872ba 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml @@ -9,7 +9,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminLoginAdminUserWithValidExpiration"> + <test name="AdminLoginAdminUserWithValidExpirationTest"> <annotations> <features value="Security"/> <stories value="Login as a user with a valid expiration date."/> diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php index c431f1ecda332..dd86b3b574ead 100644 --- a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php @@ -64,7 +64,7 @@ protected function setUp(): void $this->messageManager = $this->getMockForAbstractClass( ManagerInterface::class, - ['addWarning'], + ['addWarningMessage'], '', false ); @@ -100,7 +100,7 @@ public function testAfterLogin() ->method('isOtherSessionsTerminated') ->willReturn(true); $this->messageManager->expects($this->once()) - ->method('addWarning') + ->method('addWarningMessage') ->with($warningMessage); $this->model->afterLogin($this->authMock); diff --git a/app/code/Magento/Security/view/base/requirejs-config.js b/app/code/Magento/Security/view/base/requirejs-config.js new file mode 100644 index 0000000000000..579980336f2cb --- /dev/null +++ b/app/code/Magento/Security/view/base/requirejs-config.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + map: { + '*': { + escaper: 'Magento_Security/js/escaper' + } + } +}; diff --git a/app/code/Magento/Security/view/base/web/js/escaper.js b/app/code/Magento/Security/view/base/web/js/escaper.js new file mode 100644 index 0000000000000..dc1c896ad6836 --- /dev/null +++ b/app/code/Magento/Security/view/base/web/js/escaper.js @@ -0,0 +1,174 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * A loose JavaScript version of Magento\Framework\Escaper + * + * Due to differences in how XML/HTML is processed in PHP vs JS there are a couple of minor differences in behavior + * from the PHP counterpart. + * + * The first difference is that the default invocation of escapeHtml without allowedTags will double-escape existing + * entities as the intention of such an invocation is that the input isn't supposed to contain any HTML. + * + * The second difference is that escapeHtml will not escape quotes. Since the input is actually being processed by the + * DOM there is no chance of quotes being mixed with HTML syntax. And, since escapeHtml is not + * intended to be used with raw injection into a HTML attribute, this is acceptable. + * + * @api + */ +define([], function () { + 'use strict'; + + return { + neverAllowedElements: ['script', 'img', 'embed', 'iframe', 'video', 'source', 'object', 'audio'], + generallyAllowedAttributes: ['id', 'class', 'href', 'title', 'style'], + forbiddenAttributesByElement: { + a: ['style'] + }, + + /** + * Escape a string for safe injection into HTML + * + * @param {String} data + * @param {Array|null} allowedTags + * @returns {String} + */ + escapeHtml: function (data, allowedTags) { + var domParser = new DOMParser(), + fragment = domParser.parseFromString('<div></div>', 'text/html'); + + fragment = fragment.body.childNodes[0]; + allowedTags = typeof allowedTags === 'object' && allowedTags.length ? allowedTags : null; + + if (allowedTags) { + fragment.innerHTML = data || ''; + allowedTags = this._filterProhibitedTags(allowedTags); + + this._removeComments(fragment); + this._removeNotAllowedElements(fragment, allowedTags); + this._removeNotAllowedAttributes(fragment); + + return fragment.innerHTML; + } + + fragment.textContent = data || ''; + + return fragment.innerHTML; + }, + + /** + * Remove the always forbidden tags from a list of provided tags + * + * @param {Array} tags + * @returns {Array} + * @private + */ + _filterProhibitedTags: function (tags) { + return tags.filter(function (n) { + return this.neverAllowedElements.indexOf(n) === -1; + }.bind(this)); + }, + + /** + * Remove comment nodes from the given node + * + * @param {Node} node + * @private + */ + _removeComments: function (node) { + var treeWalker = node.ownerDocument.createTreeWalker( + node, + NodeFilter.SHOW_COMMENT, + function () { + return NodeFilter.FILTER_ACCEPT; + }, + false + ), + nodesToRemove = []; + + while (treeWalker.nextNode()) { + nodesToRemove.push(treeWalker.currentNode); + } + + nodesToRemove.forEach(function (nodeToRemove) { + nodeToRemove.parentNode.removeChild(nodeToRemove); + }); + }, + + /** + * Strip the given node of all disallowed tags while permitting any nested text nodes + * + * @param {Node} node + * @param {Array|null} allowedTags + * @private + */ + _removeNotAllowedElements: function (node, allowedTags) { + var treeWalker = node.ownerDocument.createTreeWalker( + node, + NodeFilter.SHOW_ELEMENT, + function (currentNode) { + return allowedTags.indexOf(currentNode.nodeName.toLowerCase()) === -1 ? + NodeFilter.FILTER_ACCEPT + // SKIP instead of REJECT because REJECT also rejects child nodes + : NodeFilter.FILTER_SKIP; + }, + false + ), + nodesToRemove = []; + + while (treeWalker.nextNode()) { + if (allowedTags.indexOf(treeWalker.currentNode.nodeName.toLowerCase()) === -1) { + nodesToRemove.push(treeWalker.currentNode); + } + } + + nodesToRemove.forEach(function (nodeToRemove) { + nodeToRemove.parentNode.replaceChild( + node.ownerDocument.createTextNode(nodeToRemove.textContent), + nodeToRemove + ); + }); + }, + + /** + * Remove any invalid attributes from the given node + * + * @param {Node} node + * @private + */ + _removeNotAllowedAttributes: function (node) { + var treeWalker = node.ownerDocument.createTreeWalker( + node, + NodeFilter.SHOW_ELEMENT, + function () { + return NodeFilter.FILTER_ACCEPT; + }, + false + ), + i, + attribute, + nodeName, + attributesToRemove = []; + + while (treeWalker.nextNode()) { + for (i = 0; i < treeWalker.currentNode.attributes.length; i++) { + attribute = treeWalker.currentNode.attributes[i]; + nodeName = treeWalker.currentNode.nodeName.toLowerCase(); + + if (this.generallyAllowedAttributes.indexOf(attribute.name) === -1 || // eslint-disable-line max-depth,max-len + this.forbiddenAttributesByElement[nodeName] && + this.forbiddenAttributesByElement[nodeName].indexOf(attribute.name) !== -1 + ) { + attributesToRemove.push(attribute); + } + } + } + + attributesToRemove.forEach(function (attributeToRemove) { + attributeToRemove.ownerElement.removeAttribute(attributeToRemove.name); + }); + } + }; +}); diff --git a/app/code/Magento/SendFriend/Block/Send.php b/app/code/Magento/SendFriend/Block/Send.php index 1c4b550361359..6f2154ba29f47 100644 --- a/app/code/Magento/SendFriend/Block/Send.php +++ b/app/code/Magento/SendFriend/Block/Send.php @@ -228,6 +228,7 @@ public function canSend() /** * @inheritdoc + * @since 100.3.1 */ protected function _prepareLayout() { diff --git a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml index eb9318271c1d8..b1e3da8612f78 100644 --- a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml +++ b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml @@ -7,7 +7,10 @@ /** * Send to friend form */ -/** @var \Magento\SendFriend\Block\Send $block */ +/** + * @var \Magento\SendFriend\Block\Send $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <script id="add-recipient-tmpl" type="text/x-magento-template"> @@ -21,15 +24,20 @@ </div> <fieldset class="fieldset"> <div class="field name required"> - <label for="recipients-name<%- data._index_ %>" class="label"><span><?= $block->escapeHtml(__('Name')) ?></span></label> + <label for="recipients-name<%- data._index_ %>" class="label"> + <span><?= $block->escapeHtml(__('Name')) ?></span> + </label> <div class="control"> - <input name="recipients[name][<%- data._index_ %>]" type="text" title="<?= $block->escapeHtmlAttr(__('Name')) ?>" class="input-text" + <input name="recipients[name][<%- data._index_ %>]" type="text" + title="<?= $block->escapeHtmlAttr(__('Name')) ?>" class="input-text" id="recipients-name<%- data._index_ %>" data-validate="{required:true}"/> </div> </div> <div class="field email required"> - <label for="recipients-email<%- data._index_ %>" class="label"><span><?= $block->escapeHtml(__('Email')) ?></span></label> + <label for="recipients-email<%- data._index_ %>" class="label"> + <span><?= $block->escapeHtml(__('Email')) ?></span> + </label> <div class="control"> <input name="recipients[email][<%- data._index_ %>]" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="recipients-email<%- data._index_ %>" type="email" class="input-text" @@ -71,7 +79,8 @@ <label for="sender-email" class="label"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> <input name="sender[email]" value="<?= $block->escapeHtmlAttr($block->getEmail()) ?>" - title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="sender-email" type="email" class="input-text" + title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="sender-email" type="email" + class="input-text" data-mage-init='{"mage/trim-input":{}}' data-validate="{required:true, 'validate-email':true}"/> </div> @@ -91,14 +100,16 @@ <legend class="legend"><span><?= $block->escapeHtml(__('Invitee')) ?></span></legend> <br /> <div id="recipients-options"></div> - <?php if ($block->getMaxRecipients()) : ?> - <div id="max-recipient-message" style="display: none;" class="message notice limit" role="alert"> - <span><?= $block->escapeHtml(__('Maximum %1 email addresses allowed.', $block->getMaxRecipients())) ?></span> + <?php if ($block->getMaxRecipients()): ?> + <div id="max-recipient-message" class="message notice limit" role="alert"> + <span><?= $block->escapeHtml(__('Maximum %1 email addresses allowed.', $block->getMaxRecipients())) ?> + </span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#max-recipient-message') ?> <?php endif; ?> <div class="actions-toolbar"> <div class="secondary"> - <?php if (1 < $block->getMaxRecipients()) : ?> + <?php if (1 < $block->getMaxRecipients()): ?> <button type="button" id="add-recipient-button" class="action add"> <span><?= $block->escapeHtml(__('Add Invitee')) ?></span></button> <?php endif; ?> @@ -110,7 +121,7 @@ <div class="actions-toolbar"> <div class="primary"> <button type="submit" - class="action submit primary"<?php if (!$block->canSend()) : ?> disabled="disabled"<?php endif ?>> + class="action submit primary"<?php if (!$block->canSend()): ?> disabled="disabled"<?php endif ?>> <span><?= $block->escapeHtml(__('Send Email')) ?></span></button> </div> <div class="secondary"> diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Create/Form.php b/app/code/Magento/Shipping/Block/Adminhtml/Create/Form.php index 17efc11856364..4869a685f3064 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Create/Form.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Create/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Shipping\Block\Adminhtml\Create; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; + /** * Adminhtml shipment create form * @@ -13,6 +16,24 @@ */ class Form extends \Magento\Sales\Block\Adminhtml\Order\AbstractOrder { + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Sales\Helper\Admin $adminHelper + * @param array $data + * @param TaxHelper|null $taxHelper + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Sales\Helper\Admin $adminHelper, + array $data = [], + ?TaxHelper $taxHelper = null + ) { + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); + parent::__construct($context, $registry, $adminHelper, $data); + } + /** * Retrieve invoice order * @@ -44,6 +65,8 @@ public function getShipment() } /** + * Prepare layout. + * * @return \Magento\Framework\View\Element\AbstractBlock */ protected function _prepareLayout() @@ -53,6 +76,8 @@ protected function _prepareLayout() } /** + * Return payment html. + * * @return string */ public function getPaymentHtml() @@ -61,6 +86,8 @@ public function getPaymentHtml() } /** + * Return items html. + * * @return string */ public function getItemsHtml() @@ -69,6 +96,8 @@ public function getItemsHtml() } /** + * Generate save url. + * * @return string */ public function getSaveUrl() diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php index e5e419328eea4..ce4521c9baa51 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Shipping\Block\Adminhtml\Order; +use Magento\Framework\App\ObjectManager; +use Magento\Shipping\Helper\Carrier; + /** * Adminhtml shipment packaging * @@ -44,6 +48,7 @@ class Packaging extends \Magento\Backend\Block\Template * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Shipping\Model\CarrierFactory $carrierFactory * @param array $data + * @param Carrier|null $carrierHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -51,12 +56,14 @@ public function __construct( \Magento\Shipping\Model\Carrier\Source\GenericInterface $sourceSizeModel, \Magento\Framework\Registry $coreRegistry, \Magento\Shipping\Model\CarrierFactory $carrierFactory, - array $data = [] + array $data = [], + ?Carrier $carrierHelper = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_coreRegistry = $coreRegistry; $this->_sourceSizeModel = $sourceSizeModel; $this->_carrierFactory = $carrierFactory; + $data['carrierHelper'] = $carrierHelper ?? ObjectManager::getInstance()->get(Carrier::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php index 55eecfa00d6da..5830160b60791 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php @@ -5,6 +5,9 @@ */ namespace Magento\Shipping\Block\Adminhtml\Order\Tracking; +use Magento\Framework\App\ObjectManager; +use Magento\Shipping\Helper\Data as ShippingHelper; + /** * Shipment tracking control form * @@ -24,14 +27,17 @@ class View extends \Magento\Shipping\Block\Adminhtml\Order\Tracking * @param \Magento\Framework\Registry $registry * @param \Magento\Shipping\Model\CarrierFactory $carrierFactory * @param array $data + * @param ShippingHelper|null $shippingHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Shipping\Model\Config $shippingConfig, \Magento\Framework\Registry $registry, \Magento\Shipping\Model\CarrierFactory $carrierFactory, - array $data = [] + array $data = [], + ?ShippingHelper $shippingHelper = null ) { + $data['shippingHelper'] = $shippingHelper ?? ObjectManager::getInstance()->get(ShippingHelper::class); parent::__construct($context, $shippingConfig, $registry, $data); $this->_carrierFactory = $carrierFactory; } diff --git a/app/code/Magento/Shipping/Block/Adminhtml/View/Form.php b/app/code/Magento/Shipping/Block/Adminhtml/View/Form.php index 409797780bcf6..8467a34ed0368 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/View/Form.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/View/Form.php @@ -11,6 +11,10 @@ */ namespace Magento\Shipping\Block\Adminhtml\View; +use Magento\Framework\App\ObjectManager; +use Magento\Shipping\Helper\Data as ShippingHelper; +use Magento\Tax\Helper\Data as TaxHelper; + /** * @api * @since 100.0.2 @@ -28,15 +32,21 @@ class Form extends \Magento\Sales\Block\Adminhtml\Order\AbstractOrder * @param \Magento\Sales\Helper\Admin $adminHelper * @param \Magento\Shipping\Model\CarrierFactory $carrierFactory * @param array $data + * @param ShippingHelper|null $shippingHelper + * @param TaxHelper|null $taxHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Registry $registry, \Magento\Sales\Helper\Admin $adminHelper, \Magento\Shipping\Model\CarrierFactory $carrierFactory, - array $data = [] + array $data = [], + ?ShippingHelper $shippingHelper = null, + ?TaxHelper $taxHelper = null ) { $this->_carrierFactory = $carrierFactory; + $data['shippingHelper'] = $shippingHelper ?? ObjectManager::getInstance()->get(ShippingHelper::class); + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); parent::__construct($context, $registry, $adminHelper, $data); } diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php index 76555ce8a6d8c..0965c4a472c25 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php @@ -332,6 +332,7 @@ public function checkAvailableShipCountries(\Magento\Framework\DataObject $reque * @param \Magento\Framework\DataObject $request * @return $this|bool|\Magento\Framework\DataObject * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @since 100.2.6 */ public function processAdditionalValidation(\Magento\Framework\DataObject $request) { @@ -343,7 +344,7 @@ public function processAdditionalValidation(\Magento\Framework\DataObject $reque * * @param \Magento\Framework\DataObject $request * @return $this|bool|\Magento\Framework\DataObject - * @deprecated + * @deprecated 100.2.6 * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index 27047ae46bf1f..c2238ff1a3809 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -303,7 +303,7 @@ public function getAllItems(RateRequest $request) * * @param \Magento\Framework\DataObject $request * @return $this|bool|\Magento\Framework\DataObject - * @deprecated + * @deprecated 100.2.6 * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -319,6 +319,7 @@ public function proccessAdditionalValidation(\Magento\Framework\DataObject $requ * @return $this|bool|\Magento\Framework\DataObject * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @since 100.2.6 */ public function processAdditionalValidation(\Magento\Framework\DataObject $request) { diff --git a/app/code/Magento/Shipping/Model/ShipmentProviderInterface.php b/app/code/Magento/Shipping/Model/ShipmentProviderInterface.php index 4ff9ba0008340..546afdca5028b 100644 --- a/app/code/Magento/Shipping/Model/ShipmentProviderInterface.php +++ b/app/code/Magento/Shipping/Model/ShipmentProviderInterface.php @@ -11,6 +11,7 @@ * Provide shipment items data. * * @api + * @since 100.3.0 */ interface ShipmentProviderInterface { @@ -18,6 +19,7 @@ interface ShipmentProviderInterface * Retrieve shipment items. * * @return array + * @since 100.3.0 */ public function getShipmentData(): array; } diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml index 0e69dba36d41c..fe2a1bf86a8ce 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml @@ -64,7 +64,9 @@ <argument name="file" value="usa_tablerates.csv"/> </actionGroup> <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!--Delete created data--> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml index 5e57224bfee48..b1fb2aad54272 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml @@ -28,7 +28,9 @@ <!-- Enable payment method one of "Check/Money Order" and shipping method one of "Free Shipping" --> <magentoCLI command="config:set {{enabledCheckMoneyOrder.label}} {{enabledCheckMoneyOrder.value}}" stepKey="enableCheckMoneyOrder"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> - <magentoCLI command="cache:clean config" stepKey="flushCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <!-- Delete data --> @@ -49,8 +51,7 @@ </actionGroup> <!-- Select Free shipping --> <actionGroup ref="OrderSelectFreeShippingActionGroup" stepKey="selectFreeShippingOption"/> - <!--Click *Submit Order* button--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="clickSubmitOrder" /> <!-- Create Partial Shipment --> <actionGroup ref="AdminCreateShipmentFromOrderPage" stepKey="createNewShipment"> <argument name="Qty" value="1"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml index 6b388ae31e45e..5d46ef0a76263 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml @@ -28,7 +28,9 @@ <!-- Enable payment method one of "Check/Money Order" and shipping method one of "Free Shipping" --> <magentoCLI command="config:set {{enabledCheckMoneyOrder.label}} {{enabledCheckMoneyOrder.value}}" stepKey="enableCheckMoneyOrder"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> - <magentoCLI command="cache:clean config" stepKey="flushCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <!-- Delete data --> @@ -49,8 +51,7 @@ </actionGroup> <!-- Select Free shipping --> <actionGroup ref="OrderSelectFreeShippingActionGroup" stepKey="selectFreeShippingOption"/> - <!--Click *Submit Order* button--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="clickSubmitOrder" /> <!-- Create Shipment --> <actionGroup ref="AdminCreateShipmentFromOrderPage" stepKey="createNewShipment"> <argument name="Title" value="Title"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml index 5fd9a6a29c0e3..d448f51a00406 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml @@ -67,8 +67,7 @@ <argument name="shippingMethodName" value="Best Way"/> </actionGroup> <!--Proceed to Review and Payments section--> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickToSaveShippingInfo"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickToSaveShippingInfo"/> <waitForPageLoad stepKey="waitForReviewAndPaymentsPageIsLoaded"/> <!--Place order and assert the message of success--> <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrderProductSuccessful"/> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml index d539a44f58a63..7de40943878cf 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml @@ -3,8 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -//phpcs:disable Magento2.Files.LineLength.MaxExceeded + +/** + * @var \Magento\Shipping\Block\Adminhtml\Create\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); ?> <form id="edit_form" method="post" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>"> <?= $block->getBlockHtml('formkey') ?> @@ -22,7 +28,9 @@ </div> <div class="admin__page-section-item-content"> <div><?= $block->getPaymentHtml() ?></div> - <div class="order-payment-currency"><?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?></div> + <div class="order-payment-currency"> + <?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?> + </div> </div> </div> <div class="admin__page-section-item order-shipping-address"> @@ -37,15 +45,15 @@ <div class="shipping-description-content"> <?= $block->escapeHtml(__('Total Shipping Charges')) ?>: - <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + <?php if ($taxHelper->displayShippingPriceIncludingTax()): ?> <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> - <?php else : ?> + <?php else: ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> <?= /** @noEscape */ $_excl ?> - <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingBothPrices() - && $_incl != $_excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() + && $_incl != $_excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /** @noEscape */ $_incl ?>) <?php endif; ?> </div> @@ -59,7 +67,8 @@ <?= $block->getItemsHtml() ?> </div> </form> -<script> +<?php $scriptString = <<<script + require([ "jquery", "mage/mage", @@ -68,5 +77,8 @@ require([ jQuery('#edit_form').mage('form').mage('validation'); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?= $block->getChildHtml('shipment_packaging'); diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml index ddb5dde5dfac7..9b55d2b969d3f 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml @@ -3,8 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace -//phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <section class="admin__page-section"> @@ -17,17 +17,17 @@ <tr class="headings"> <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> <th class="col-ordered-qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> - <th class="col-qty<?php if ($block->isShipmentRegular()) : ?> last<?php endif; ?>"> + <th class="col-qty<?php if ($block->isShipmentRegular()): ?> last<?php endif; ?>"> <span><?= $block->escapeHtml(__('Qty to Ship')) ?></span> </th> - <?php if (!$block->canShipPartiallyItem()) : ?> + <?php if (!$block->canShipPartiallyItem()): ?> <th class="col-ship last"><span><?= $block->escapeHtml(__('Ship')) ?></span></th> <?php endif; ?> </tr> </thead> <?php $_items = $block->getShipment()->getAllItems() ?> - <?php $_i = 0; foreach ($_items as $_item) : - if ($_item->getOrderItem()->getParentItem()) : + <?php $_i = 0; foreach ($_items as $_item): + if ($_item->getOrderItem()->getParentItem()): continue; endif; $_i++ ?> @@ -70,17 +70,21 @@ <span class="title"><?= $block->escapeHtml(__('Shipment Options')) ?></span> </div> <div class="admin__page-section-item-content"> - <?php if ($block->canCreateShippingLabel()) : ?> + <?php if ($block->canCreateShippingLabel()): ?> <div class="field choice admin__field admin__field-option field-create"> <input id="create_shipping_label" class="admin__control-checkbox" name="shipment[create_shipping_label]" value="1" - type="checkbox" - onclick="toggleCreateLabelCheckbox();"/> + type="checkbox"/> <label class="admin__field-label" for="create_shipping_label"> <span><?= $block->escapeHtml(__('Create Shipping Label')) ?></span></label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'toggleCreateLabelCheckbox();', + 'input#create_shipping_label' + ) ?> </div> <?php endif ?> @@ -95,7 +99,7 @@ <span><?=$block->escapeHtml(__('Append Comments')) ?></span></label> </div> - <?php if ($block->canSendShipmentEmail()) : ?> + <?php if ($block->canSendShipmentEmail()): ?> <div class="field choice admin__field admin__field-option field-email"> <input id="send_email" class="admin__control-checkbox" @@ -115,7 +119,8 @@ </div> </div> </section> -<script> +<?php $scriptString = <<<script + require([ "jquery", "Magento_Ui/js/modal/alert", @@ -150,7 +155,7 @@ window.toggleCreateLabelCheckbox = function() { window.submitShipment = function(btn) { if (!validQtyItems()) { alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('Invalid value(s) for Qty to Ship'))) ?>' + content: '{$block->escapeJs(__('Invalid value(s) for Qty to Ship'))}' }); return; } @@ -186,4 +191,7 @@ window.sendEmailCheckbox = sendEmailCheckbox; //]]> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml index 22d546f4fb474..7ddfc068fb115 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml @@ -3,39 +3,50 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace -//phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore -//phpcs:disable Squiz.Operators.IncrementDecrementUsage.NotAllowed //phpcs:disable Squiz.PHP.NonExecutableCode.Unreachable +/** + * @var \Magento\Shipping\Block\Adminhtml\Order\Packaging $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="grid"> <?php $randomId = rand(); ?> <div class="admin__table-wrapper"> - <table class="data-grid"> + <table id="packaging-data-grid-<?= /* @noEscape */ $randomId ?>" class="data-grid"> <thead> - <tr> - <th class="data-grid-checkbox-cell"> - <label class="data-grid-checkbox-cell-inner"> - <input type="checkbox" - id="select-items-<?= /* @noEscape */ $randomId ?>" - onchange="packaging.checkAllItems(this);" - class="checkbox admin__control-checkbox" - title="<?= $block->escapeHtmlAttr(__('Select All')) ?>"> - <label for="select-items-<?= /* @noEscape */ $randomId ?>"></label> - </label> - </th> - <th class="data-grid-th"><?= $block->escapeHtml(__('Product Name')) ?></th> - <th class="data-grid-th"><?= $block->escapeHtml(__('Weight')) ?></th> - <th class="data-grid-th" <?= $block->displayCustomsValue() ? '' : 'style="display: none;"' ?>> - <?= $block->escapeHtml(__('Customs Value')) ?> - </th> - <th class="data-grid-th"><?= $block->escapeHtml(__('Qty Ordered')) ?></th> - <th class="data-grid-th"><?= $block->escapeHtml(__('Qty')) ?></th> - </tr> + <tr> + <th class="data-grid-checkbox-cell"> + <label class="data-grid-checkbox-cell-inner"> + <input type="checkbox" + id="select-items-<?= /* @noEscape */ $randomId ?>" + class="checkbox admin__control-checkbox" + title="<?= $block->escapeHtmlAttr(__('Select All')) ?>"> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'packaging.checkAllItems(this);', + 'input#select-items-' . /* @noEscape */ $randomId + ) ?> + <label for="select-items-<?= /* @noEscape */ $randomId ?>"></label> + </label> + </th> + <th class="data-grid-th"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="data-grid-th"><?= $block->escapeHtml(__('Weight')) ?></th> + <th class="data-grid-th custom-value"> + <?= $block->escapeHtml(__('Customs Value')) ?> + </th> + <?php if (!$block->displayCustomsValue()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + '#packaging-data-grid-' . $randomId . ' th.custom-value' + ) ?> + <?php endif ?> + <th class="data-grid-th"><?= $block->escapeHtml(__('Qty Ordered')) ?></th> + <th class="data-grid-th"><?= $block->escapeHtml(__('Qty')) ?></th> + </tr> </thead> <tbody> - <?php $i=0; ?> - <?php foreach ($block->getCollection() as $item) : ?> + <?php $i = 0; ?> + <?php foreach ($block->getCollection() as $item): ?> <?php $_order = $block->getShipment()->getOrder(); $_orderItem = $_order->getItemById($item->getOrderItemId()); @@ -44,17 +55,17 @@ || ($_orderItem->isShipSeparately() && !($_orderItem->getParentItemId() || $_orderItem->getParentItem())) || (!$_orderItem->isShipSeparately() - && ($_orderItem->getParentItemId() || $_orderItem->getParentItem()))) : ?> + && ($_orderItem->getParentItemId() || $_orderItem->getParentItem()))): ?> <?php continue; ?> <?php endif; ?> <tr class="data-grid-controls-row data-row <?= ($i++ % 2 != 0) ? '_odd-row' : '' ?>"> <td class="data-grid-checkbox-cell"> - <?php $id = $item->getId() ? $item->getId() : $item->getOrderItemId(); ?> + <?php $id = $item->getId() ?? $item->getOrderItemId(); ?> <label class="data-grid-checkbox-cell-inner"> <input type="checkbox" name="" id="select-item-<?= /* @noEscape */ $randomId . '-' . $id ?>" - value="<?= (int) $id ?>" + value="<?= (int)$id ?>" class="checkbox admin__control-checkbox"> <label for="select-item-<?= /* @noEscape */ $randomId . '-' . $id ?>"></label> </label> @@ -67,45 +78,61 @@ </td> <?php if ($block->displayCustomsValue()) { - $customsValueDisplay = ''; $customsValueValidation = ' validate-zero-or-greater '; } else { - $customsValueDisplay = ' style="display: none;" '; $customsValueValidation = ''; } ?> - <td <?= /* @noEscape */ $customsValueDisplay ?>> + <td id="custom-value-<?= /* @noEscape */ $randomId . '-' . $id ?>" class="custom-value"> <input type="text" name="customs_value" class="input-text admin__control-text <?= /* @noEscape */ $customsValueValidation ?>" value="<?= $block->escapeHtmlAttr($block->formatPrice($item->getPrice())) ?>" - size="10" - onblur="packaging.recalcContainerWeightAndCustomsValue(this);"> + size="10"> </td> + <?php if (!$block->displayCustomsValue()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'td#custom-value-' . $randomId . '-' . $id + ) ?> + <?php endif ?> <td> - <?= /* @noEscape */ $item->getOrderItem()->getQtyOrdered()*1 ?> + <?= /* @noEscape */ $item->getOrderItem()->getQtyOrdered() * 1 ?> </td> <td> <input type="hidden" name="price" value="<?= $block->escapeHtml($item->getPrice()) ?>"> <input type="text" name="qty" - value="<?= /* @noEscape */ $item->getQty()*1 ?>" + value="<?= /* @noEscape */ $item->getQty() * 1 ?>" class="input-text admin__control-text qty - <?php if ($item->getOrderItem()->getIsQtyDecimal()) : ?> + <?php if ($item->getOrderItem()->getIsQtyDecimal()): ?> qty-decimal <?php endif ?>">  <button type="button" + id="packaging-delete-item-<?= /* @noEscape */ $randomId . '-' . $id ?>" class="action-delete" - data-action="package-delete-item" - onclick="packaging.deleteItem(this);" - style="display:none;"> + data-action="package-delete-item"> <span><?= $block->escapeHtml(__('Delete')) ?></span> </button> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'button#packaging-delete-item-' . $randomId . '-' . $id + ) ?> </td> </tr> <?php endforeach; ?> </tbody> </table> + <?php $scriptString = <<<script + require(['jquery'], function ($) { + $("#packaging-data-grid-{$randomId}").on('blur', 'td.custom-value input', + function(){packaging.recalcContainerWeightAndCustomsValue(this)}); + $("#packaging-data-grid-{$randomId}").on('click', 'button[data-action="package-delete-item"]', + function(){packaging.deleteItem(this)}); + }); +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml index 8d47f533449a7..90ecfa3862000 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Magento2.Files.LineLength.MaxExceeded -//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +/** @var \Magento\Shipping\Helper\Carrier $carrierHelper */ +$carrierHelper = $block->getData('carrierHelper'); ?> <div id="packed_window"> -<?php foreach ($block->getPackages() as $packageId => $package) : ?> +<?php foreach ($block->getPackages() as $packageId => $package): ?> <?php $package = new \Magento\Framework\DataObject($package) ?> <?php $params = new \Magento\Framework\DataObject($package->getParams()) ?> <section class="admin__page-section"> @@ -27,15 +30,18 @@ </td> </tr> <tr> - <?php if ($block->displayCustomsValue()) : ?> + <?php if ($block->displayCustomsValue()): ?> <th><?= $block->escapeHtml(__('Customs Value')) ?></th> - <td><?= $block->escapeHtml($block->displayCustomsPrice($params->getCustomsValue())) ?></td> - <?php else : ?> + <td><?= $block->escapeHtml($block->displayCustomsPrice($params->getCustomsValue())) ?> + </td> + <?php else: ?> <th><?= $block->escapeHtml(__('Total Weight')) ?></th> - <td><?= $block->escapeHtml($params->getWeight() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureWeightName($params->getWeightUnits())) ?></td> + <td><?= $block->escapeHtml($params->getWeight() . ' ' . + $carrierHelper->getMeasureWeightName($params->getWeightUnits())) ?> + </td> <?php endif; ?> </tr> - <?php if ($params->getSize()) : ?> + <?php if ($params->getSize()): ?> <tr> <th><?= $block->escapeHtml(__('Size')) ?></th> <td><?= $block->escapeHtml(ucfirst(strtolower($params->getSize()))) ?></td> @@ -50,9 +56,10 @@ <tr> <th><?= $block->escapeHtml(__('Length')) ?></th> <td> - <?php if ($params->getLength() != null) : ?> - <?= $block->escapeHtml($params->getLength() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getDimensionUnits())) ?> - <?php else : ?> + <?php if ($params->getLength() != null): ?> + <?= $block->escapeHtml($params->getLength() . ' ' . + $carrierHelper->getMeasureDimensionName($params->getDimensionUnits())) ?> + <?php else: ?> -- <?php endif; ?> </td> @@ -60,9 +67,10 @@ <tr> <th><?= $block->escapeHtml(__('Width')) ?></th> <td> - <?php if ($params->getWidth() != null) : ?> - <?= $block->escapeHtml($params->getWidth() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getDimensionUnits())) ?> - <?php else : ?> + <?php if ($params->getWidth() != null): ?> + <?= $block->escapeHtml($params->getWidth() . ' ' . + $carrierHelper->getMeasureDimensionName($params->getDimensionUnits())) ?> + <?php else: ?> -- <?php endif; ?> </td> @@ -70,9 +78,10 @@ <tr> <th><?= $block->escapeHtml(__('Height')) ?></th> <td> - <?php if ($params->getHeight() != null) : ?> - <?= $block->escapeHtml($params->getHeight() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getDimensionUnits())) ?> - <?php else : ?> + <?php if ($params->getHeight() != null): ?> + <?= $block->escapeHtml($params->getHeight() . ' ' . + $carrierHelper->getMeasureDimensionName($params->getDimensionUnits())) ?> + <?php else: ?> -- <?php endif; ?> </td> @@ -83,26 +92,33 @@ <div class="col-m-4"> <table class="admin__table-secondary"> <tbody> - <?php if ($params->getDeliveryConfirmation() != null) : ?> + <?php if ($params->getDeliveryConfirmation() != null): ?> <tr> <th><?= $block->escapeHtml(__('Signature Confirmation')) ?></th> - <td><?= $block->escapeHtml($block->getDeliveryConfirmationTypeByCode($params->getDeliveryConfirmation())) ?></td> + <td> + <?= $block->escapeHtml( + $block->getDeliveryConfirmationTypeByCode($params->getDeliveryConfirmation()) + ) ?></td> </tr> <?php endif; ?> - <?php if ($params->getContentType() != null) : ?> + <?php if ($params->getContentType() != null): ?> <tr> <th><?= $block->escapeHtml(__('Contents')) ?></th> - <?php if ($params->getContentType() == 'OTHER') : ?> + <?php if ($params->getContentType() == 'OTHER'): ?> <td><?= $block->escapeHtml($params->getContentTypeOther()) ?></td> - <?php else : ?> - <td><?= $block->escapeHtml($block->getContentTypeByCode($params->getContentType())) ?></td> + <?php else: ?> + <td> + <?= $block->escapeHtml($block->getContentTypeByCode($params->getContentType())) + ?></td> <?php endif; ?> </tr> <?php endif; ?> - <?php if ($params->getGirth()) : ?> + <?php if ($params->getGirth()): ?> <tr> <th><?= $block->escapeHtml(__('Girth')) ?></th> - <td><?= $block->escapeHtml($params->getGirth() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getGirthDimensionUnits())) ?></td> + <td><?= $block->escapeHtml($params->getGirth() . ' ' . + $carrierHelper->getMeasureDimensionName($params->getGirthDimensionUnits())) ?> + </td> </tr> <?php endif; ?> </tbody> @@ -119,7 +135,7 @@ <tr class="headings"> <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> <th class="col-weight"><span><?= $block->escapeHtml(__('Weight')) ?></span></th> - <?php if ($block->displayCustomsValue()) : ?> + <?php if ($block->displayCustomsValue()): ?> <th class="col-custom"><span><?= $block->escapeHtml(__('Customs Value')) ?></span></th> <?php endif; ?> <th class="col-qty"><span><?= $block->escapeHtml(__('Qty Ordered')) ?></span></th> @@ -127,7 +143,7 @@ </tr> </thead> <tbody id=""> - <?php foreach ($package->getItems() as $itemId => $item) : ?> + <?php foreach ($package->getItems() as $itemId => $item): ?> <?php $item = new \Magento\Framework\DataObject($item) ?> <tr title="#" id=""> <td class="col-product"> @@ -136,7 +152,7 @@ <td class="col-weight"> <?= $block->escapeHtml($item->getWeight()) ?> </td> - <?php if ($block->displayCustomsValue()) : ?> + <?php if ($block->displayCustomsValue()): ?> <td class="col-custom"> <?= $block->escapeHtml($block->displayCustomsPrice($item->getCustomsValue())) ?> </td> @@ -155,11 +171,15 @@ </section> <?php endforeach; ?> </div> -<script> +<?php $scriptString = <<<script + function showPackedWindow() { jQuery('#packed_window').modal('openModal'); } -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml index 28322d9534926..206deb0f5c795 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml @@ -5,14 +5,20 @@ */ //phpcs:disable PSR2.Methods.FunctionCallSignature.SpaceBeforeOpenBracket //phpcs:disable Magento2.Security.IncludeFile.FoundIncludeFile + +/** + * @var $block \Magento\Shipping\Block\Adminhtml\Order\Packaging + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php /** @var $block \Magento\Shipping\Block\Adminhtml\Order\Packaging */ ?> <?php $shippingMethod = $block->getShipment()->getOrder()->getShippingMethod(); $sizeSource = $block->getSourceSizeModel()->toOptionArray(); $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : 0; ?> -<script> + +<?php $scriptString = <<<script + require([ "jquery", "prototype", @@ -20,21 +26,21 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : "Magento_Ui/js/modal/modal" ], function(jQuery){ - window.packaging = new Packaging(<?= /* @noEscape */ $block->getConfigDataJson() ?>); + window.packaging = new Packaging({$block->getConfigDataJson()}); packaging.changeContainerType($$('select[name=package_container]')[0]); packaging.checkSizeAndGirthParameter( - $$('select[name=package_container]')[0], - <?= /* @noEscape */ $girthEnabled ?> + \$$('select[name=package_container]')[0], + {$girthEnabled} ); packaging.setConfirmPackagingCallback(function(){ packaging.setParamsCreateLabelRequest($('edit_form').serialize(true)); packaging.sendCreateLabelRequest(); }); packaging.setLabelCreatedCallback(function(response){ - setLocation("<?= $block->escapeJs($block->escapeUrl($block->getUrl( + setLocation("{$block->escapeJs($block->getUrl( 'sales/order/view', ['order_id' => $block->getShipment()->getOrderId()] - ))); ?>"); + ))}"); }); packaging.setCancelCallback(function() { if ($('create_shipping_label')) { @@ -51,23 +57,23 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : }); jQuery('#packaging_window').modal({ type: 'slide', - title: '<?= $block->escapeJs($block->escapeHtml(__('Create Packages'))) ?>', + title: '{$block->escapeJs(__('Create Packages'))}', buttons: [{ - text: '<?= $block->escapeJs($block->escapeHtml(__('Cancel'))) ?>', + text: '{$block->escapeJs(__('Cancel'))}', 'class': 'action-secondary', click: function () { packaging.cancelPackaging(); this.closeModal(); } }, { - text: '<?= $block->escapeJs($block->escapeHtml(__('Save'))) ?>', + text: '{$block->escapeJs(__('Save'))}', 'attr': {'disabled':'disabled', 'data-action':'save-packages'}, 'class': 'action-primary _disabled', click: function () { packaging.confirmPackaging(); } }, { - text: '<?= $block->escapeJs($block->escapeHtml(__('Add Package'))) ?>', + text: '{$block->escapeJs(__('Add Package'))}', 'attr': {'data-action':'add-packages'}, 'class': 'action-secondary', click: function () { @@ -78,5 +84,8 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : jQuery(document).trigger('packaging:inited'); jQuery(document).data('packagingInited', true); }); -</script> -<?php include ($block->getTemplateFile('Magento_Shipping::order/packaging/popup_content.phtml')) ?> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<?php include($block->getTemplateFile('Magento_Shipping::order/packaging/popup_content.phtml')) ?> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml index f91741f439d46..c3418049a38a0 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Magento2.Files.LineLength.MaxExceeded + +/** + * @var \Magento\Shipping\Block\Adminhtml\Order\Packaging $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php /** @var $block \Magento\Shipping\Block\Adminhtml\Order\Packaging */ ?> <div id="packaging_window"> - <div class="message message-warning" style="display: none"></div> - <section class="admin__page-section" id="package_template" style="display:none;"> + <div class="message message-warning"></div> + <section class="admin__page-section" id="package_template"> <div class="admin__page-section-title"> <span class="title"> <?= $block->escapeHtml(__('Package')) ?> <span data-role="package-number"></span> @@ -16,14 +19,12 @@ <div class="actions _primary"> <button type="button" class="action-secondary" - data-action="package-save-items" - onclick="packaging.packItems(this);"> + data-action="package-save-items"> <span><?= $block->escapeHtml(__('Add Selected Product(s) to Package')) ?></span> </button> <button type="button" class="action-secondary" - data-action="package-add-items" - onclick="packaging.getItemsForPack(this);"> + data-action="package-add-items"> <span><?= $block->escapeHtml(__('Add Products to Package')) ?></span> </button> </div> @@ -33,51 +34,54 @@ <thead> <tr> <th class="col-type"><?= $block->escapeHtml(__('Type')) ?></th> - <?php if ($girthEnabled == 1) : ?> + <?php if ($girthEnabled == 1): ?> <th class="col-size"><?= $block->escapeHtml(__('Size')) ?></th> <th class="col-girth"><?= $block->escapeHtml(__('Girth')) ?></th> <th> </th> <?php endif; ?> - <th class="col-custom" <?= $block->displayCustomsValue() ? '' : 'style="display: none;"' ?>> + <th class="col-custom"> <?= $block->escapeHtml(__('Customs Value')) ?> </th> + <?php if (!$block->displayCustomsValue()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none', 'th.col-custom') ?> + <?php endif ?> <th class="col-total-weight"><?= $block->escapeHtml(__('Total Weight')) ?></th> <th class="col-length"><?= $block->escapeHtml(__('Length')) ?></th> <th class="col-width"><?= $block->escapeHtml(__('Width')) ?></th> <th class="col-height"><?= $block->escapeHtml(__('Height')) ?></th> <th> </th> - <?php if ($block->getDeliveryConfirmationTypes()) : ?> + <?php if ($block->getDeliveryConfirmationTypes()): ?> <th class="col-signature"><?= $block->escapeHtml(__('Signature Confirmation')) ?></th> <?php endif; ?> <th class="col-actions"> </th> </tr> </thead> + <tbody> <tr> <td class="col-type"> <?php $containers = $block->getContainers(); ?> <select name="package_container" - onchange="packaging.changeContainerType(this);packaging.checkSizeAndGirthParameter(this, <?= $block->escapeJs($girthEnabled) ?>);" - <?php if (empty($containers)) : ?> - title="<?= $block->escapeHtmlAttr(__('USPS domestic shipments don\'t use package types.')) ?>" + <?php if (empty($containers)): ?> + title="<?= $block->escapeHtmlAttr(__( + 'USPS domestic shipments don\'t use package types.' + )) ?>" disabled="" class="admin__control-select disabled" - <?php else : ?> + <?php else: ?> class="admin__control-select" <?php endif; ?>> - <?php foreach ($containers as $key => $value) : ?> + <?php foreach ($containers as $key => $value): ?> <option value="<?= $block->escapeHtmlAttr($key) ?>" > <?= $block->escapeHtml($value) ?> </option> <?php endforeach; ?> </select> </td> - <?php if ($girthEnabled == 1 && !empty($sizeSource)) : ?> + <?php if ($girthEnabled == 1 && !empty($sizeSource)): ?> <td> - <select name="package_size" - class="admin__control-select" - onchange="packaging.checkSizeAndGirthParameter(this, <?= $block->escapeJs($girthEnabled) ?>);"> - <?php foreach ($sizeSource as $key => $value) : ?> + <select name="package_size" class="admin__control-select"> + <?php foreach ($sizeSource as $key => $value): ?> <option value="<?= $block->escapeHtmlAttr($sizeSource[$key]['value']) ?>"> <?= $block->escapeHtml($sizeSource[$key]['label']) ?> </option> @@ -91,26 +95,28 @@ </td> <td> <select name="container_girth_dimension_units" - class="options-units-dimensions measures admin__control-select" - onchange="packaging.changeMeasures(this);"> - <option value="<?= /* @noEscape */ Zend_Measure_Length::INCH ?>" selected="selected" ><?= $block->escapeHtml(__('in')) ?></option> - <option value="<?= /* @noEscape */ Zend_Measure_Length::CENTIMETER ?>" ><?= $block->escapeHtml(__('cm')) ?></option> + class="options-units-dimensions measures admin__control-select"> + <option value="<?= /* @noEscape */ Zend_Measure_Length::INCH ?>" selected="selected" > + <?= $block->escapeHtml(__('in')) ?> + </option> + <option value="<?= /* @noEscape */ Zend_Measure_Length::CENTIMETER ?>" > + <?= $block->escapeHtml(__('cm')) ?> + </option> </select> </td> <?php endif; ?> <?php if ($block->displayCustomsValue()) { - $customsValueDisplay = ''; $customsValueValidation = ' validate-zero-or-greater '; } else { - $customsValueDisplay = ' style="display: none;" '; $customsValueValidation = ''; } ?> - <td class="col-custom" <?= /* @noEscape */ $customsValueDisplay ?>> + <td class="col-custom"> <div class="admin__control-addon"> <input type="text" - class="customs-value input-text admin__control-text <?= /* @noEscape */ $customsValueValidation ?>" + class="customs-value input-text admin__control-text <?= + /* @noEscape */ $customsValueValidation ?>" name="package_customs_value" /> <span class="admin__addon-suffix"> <span class="customs-value-currency"> @@ -119,16 +125,23 @@ </span> </div> </td> + <?php if (!$block->displayCustomsValue()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none', 'td.col-custom') ?> + <?php endif ?> <td class="col-total-weight"> <div class="admin__control-addon"> <input type="text" - class="options-weight input-text admin__control-text required-entry validate-greater-than-zero" + class="options-weight input-text admin__control-text required-entry + validate-greater-than-zero" name="container_weight" /> <select name="container_weight_units" - class="options-units-weight measures admin__control-select" - onchange="packaging.changeMeasures(this);"> - <option value="<?= /* @noEscape */ Zend_Measure_Weight::POUND ?>" selected="selected" ><?= $block->escapeHtml(__('lb')) ?></option> - <option value="<?= /* @noEscape */ Zend_Measure_Weight::KILOGRAM ?>" ><?= $block->escapeHtml(__('kg')) ?></option> + class="options-units-weight measures admin__control-select"> + <option value="<?= /* @noEscape */ Zend_Measure_Weight::POUND + ?>" selected="selected" ><?= $block->escapeHtml(__('lb')) ?> + </option> + <option value="<?= /* @noEscape */ Zend_Measure_Weight::KILOGRAM ?>" > + <?= $block->escapeHtml(__('kg')) ?> + </option> </select> <span class="admin__addon-prefix"></span> </div> @@ -150,16 +163,19 @@ </td> <td class="col-measure"> <select name="container_dimension_units" - class="options-units-dimensions measures admin__control-select" - onchange="packaging.changeMeasures(this);"> - <option value="<?= /* @noEscape */ Zend_Measure_Length::INCH ?>" selected="selected" ><?= $block->escapeHtml(__('in')) ?></option> - <option value="<?= /* @noEscape */ Zend_Measure_Length::CENTIMETER ?>" ><?= $block->escapeHtml(__('cm')) ?></option> + class="options-units-dimensions measures admin__control-select"> + <option value="<?= /* @noEscape */ Zend_Measure_Length::INCH ?>" selected="selected" > + <?= $block->escapeHtml(__('in')) ?> + </option> + <option value="<?= /* @noEscape */ Zend_Measure_Length::CENTIMETER ?>" > + <?= $block->escapeHtml(__('cm')) ?> + </option> </select> </td> - <?php if ($block->getDeliveryConfirmationTypes()) : ?> + <?php if ($block->getDeliveryConfirmationTypes()): ?> <td> <select name="delivery_confirmation_types" class="admin__control-select"> - <?php foreach ($block->getDeliveryConfirmationTypes() as $key => $value) : ?> + <?php foreach ($block->getDeliveryConfirmationTypes() as $key => $value): ?> <option value="<?= $block->escapeHtmlAttr($key) ?>" > <?= $block->escapeHtml($value) ?> </option> @@ -169,15 +185,14 @@ <?php endif; ?> <td class="col-actions"> <button type="button" - class="action-delete DeletePackageBtn" - onclick="packaging.deletePackage(this);"> + class="action-delete DeletePackageBtn"> <span><?= $block->escapeHtml(__('Delete Package')) ?></span> </button> </td> </tr> </tbody> </table> - <?php if ($block->getContentTypes()) : ?> + <?php if ($block->getContentTypes()): ?> <table class="data-table admin__control-table" cellspacing="0"> <thead> <tr> @@ -189,9 +204,8 @@ <tr> <td> <select name="content_type" - class="admin__control-select" - onchange="packaging.changeContentTypes(this);"> - <?php foreach ($block->getContentTypes() as $key => $value) : ?> + class="admin__control-select"> + <?php foreach ($block->getContentTypes() as $key => $value): ?> <option value="<?= $block->escapeHtmlAttr($key) ?>" > <?= $block->escapeHtml($value) ?> </option> @@ -213,5 +227,47 @@ <div class="grid_prepare admin__page-subsection"></div> </div> </section> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '#package_template') ?> <div id="packages_content"></div> + <?php $scriptString = <<<script +require(['jquery'], function($){ + $("div#packages_content").on('click', "button[data-action='package-save-items']", + function(){packaging.packItems(this)}); + $("div#packages_content").on('click', "button[data-action='package-add-items']", + function(){packaging.getItemsForPack(this)}); + $("div#packages_content").on('change', "select[name='package_container']", + function(){ + packaging.changeContainerType(this); + packaging.checkSizeAndGirthParameter(this, {$block->escapeJs($girthEnabled)}) + }); + $("div#packages_content").on('change', "select[name='container_weight_units']", + function(){packaging.changeMeasures(this)}); + $("div#packages_content").on('change', "select[name='container_dimension_units']", + function(){packaging.changeMeasures(this)}); + $("div#packages_content").on('click', "button.action-delete.DeletePackageBtn", + function(){packaging.deletePackage(this)}); +script; + if ($girthEnabled == 1 && !empty($sizeSource)) { + $scriptString .= <<<script + $("div#packages_content").on('change', "select[name='package_size']", + function(){packaging.checkSizeAndGirthParameter(this, {$block->escapeJs($girthEnabled)})}); + $("div#packages_content").on('change', "select[name='container_girth_dimension_units']", + function(){packaging.changeMeasures(this)}); +script; + } + if ($block->getContentTypes()) { + $scriptString .= <<<script + $("div#packages_content").on('change', "select[name='content_type']", + function(){packaging.changeContentTypes(this)}); +script; + } + $scriptString .= <<<script +}) +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#packaging_window div.message.message-warning' +) ?> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml index d65fa819eaeed..1dcc7439532b6 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml @@ -4,8 +4,14 @@ * See COPYING.txt for license details. */ ?> -<?php /** @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking */?> -<script> +<?php +/** + * @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ +?> +<?php $scriptString = <<<script + require(['prototype'], function(){ //<![CDATA[ @@ -57,7 +63,11 @@ require(['prototype'], function(){ //]]> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <script id="track_row_template" type="text/x-magento-template"> <tr> <td class="col-carrier"> @@ -65,7 +75,7 @@ require(['prototype'], function(){ id="trackingC<%- data.index %>" class="select admin__control-select carrier" disabled="disabled"> - <?php foreach ($block->getCarriers() as $_code => $_name) : ?> + <?php foreach ($block->getCarriers() as $_code => $_name): ?> <option value="<?= $block->escapeHtmlAttr($_code) ?>"><?= $block->escapeHtml($_name) ?></option> <?php endforeach; ?> </select> @@ -116,7 +126,8 @@ require(['prototype'], function(){ </tbody> </table> </div> -<script> +<?php $scriptString = <<<script + require([ 'mage/template', 'prototype' @@ -127,4 +138,7 @@ require([ //]]> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml index a013abfd65f87..df303b777188c 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml @@ -3,11 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace -//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -//phpcs:disable Magento2.Files.LineLength.MaxExceeded + +/** + * @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking\View + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Shipping\Helper\Data $shippingHelper */ +$shippingHelper = $block->getData('shippingHelper'); ?> -<?php /** @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking\View */ ?> <div class="admin__control-table-wrapper"> <form id="tracking-shipping-form" data-mage-init='{"validation": {}}'> <table class="data-table admin__control-table" id="shipment_tracking_info"> @@ -22,13 +26,17 @@ <tfoot> <tr> <td class="col-carrier"> - <select name="carrier" - class="select admin__control-select" - onchange="selectCarrier(this)"> - <?php foreach ($block->getCarriers() as $_code => $_name) : ?> - <option value="<?= $block->escapeHtmlAttr($_code) ?>"><?= $block->escapeHtml($_name) ?></option> + <select name="carrier" class="select admin__control-select"> + <?php foreach ($block->getCarriers() as $_code => $_name): ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>"> + <?= $block->escapeHtml($_name) ?></option> <?php endforeach; ?> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'selectCarrier(this)', + "select[name='carrier']" + ) ?> </td> <td class="col-title"> <input class="input-text admin__control-text" @@ -47,23 +55,42 @@ <td class="col-delete last"><?= $block->getSaveButtonHtml() ?></td> </tr> </tfoot> - <?php if ($_tracks = $block->getShipment()->getAllTracks()) : ?> + <?php if ($_tracks = $block->getShipment()->getAllTracks()): ?> <tbody> - <?php $i = 0; foreach ($_tracks as $_track) :$i++ ?> + <?php $i = 0; foreach ($_tracks as $_track): $i++ ?> <tr class="<?= /* @noEscape */ ($i%2 == 0) ? 'even' : 'odd' ?>"> <td class="col-carrier"> <?= $block->escapeHtml($block->getCarrierTitle($_track->getCarrierCode())) ?> </td> <td class="col-title"><?= $block->escapeHtml($_track->getTitle()) ?></td> <td class="col-number"> - <?php if ($_track->isCustom()) : ?> + <?php if ($_track->isCustom()): ?> <?= $block->escapeHtml($_track->getNumber()) ?> - <?php else : ?> - <a href="#" onclick="popWin('<?= $block->escapeJs($block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($_track))) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')"><?= $block->escapeHtml($_track->getNumber()) ?></a> + <?php else: ?> + <a id="col-track-<?= (int) $_track->getId() ?>" href="#"> + <?= $block->escapeHtml($_track->getNumber()) ?> + </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "event.preventDefault(); + popWin('{$block->escapeJs($shippingHelper->getTrackingPopupUrlBySalesModel($_track))}', + 'trackorder','width=800,height=600,resizable=yes,scrollbars=yes')", + 'a#col-track-' . (int) $_track->getId() + ) ?> <div id="shipment_tracking_info_response_<?= (int) $_track->getId() ?>"></div> <?php endif; ?> </td> - <td class="col-delete last"><button class="action-delete" type="button" onclick="deleteTrackingNumber('<?= $block->escapeJs($block->escapeUrl($block->getRemoveUrl($_track))) ?>'); return false;"><span><?= $block->escapeHtml(__('Delete')) ?></span></button></td> + <td class="col-delete last"> + <button class="action-delete" type="button" id="del-track-<?= (int) $_track->getId() ?>"> + <span><?= $block->escapeHtml(__('Delete')) ?></span> + </button> + </td> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "deleteTrackingNumber('{$block->escapeJs($block->getRemoveUrl($_track))}'); + event.preventDefault();", + '#del-track-' . (int) $_track->getId() + ) ?> </tr> <?php endforeach; ?> </tbody> @@ -71,9 +98,9 @@ </table> </form> </div> +<?php $scriptString = <<<script -<script> -require(['prototype', 'jquery', 'Magento_Ui/js/modal/confirm'], function(prototype, $j, confirm) { +require(['prototype', 'jquery', 'Magento_Ui/js/modal/confirm'], function(prototype, \$j, confirm) { //<![CDATA[ function selectCarrier(elem) { var option = elem.options[elem.selectedIndex]; @@ -81,7 +108,7 @@ function selectCarrier(elem) { } function saveTrackingInfo(node, url) { - var form = $j('#tracking-shipping-form'); + var form = \$j('#tracking-shipping-form'); if (form.validation() && form.validation('isValid')) { submitAndReloadArea(node, url); @@ -90,7 +117,7 @@ function saveTrackingInfo(node, url) { function deleteTrackingNumber(url) { confirm({ - content: '<?= $block->escapeJs($block->escapeHtml(__('Are you sure?'))) ?>', + content: '{$block->escapeJs(__('Are you sure?'))}', actions: { /** * Confirm action. @@ -108,4 +135,7 @@ window.saveTrackingInfo = saveTrackingInfo; //]]> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml index 720b34983551d..002a960f3b38a 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml @@ -3,11 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** + * @var $block \Magento\Sales\Block\Adminhtml\Order\AbstractOrder + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Shipping\Helper\Data $shippingHelper */ +$shippingHelper = $block->getData('shippingHelper'); +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); ?> -<?php /** @var $block \Magento\Shipping\Block\Adminhtml\View */ ?> <?php $order = $block->getOrder() ?> -<?php if ($order->getIsVirtual()) : +<?php if ($order->getIsVirtual()): return ''; endif; ?> @@ -17,25 +25,34 @@ endif; ?> <span class="title"><?= $block->escapeHtml(__('Shipping & Handling Information')) ?></span> </div> <div class="admin__page-section-item-content"> - <?php if ($order->getTracksCollection()->count()) : ?> - <p><a href="#" id="linkId" onclick="popWin('<?= $block->escapeJs($block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($order))) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')" title="<?= $block->escapeHtmlAttr(__('Track Order')) ?>"><?= $block->escapeHtml(__('Track Order')) ?></a></p> + <?php if ($order->getTracksCollection()->count()): ?> + <p> + <a href="#" id="linkId" title="<?= $block->escapeHtmlAttr(__('Track Order')) ?>"> + <?= $block->escapeHtml(__('Track Order')) ?> + </a> + </p> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "popWin('" . $block->escapeJs($shippingHelper->getTrackingPopupUrlBySalesModel($order)) . + "','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')", + 'a#linkId' + ) ?> <?php endif; ?> - <?php if ($order->getShippingDescription()) : ?> + <?php if ($order->getShippingDescription()): ?> <strong><?= $block->escapeHtml($order->getShippingDescription()) ?></strong> - <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + <?php if ($taxHelper->displayShippingPriceIncludingTax()): ?> <?php $_excl = $block->displayShippingPriceInclTax($order); ?> - <?php else : ?> + <?php else: ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($order); ?> <?= /** @noEscape */ $_excl ?> - <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingBothPrices() - && $_incl != $_excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() && $_incl != $_excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /** @noEscape */ $_incl ?>) <?php endif; ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml(__('No shipping information available')) ?> <?php endif; ?> </div> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml index 44fe4b9ccd353..d023f614f55aa 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml @@ -5,9 +5,14 @@ */ /** * @var \Magento\Shipping\Block\Adminhtml\View\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ -//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -//phpcs:disable Magento2.Files.LineLength.MaxExceeded + +/** @var \Magento\Shipping\Helper\Data $shippingHelper */ +$shippingHelper = $block->getData('shippingHelper'); +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); +/** @var \Magento\Sales\Model\Order $order */ $order = $block->getShipment()->getOrder(); ?> <?= $block->getChildHtml('order_info'); ?> @@ -34,12 +39,19 @@ $order = $block->getShipment()->getOrder(); </div> <div class="admin__page-section-item-content"> <div class="shipping-description-wrapper"> - <?php if ($block->getShipment()->getTracksCollection()->count()) : ?> + <?php if ($block->getShipment()->getTracksCollection()->count()): ?> <p> - <a href="#" id="linkId" onclick="popWin('<?= $block->escapeUrl($this->helper(\Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($block->getShipment())); ?>','trackshipment','width=800,height=600,resizable=yes,scrollbars=yes')" - title="<?= $block->escapeHtml(__('Track this shipment')); ?>"> + <a href="#" id="linkId" title="<?= $block->escapeHtml(__('Track this shipment')); ?>"> <?= $block->escapeHtml(__('Track this shipment')); ?> </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault();' . + "popWin('{$block->escapeJs($shippingHelper->getTrackingPopupUrlBySalesModel( + $block->getShipment() + ))}','trackshipment','width=800,height=600,resizable=yes,scrollbars=yes')", + 'a#linkId' + ) ?> </p> <?php endif; ?> <div class="shipping-description-title"> @@ -48,34 +60,35 @@ $order = $block->getShipment()->getOrder(); <?= $block->escapeHtml(__('Total Shipping Charges')); ?>: - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + <?php if ($taxHelper->displayShippingPriceIncludingTax()): ?> <?php $excl = $block->displayShippingPriceInclTax($order); ?> - <?php else : ?> + <?php else: ?> <?php $excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $incl = $block->displayShippingPriceInclTax($order); ?> <?= /* @noEscape */ $excl; ?> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $incl != $excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() && $incl != $excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')); ?> <?= /* @noEscape */ $incl; ?>) <?php endif; ?> </div> <p> - <?php if ($block->canCreateShippingLabel()) : ?> + <?php if ($block->canCreateShippingLabel()): ?> <?= /* @noEscape */ $block->getCreateLabelButton(); ?> <?php endif ?> - <?php if ($block->getShipment()->getShippingLabel()) : ?> + <?php if ($block->getShipment()->getShippingLabel()): ?> <?= /* @noEscape */ $block->getPrintLabelButton(); ?> <?php endif ?> - <?php if ($block->getShipment()->getPackages()) : ?> + <?php if ($block->getShipment()->getPackages()): ?> <?= /* @noEscape */ $block->getShowPackagesButton(); ?> <?php endif ?> </p> <?= $block->getChildHtml('shipment_tracking'); ?> <?= $block->getChildHtml('shipment_packaging'); ?> - <script> + <?php $scriptString = <<<script + require([ 'jquery', 'prototype' @@ -85,7 +98,10 @@ $order = $block->getShipment()->getOrder(); window.packaging.sendCreateLabelRequest(); }); window.packaging.setLabelCreatedCallback(function () { - setLocation("<?= $block->escapeUrl($block->getUrl('adminhtml/order_shipment/view', ['shipment_id' => $block->getShipment()->getId()])); ?>"); + setLocation("{$block->escapeJs($block->getUrl( + 'adminhtml/order_shipment/view', + ['shipment_id' => $block->getShipment()->getId()] + ))}"); }); }; @@ -95,7 +111,10 @@ $order = $block->getShipment()->getOrder(); jQuery(document).on('packaging:inited', setCallbacks); } }); - </script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> </div> diff --git a/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml b/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml index 1e8760b3afd6d..925dbd03db8e0 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml @@ -6,19 +6,23 @@ use Magento\Framework\View\Element\Template; -/** @var $block \Magento\Shipping\Block\Tracking\Popup */ -//phpcs:disable Magento2.Files.LineLength.MaxExceeded +/** + * @var $block \Magento\Shipping\Block\Tracking\Popup + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $results = $block->getTrackingInfo(); ?> <div class="page tracking"> - <?php if (!empty($results)) : ?> - <?php foreach ($results as $shipId => $result) : ?> - <?php if ($shipId) : ?> - <div class="order subtitle caption"><?= /* @noEscape */ $block->escapeHtml(__('Shipment #')) . $shipId ?></div> + <?php if (!empty($results)): ?> + <?php foreach ($results as $shipId => $result): ?> + <?php if ($shipId): ?> + <div class="order subtitle caption"> + <?= /* @noEscape */ $block->escapeHtml(__('Shipment #')) . $shipId ?> + </div> <?php endif; ?> - <?php if (!empty($result)) : ?> - <?php foreach ($result as $counter => $track) : ?> + <?php if (!empty($result)): ?> + <?php foreach ($result as $counter => $track): ?> <div class="table-wrapper"> <?php $shipmentBlockIdentifier = $shipId . '.' . $counter; @@ -28,25 +32,28 @@ $results = $block->getTrackingInfo(); 'storeSupportEmail' => $block->getStoreSupportEmail() ]); ?> - <?= /* @noEscape */ $block->getChildHtml('shipping.tracking.details.' . $shipmentBlockIdentifier) ?> + <?= /* @noEscape */ $block->getChildHtml('shipping.tracking.details.' . + $shipmentBlockIdentifier) ?> </div> - <?php if (is_object($track) && !empty($track->getProgressdetail())) : ?> + <?php if (is_object($track) && !empty($track->getProgressdetail())): ?> <?php - $block->addChild('shipping.tracking.progress.' . $shipmentBlockIdentifier, Template::class, [ - 'track' => $track, - 'template' => 'Magento_Shipping::tracking/progress.phtml' - ]); + $block->addChild( + 'shipping.tracking.progress.' . $shipmentBlockIdentifier, + Template::class, + ['track' => $track, 'template' => 'Magento_Shipping::tracking/progress.phtml'] + ); ?> - <?= /* @noEscape */ $block->getChildHtml('shipping.tracking.progress.' . $shipmentBlockIdentifier) ?> + <?= /* @noEscape */ $block->getChildHtml('shipping.tracking.progress.' . + $shipmentBlockIdentifier) ?> <?php endif; ?> <?php endforeach; ?> - <?php else : ?> + <?php else: ?> <div class="message info empty"> <div><?= $block->escapeHtml(__('There is no tracking available for this shipment.')) ?></div> </div> <?php endif; ?> <?php endforeach; ?> - <?php else : ?> + <?php else: ?> <div class="message info empty"> <div><?= $block->escapeHtml(__('There is no tracking available.')) ?></div> </div> @@ -54,13 +61,18 @@ $results = $block->getTrackingInfo(); <div class="actions"> <button type="button" title="<?= $block->escapeHtml(__('Close Window')) ?>" - class="action close" - onclick="window.close(); window.opener.focus();"> + class="action close"> <span><?= $block->escapeHtml(__('Close Window')) ?></span> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.close(); window.opener.focus();", + 'button.action.close' + ) ?> </div> </div> -<script> +<?php $scriptString = <<<script + require([ 'jquery' ], function (jQuery) { @@ -69,4 +81,7 @@ $results = $block->getTrackingInfo(); jQuery('.actions button.close').hide(); } }); -</script> \ No newline at end of file + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sitemap/Block/Robots.php b/app/code/Magento/Sitemap/Block/Robots.php index ac99b2ab1cd4a..a074e95ce2f80 100644 --- a/app/code/Magento/Sitemap/Block/Robots.php +++ b/app/code/Magento/Sitemap/Block/Robots.php @@ -11,6 +11,8 @@ use Magento\Robots\Model\Config\Value; use Magento\Sitemap\Helper\Data as SitemapHelper; use Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory; +use Magento\Sitemap\Model\SitemapConfigReader; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreResolver; @@ -18,7 +20,7 @@ * Prepares sitemap links to add to the robots.txt file * * @api - * @since 100.2.0 + * @since 100.1.5 */ class Robots extends AbstractBlock implements IdentityInterface { @@ -29,6 +31,7 @@ class Robots extends AbstractBlock implements IdentityInterface /** * @var SitemapHelper + * @deprecated */ private $sitemapHelper; @@ -37,6 +40,11 @@ class Robots extends AbstractBlock implements IdentityInterface */ private $storeManager; + /** + * @var SitemapConfigReader + */ + private $sitemapConfigReader; + /** * @param Context $context * @param StoreResolver $storeResolver @@ -44,7 +52,7 @@ class Robots extends AbstractBlock implements IdentityInterface * @param SitemapHelper $sitemapHelper * @param StoreManagerInterface $storeManager * @param array $data - * + * @param SitemapConfigReader|null $sitemapConfigReader * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -53,11 +61,14 @@ public function __construct( CollectionFactory $sitemapCollectionFactory, SitemapHelper $sitemapHelper, StoreManagerInterface $storeManager, - array $data = [] + array $data = [], + ?SitemapConfigReader $sitemapConfigReader = null ) { $this->sitemapCollectionFactory = $sitemapCollectionFactory; $this->sitemapHelper = $sitemapHelper; $this->storeManager = $storeManager; + $this->sitemapConfigReader = $sitemapConfigReader + ?: ObjectManager::getInstance()->get(SitemapConfigReader::class); parent::__construct($context, $data); } @@ -70,26 +81,20 @@ public function __construct( * and adds links for this sitemap files into result data. * * @return string - * @since 100.2.0 + * @since 100.1.5 */ protected function _toHtml() { - $defaultStore = $this->storeManager->getDefaultStoreView(); - - /** @var \Magento\Store\Model\Website $website */ - $website = $this->storeManager->getWebsite($defaultStore->getWebsiteId()); + $website = $this->storeManager->getWebsite(); $storeIds = []; foreach ($website->getStoreIds() as $storeId) { - if ((bool)$this->sitemapHelper->getEnableSubmissionRobots($storeId)) { - $storeIds[] = (int)$storeId; + if ((bool) $this->sitemapConfigReader->getEnableSubmissionRobots($storeId)) { + $storeIds[] = (int) $storeId; } } - $links = []; - if ($storeIds) { - $links = array_merge($links, $this->getSitemapLinks($storeIds)); - } + $links = $storeIds ? $this->getSitemapLinks($storeIds) : []; return $links ? implode(PHP_EOL, $links) . PHP_EOL : ''; } @@ -102,22 +107,16 @@ protected function _toHtml() * * @param int[] $storeIds * @return array - * @since 100.2.0 + * @since 100.1.5 */ protected function getSitemapLinks(array $storeIds) { - $sitemapLinks = []; - - /** @var \Magento\Sitemap\Model\ResourceModel\Sitemap\Collection $collection */ $collection = $this->sitemapCollectionFactory->create(); $collection->addStoreFilter($storeIds); + $sitemapLinks = []; foreach ($collection as $sitemap) { - /** @var \Magento\Sitemap\Model\Sitemap $sitemap */ - $sitemapFilename = $sitemap->getSitemapFilename(); - $sitemapPath = $sitemap->getSitemapPath(); - - $sitemapUrl = $sitemap->getSitemapUrl($sitemapPath, $sitemapFilename); + $sitemapUrl = $sitemap->getSitemapUrl($sitemap->getSitemapPath(), $sitemap->getSitemapFilename()); $sitemapLinks[$sitemapUrl] = 'Sitemap: ' . $sitemapUrl; } @@ -128,7 +127,7 @@ protected function getSitemapLinks(array $storeIds) * Get unique page cache identities * * @return array - * @since 100.2.0 + * @since 100.1.5 */ public function getIdentities() { diff --git a/app/code/Magento/Sitemap/Helper/Data.php b/app/code/Magento/Sitemap/Helper/Data.php index 44661bbef888e..118aeff28a14f 100644 --- a/app/code/Magento/Sitemap/Helper/Data.php +++ b/app/code/Magento/Sitemap/Helper/Data.php @@ -12,7 +12,7 @@ use Magento\Store\Model\ScopeInterface; /** - * @deprecated + * @deprecated 100.3.0 */ class Data extends \Magento\Framework\App\Helper\AbstractHelper { @@ -70,7 +70,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper * * @param int $storeId * @return int - * @deprecated + * @deprecated 100.3.0 * @see SitemapConfigReader::getMaximumLinesNumber() */ public function getMaximumLinesNumber($storeId) @@ -87,7 +87,7 @@ public function getMaximumLinesNumber($storeId) * * @param int $storeId * @return int - * @deprecated + * @deprecated 100.3.0 * @see SitemapConfigReader::getMaximumFileSize() */ public function getMaximumFileSize($storeId) @@ -104,7 +104,7 @@ public function getMaximumFileSize($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see CategoryConfigReader::getChangeFrequency() */ public function getCategoryChangefreq($storeId) @@ -121,7 +121,7 @@ public function getCategoryChangefreq($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see ProductConfigReader::getChangeFrequency() */ public function getProductChangefreq($storeId) @@ -138,7 +138,7 @@ public function getProductChangefreq($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see CmsPageConfigReader::getChangeFrequency() */ public function getPageChangefreq($storeId) @@ -155,7 +155,7 @@ public function getPageChangefreq($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see CategoryConfigReader::getPriority() */ public function getCategoryPriority($storeId) @@ -172,7 +172,7 @@ public function getCategoryPriority($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see ProductConfigReader::getPriority() */ public function getProductPriority($storeId) @@ -189,7 +189,7 @@ public function getProductPriority($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see CmsPageConfigReader::getPriority() */ public function getPagePriority($storeId) @@ -206,7 +206,7 @@ public function getPagePriority($storeId) * * @param int $storeId * @return int - * @deprecated + * @deprecated 100.3.0 * @see SitemapConfigReader::getEnableSubmissionRobots() */ public function getEnableSubmissionRobots($storeId) @@ -223,7 +223,7 @@ public function getEnableSubmissionRobots($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see SitemapConfigReader::getProductImageIncludePolicy() */ public function getProductImageIncludePolicy($storeId) @@ -239,7 +239,7 @@ public function getProductImageIncludePolicy($storeId) * Get list valid paths for generate a sitemap XML file * * @return string[] - * @deprecated + * @deprecated 100.3.0 * @see SitemapConfigReader::getValidPaths() */ public function getValidPaths() diff --git a/app/code/Magento/Sitemap/Model/ItemProvider/ConfigReaderInterface.php b/app/code/Magento/Sitemap/Model/ItemProvider/ConfigReaderInterface.php index 1e8b545728a04..6c8ff087aeb60 100644 --- a/app/code/Magento/Sitemap/Model/ItemProvider/ConfigReaderInterface.php +++ b/app/code/Magento/Sitemap/Model/ItemProvider/ConfigReaderInterface.php @@ -10,6 +10,7 @@ * Item resolver config reader interface * * @api + * @since 100.3.0 */ interface ConfigReaderInterface { @@ -18,6 +19,7 @@ interface ConfigReaderInterface * * @param int $storeId * @return string + * @since 100.3.0 */ public function getPriority($storeId); @@ -26,6 +28,7 @@ public function getPriority($storeId); * * @param int $storeId * @return string + * @since 100.3.0 */ public function getChangeFrequency($storeId); } diff --git a/app/code/Magento/Sitemap/Model/ItemProvider/ItemProviderInterface.php b/app/code/Magento/Sitemap/Model/ItemProvider/ItemProviderInterface.php index 89ad2afdd01a2..da56f86b7237c 100644 --- a/app/code/Magento/Sitemap/Model/ItemProvider/ItemProviderInterface.php +++ b/app/code/Magento/Sitemap/Model/ItemProvider/ItemProviderInterface.php @@ -11,6 +11,7 @@ * Sitemap item provider interface * * @api + * @since 100.3.0 */ interface ItemProviderInterface { @@ -19,6 +20,7 @@ interface ItemProviderInterface * * @param int $storeId * @return SitemapItemInterface[] + * @since 100.3.0 */ public function getItems($storeId); } diff --git a/app/code/Magento/Sitemap/Model/Observer.php b/app/code/Magento/Sitemap/Model/Observer.php index ce74d738c4bc3..4333c71c7497f 100644 --- a/app/code/Magento/Sitemap/Model/Observer.php +++ b/app/code/Magento/Sitemap/Model/Observer.php @@ -3,11 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sitemap\Model; -use Magento\Sitemap\Model\EmailNotification as SitemapEmail; +use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Sitemap\Model\EmailNotification as SitemapEmail; use Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory; +use Magento\Store\Model\App\Emulation; use Magento\Store\Model\ScopeInterface; /** @@ -61,20 +65,28 @@ class Observer */ private $emailNotification; + /** + * @var Emulation + */ + private $appEmulation; + /** * Observer constructor. * @param ScopeConfigInterface $scopeConfig * @param CollectionFactory $collectionFactory * @param EmailNotification $emailNotification + * @param Emulation $appEmulation */ public function __construct( ScopeConfigInterface $scopeConfig, CollectionFactory $collectionFactory, - SitemapEmail $emailNotification + SitemapEmail $emailNotification, + Emulation $appEmulation ) { $this->scopeConfig = $scopeConfig; $this->collectionFactory = $collectionFactory; $this->emailNotification = $emailNotification; + $this->appEmulation = $appEmulation; } /** @@ -105,9 +117,16 @@ public function scheduledGenerateSitemaps() foreach ($collection as $sitemap) { /* @var $sitemap \Magento\Sitemap\Model\Sitemap */ try { + $this->appEmulation->startEnvironmentEmulation( + $sitemap->getStoreId(), + Area::AREA_FRONTEND, + true + ); $sitemap->generateXml(); } catch (\Exception $e) { $errors[] = $e->getMessage(); + } finally { + $this->appEmulation->stopEnvironmentEmulation(); } } if ($errors && $recipient) { diff --git a/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php b/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php index 8b2154e6ee47a..dc15819b087b2 100644 --- a/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php +++ b/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php @@ -474,6 +474,7 @@ protected function _getMediaConfig() * * @param \Magento\Framework\DB\Select $select * @return \Magento\Framework\DB\Select + * @since 100.2.1 */ public function prepareSelectStatement(\Magento\Framework\DB\Select $select) { diff --git a/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php b/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php index 01addd0c19666..92cbcbd500e8a 100644 --- a/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php +++ b/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php @@ -38,7 +38,6 @@ class Page extends AbstractDb /** * @var GetUtilityPageIdentifiersInterface - * @since 100.2.0 */ private $getUtilityPageIdentifiers; diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index 21839e1057125..9a8d2c57a280c 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -160,7 +160,7 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento /** * @inheritdoc * - * @since 100.2.0 + * @since 100.1.5 */ protected $_cacheTag = [Value::CACHE_TAG]; @@ -297,8 +297,9 @@ protected function _getStream() * * @param DataObject $sitemapItem * @return $this - * @deprecated 100.2.0 + * @deprecated 100.3.0 * @see ItemProviderInterface + * @since 100.2.0 */ public function addSitemapItem(DataObject $sitemapItem) { @@ -311,8 +312,9 @@ public function addSitemapItem(DataObject $sitemapItem) * Collect all sitemap items * * @return void - * @deprecated 100.2.0 + * @deprecated 100.3.0 * @see ItemProviderInterface + * @since 100.2.0 */ public function collectSitemapItems() { @@ -723,7 +725,7 @@ protected function _getUrl($url, $type = UrlInterface::URL_TYPE_LINK) * * @param string $url * @return string - * @deprecated No longer used, as we're generating product image URLs inside collection instead + * @deprecated 100.2.0 No longer used, as we're generating product image URLs inside collection instead * @see \Magento\Sitemap\Model\ResourceModel\Catalog\Product::_loadProductImages() */ protected function _getMediaUrl($url) @@ -807,7 +809,7 @@ public function getSitemapUrl($sitemapPath, $sitemapFileName) * Check is enabled submission to robots.txt * * @return bool - * @deprecated Because the robots.txt file is not generated anymore, + * @deprecated 100.1.5 Because the robots.txt file is not generated anymore, * this method is not needed and will be removed in major release. */ protected function _isEnabledSubmissionRobots() @@ -821,7 +823,7 @@ protected function _isEnabledSubmissionRobots() * * @param string $sitemapFileName * @return void - * @deprecated Because the robots.txt file is not generated anymore, + * @deprecated 100.1.5 Because the robots.txt file is not generated anymore, * this method is not needed and will be removed in major release. */ protected function _addSitemapToRobotsTxt($sitemapFileName) @@ -891,7 +893,7 @@ private function mapToSitemapItem() * Get unique page cache identities * * @return array - * @since 100.2.0 + * @since 100.1.5 */ public function getIdentities() { diff --git a/app/code/Magento/Sitemap/Model/SitemapConfigReaderInterface.php b/app/code/Magento/Sitemap/Model/SitemapConfigReaderInterface.php index f094b8856ab14..f11b54c5842f8 100644 --- a/app/code/Magento/Sitemap/Model/SitemapConfigReaderInterface.php +++ b/app/code/Magento/Sitemap/Model/SitemapConfigReaderInterface.php @@ -10,6 +10,7 @@ * Sitemap config reader interface * * @api + * @since 100.3.0 */ interface SitemapConfigReaderInterface { @@ -18,6 +19,7 @@ interface SitemapConfigReaderInterface * * @param int $storeId * @return int + * @since 100.3.0 */ public function getEnableSubmissionRobots($storeId); @@ -26,6 +28,7 @@ public function getEnableSubmissionRobots($storeId); * * @param int $storeId * @return int + * @since 100.3.0 */ public function getMaximumFileSize($storeId); @@ -34,6 +37,7 @@ public function getMaximumFileSize($storeId); * * @param int $storeId * @return int + * @since 100.3.0 */ public function getMaximumLinesNumber($storeId); @@ -42,6 +46,7 @@ public function getMaximumLinesNumber($storeId); * * @param int $storeId * @return string + * @since 100.3.0 */ public function getProductImageIncludePolicy($storeId); @@ -49,6 +54,7 @@ public function getProductImageIncludePolicy($storeId); * Get list valid paths for generate a sitemap XML file * * @return string[] + * @since 100.3.0 */ public function getValidPaths(); } diff --git a/app/code/Magento/Sitemap/Model/SitemapItemInterface.php b/app/code/Magento/Sitemap/Model/SitemapItemInterface.php index afd95768a2c84..94f19c5726b13 100644 --- a/app/code/Magento/Sitemap/Model/SitemapItemInterface.php +++ b/app/code/Magento/Sitemap/Model/SitemapItemInterface.php @@ -10,6 +10,7 @@ * Representation of sitemap item * * @api + * @since 100.3.0 */ interface SitemapItemInterface { @@ -18,6 +19,7 @@ interface SitemapItemInterface * Get url * * @return string + * @since 100.3.0 */ public function getUrl(); @@ -25,6 +27,7 @@ public function getUrl(); * Get priority * * @return string + * @since 100.3.0 */ public function getPriority(); @@ -32,6 +35,7 @@ public function getPriority(); * Get change frequency * * @return string + * @since 100.3.0 */ public function getChangeFrequency(); @@ -39,6 +43,7 @@ public function getChangeFrequency(); * Get images * * @return array|null + * @since 100.3.0 */ public function getImages(); @@ -46,6 +51,7 @@ public function getImages(); * Get last update date * * @return string|null + * @since 100.3.0 */ public function getUpdatedAt(); } diff --git a/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php b/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php index 0ddac1bb98fd1..3513fe32cd9b8 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Sitemap\Test\Unit\Block; @@ -10,49 +11,35 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\DataObject; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Element\Context; use Magento\Robots\Model\Config\Value; use Magento\Sitemap\Block\Robots; -use Magento\Sitemap\Helper\Data; use Magento\Sitemap\Model\ResourceModel\Sitemap\Collection; use Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory; use Magento\Sitemap\Model\Sitemap; +use Magento\Sitemap\Model\SitemapConfigReader; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; -use Magento\Store\Model\StoreResolver; use Magento\Store\Model\Website; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** + * Test for \Magento\Sitemap\Block\Robots. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RobotsTest extends TestCase { - /** - * @var Context|MockObject - */ - private $context; - - /** - * @var StoreResolver|MockObject - */ - private $storeResolver; - /** * @var CollectionFactory|MockObject */ private $sitemapCollectionFactory; - /** - * @var Data|MockObject - */ - private $sitemapHelper; - /** * @var Robots */ - private $block; + private $model; /** * @var ManagerInterface|MockObject @@ -69,147 +56,112 @@ class RobotsTest extends TestCase */ private $storeManager; + /** + * @var SitemapConfigReader|MockObject + */ + private $siteMapConfigReader; + + /** + * @inheritDoc + */ protected function setUp(): void { - $this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class) - ->getMockForAbstractClass(); - - $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) - ->getMockForAbstractClass(); + $objectManager = new ObjectManager($this); - $this->context = $this->getMockBuilder(Context::class) - ->disableOriginalConstructor() - ->getMock(); + $this->eventManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); + $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); - $this->context->expects($this->any()) + $context = $this->createMock(Context::class); + $context->expects($this->any()) ->method('getEventManager') ->willReturn($this->eventManagerMock); - - $this->context->expects($this->any()) + $context->expects($this->any()) ->method('getScopeConfig') ->willReturn($this->scopeConfigMock); - $this->storeResolver = $this->getMockBuilder(StoreResolver::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->sitemapCollectionFactory = $this->getMockBuilder( - CollectionFactory::class - ) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - - $this->sitemapHelper = $this->getMockBuilder(Data::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) - ->getMockForAbstractClass(); - - $this->block = new Robots( - $this->context, - $this->storeResolver, - $this->sitemapCollectionFactory, - $this->sitemapHelper, - $this->storeManager + $this->sitemapCollectionFactory = $this->createMock(CollectionFactory::class); + $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->siteMapConfigReader = $this->createMock(SitemapConfigReader::class); + + $this->model = $objectManager->getObject( + Robots::class, + [ + 'context' => $context, + 'sitemapCollectionFactory' => $this->sitemapCollectionFactory, + 'storeManager' => $this->storeManager, + 'sitemapConfigReader' => $this->siteMapConfigReader + ] ); } /** * Check toHtml() method in case when robots submission is disabled + * + * @return void */ - public function testToHtmlRobotsSubmissionIsDisabled() + public function testToHtmlRobotsSubmissionIsDisabled(): void { $defaultStoreId = 1; - $defaultWebsiteId = 1; - $expected = ''; $this->initEventManagerMock($expected); - $this->scopeConfigMock->expects($this->once())->method('getValue')->willReturn(false); - - $storeMock = $this->getMockBuilder(StoreInterface::class) - ->getMockForAbstractClass(); - - $storeMock->expects($this->once()) - ->method('getWebsiteId') - ->willReturn($defaultWebsiteId); - - $this->storeManager->expects($this->once()) - ->method('getDefaultStoreView') - ->willReturn($storeMock); - - $storeMock->expects($this->any()) - ->method('getWebsiteId') - ->willReturn($defaultWebsiteId); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn(false); - $websiteMock = $this->getMockBuilder(Website::class) - ->disableOriginalConstructor() - ->getMock(); - $websiteMock->expects($this->any()) + $websiteMock = $this->createMock(Website::class); + $websiteMock->expects($this->once()) ->method('getStoreIds') ->willReturn([$defaultStoreId]); $this->storeManager->expects($this->once()) ->method('getWebsite') - ->with($defaultWebsiteId) + ->with(null) ->willReturn($websiteMock); - $this->sitemapHelper->expects($this->once()) + $this->siteMapConfigReader->expects($this->once()) ->method('getEnableSubmissionRobots') ->with($defaultStoreId) ->willReturn(false); - $this->assertEquals($expected, $this->block->toHtml()); + $this->assertEquals($expected, $this->model->toHtml()); } /** * Check toHtml() method in case when robots submission is enabled + * + * @return void */ - public function testAfterGetDataRobotsSubmissionIsEnabled() + public function testAfterGetDataRobotsSubmissionIsEnabled(): void { $defaultStoreId = 1; $secondStoreId = 2; - $defaultWebsiteId = 1; $sitemapPath = '/'; $sitemapFilenameOne = 'sitemap.xml'; $sitemapFilenameTwo = 'sitemap_custom.xml'; $sitemapFilenameThree = 'sitemap.xml'; - $expected = 'Sitemap: ' . $sitemapFilenameOne - . PHP_EOL - . 'Sitemap: ' . $sitemapFilenameTwo - . PHP_EOL; + $expected = 'Sitemap: ' . $sitemapFilenameOne . PHP_EOL . 'Sitemap: ' . $sitemapFilenameTwo . PHP_EOL; $this->initEventManagerMock($expected); - $this->scopeConfigMock->expects($this->once())->method('getValue')->willReturn(false); - - $storeMock = $this->getMockBuilder(StoreInterface::class) - ->getMockForAbstractClass(); - - $this->storeManager->expects($this->once()) - ->method('getDefaultStoreView') - ->willReturn($storeMock); - - $storeMock->expects($this->any()) - ->method('getWebsiteId') - ->willReturn($defaultWebsiteId); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn(false); $websiteMock = $this->getMockBuilder(Website::class) ->disableOriginalConstructor() ->getMock(); - $websiteMock->expects($this->any()) + $websiteMock->expects($this->once()) ->method('getStoreIds') ->willReturn([$defaultStoreId, $secondStoreId]); $this->storeManager->expects($this->once()) ->method('getWebsite') - ->with($defaultWebsiteId) + ->with(null) ->willReturn($websiteMock); - $this->sitemapHelper->expects($this->any()) + $this->siteMapConfigReader->expects($this->atLeastOnce()) ->method('getEnableSubmissionRobots') ->willReturnMap([ [$defaultStoreId, true], @@ -223,12 +175,12 @@ public function testAfterGetDataRobotsSubmissionIsEnabled() $sitemapCollectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); - $sitemapCollectionMock->expects($this->any()) + $sitemapCollectionMock->expects($this->once()) ->method('addStoreFilter') ->with([$defaultStoreId]) ->willReturnSelf(); - $sitemapCollectionMock->expects($this->any()) + $sitemapCollectionMock->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator([$sitemapMockOne, $sitemapMockTwo, $sitemapMockThree])); @@ -236,18 +188,19 @@ public function testAfterGetDataRobotsSubmissionIsEnabled() ->method('create') ->willReturn($sitemapCollectionMock); - $this->assertEquals($expected, $this->block->toHtml()); + $this->assertEquals($expected, $this->model->toHtml()); } /** * Check that getIdentities() method returns specified cache tag + * + * @return void */ - public function testGetIdentities() + public function testGetIdentities(): void { $storeId = 1; - $storeMock = $this->getMockBuilder(StoreInterface::class) - ->getMockForAbstractClass(); + $storeMock = $this->getMockForAbstractClass(StoreInterface::class); $this->storeManager->expects($this->once()) ->method('getDefaultStoreView') @@ -257,10 +210,8 @@ public function testGetIdentities() ->method('getId') ->willReturn($storeId); - $expected = [ - Value::CACHE_TAG . '_' . $storeId, - ]; - $this->assertEquals($expected, $this->block->getIdentities()); + $expected = [Value::CACHE_TAG . '_' . $storeId]; + $this->assertEquals($expected, $this->model->getIdentities()); } /** @@ -269,23 +220,18 @@ public function testGetIdentities() * @param string $data * @return void */ - protected function initEventManagerMock($data) + protected function initEventManagerMock($data): void { $this->eventManagerMock->expects($this->any()) ->method('dispatch') ->willReturnMap([ [ 'view_block_abstract_to_html_before', - [ - 'block' => $this->block, - ], + ['block' => $this->model], ], [ 'view_block_abstract_to_html_after', - [ - 'block' => $this->block, - 'transport' => new DataObject(['html' => $data]), - ], + ['block' => $this->model, 'transport' => new DataObject(['html' => $data])], ], ]); } @@ -297,15 +243,12 @@ protected function initEventManagerMock($data) * @param string $sitemapFilename * @return MockObject */ - protected function getSitemapMock($sitemapPath, $sitemapFilename) + protected function getSitemapMock($sitemapPath, $sitemapFilename): MockObject { $sitemapMock = $this->getMockBuilder(Sitemap::class) ->disableOriginalConstructor() - ->setMethods([ - 'getSitemapFilename', - 'getSitemapPath', - 'getSitemapUrl', - ]) + ->onlyMethods(['getSitemapUrl']) + ->addMethods(['getSitemapFilename', 'getSitemapPath']) ->getMock(); $sitemapMock->expects($this->any()) diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php index 23ebe4f85f79e..70520862faee1 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php @@ -142,4 +142,39 @@ public function testScheduledGenerateSitemapsSendsExceptionEmail() $this->observer->scheduledGenerateSitemaps(); } + + /** + * Test if cron scheduled XML sitemap generation will start and stop the store environment emulation + * + * @throws \Exception + */ + public function testCronGenerateSitemapEnvironmentEmulation() + { + $storeId = 1; + + $this->scopeConfigMock->expects($this->once())->method('isSetFlag')->willReturn(true); + + $this->collectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->sitemapCollectionMock); + + $this->sitemapCollectionMock->expects($this->any()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$this->sitemapMock])); + + $this->sitemapMock->expects($this->at(0)) + ->method('getStoreId') + ->willReturn($storeId); + + $this->sitemapMock->expects($this->once()) + ->method('generateXml'); + + $this->appEmulationMock->expects($this->once()) + ->method('startEnvironmentEmulation'); + + $this->appEmulationMock->expects($this->once()) + ->method('stopEnvironmentEmulation'); + + $this->observer->scheduledGenerateSitemaps(); + } } diff --git a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php index 537fec4c75df6..8f6011f1ae56f 100644 --- a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php +++ b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php @@ -6,7 +6,7 @@ namespace Magento\Store\Api\Data; /** - * StoreConfig interface + * Interface for store config * * @api * @since 100.0.2 @@ -141,7 +141,7 @@ public function setWeightUnit($weightUnit); public function getBaseUrl(); /** - * set base URL + * Set base URL * * @param string $baseUrl * @return $this @@ -201,7 +201,7 @@ public function setBaseMediaUrl($baseMediaUrl); public function getSecureBaseUrl(); /** - * set secure base URL + * Set secure base URL * * @param string $secureBaseUrl * @return $this diff --git a/app/code/Magento/Store/Api/Data/StoreInterface.php b/app/code/Magento/Store/Api/Data/StoreInterface.php index 0f724a23fc096..527a7038e261a 100644 --- a/app/code/Magento/Store/Api/Data/StoreInterface.php +++ b/app/code/Magento/Store/Api/Data/StoreInterface.php @@ -69,11 +69,13 @@ public function getStoreGroupId(); /** * @param int $isActive * @return $this + * @since 101.0.0 */ public function setIsActive($isActive); /** * @return int + * @since 101.0.0 */ public function getIsActive(); diff --git a/app/code/Magento/Store/Api/StoreResolverInterface.php b/app/code/Magento/Store/Api/StoreResolverInterface.php index 7c32e321fa6c4..d03d68d213135 100644 --- a/app/code/Magento/Store/Api/StoreResolverInterface.php +++ b/app/code/Magento/Store/Api/StoreResolverInterface.php @@ -8,7 +8,7 @@ /** * Store resolver interface * - * @deprecated + * @deprecated 101.0.0 * @see \Magento\Store\Model\StoreManagerInterface */ interface StoreResolverInterface diff --git a/app/code/Magento/Store/App/Response/Redirect.php b/app/code/Magento/Store/App/Response/Redirect.php index da0c49aa1bc11..7984939108d89 100644 --- a/app/code/Magento/Store/App/Response/Redirect.php +++ b/app/code/Magento/Store/App/Response/Redirect.php @@ -7,36 +7,55 @@ */ namespace Magento\Store\App\Response; +use Laminas\Uri\Uri; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\Area; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Response\RedirectInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\App\State; +use Magento\Framework\Encryption\UrlCoder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Session\SessionManagerInterface; +use Magento\Framework\Session\SidResolverInterface; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Class Redirect computes redirect urls responses. * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Redirect implements \Magento\Framework\App\Response\RedirectInterface +class Redirect implements RedirectInterface { + private const XML_PATH_USE_CUSTOM_ADMIN_URL = 'admin/url/use_custom'; + private const XML_PATH_CUSTOM_ADMIN_URL = 'admin/url/custom'; + /** - * @var \Magento\Framework\App\RequestInterface + * @var RequestInterface */ protected $_request; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Framework\Encryption\UrlCoder + * @var UrlCoder */ protected $_urlCoder; /** - * @var \Magento\Framework\Session\SessionManagerInterface + * @var SessionManagerInterface */ protected $_session; /** - * @var \Magento\Framework\Session\SidResolverInterface + * @var SidResolverInterface */ protected $_sidResolver; @@ -46,36 +65,51 @@ class Redirect implements \Magento\Framework\App\Response\RedirectInterface protected $_canUseSessionIdInParam; /** - * @var \Magento\Framework\UrlInterface + * @var UrlInterface */ protected $_urlBuilder; /** - * @var \Laminas\Uri\Uri|null + * @var Uri */ private $uri; + /** + * @var State + */ + private $appState; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * Constructor * - * @param \Magento\Framework\App\RequestInterface $request - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Encryption\UrlCoder $urlCoder - * @param \Magento\Framework\Session\SessionManagerInterface $session - * @param \Magento\Framework\Session\SidResolverInterface $sidResolver - * @param \Magento\Framework\UrlInterface $urlBuilder - * @param \Laminas\Uri\Uri|null $uri + * @param RequestInterface $request + * @param StoreManagerInterface $storeManager + * @param UrlCoder $urlCoder + * @param SessionManagerInterface $session + * @param SidResolverInterface $sidResolver + * @param UrlInterface $urlBuilder + * @param Uri|null $uri * @param bool $canUseSessionIdInParam + * @param State|null $appState + * @param ScopeConfigInterface|null $scopeConfig + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\RequestInterface $request, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Encryption\UrlCoder $urlCoder, - \Magento\Framework\Session\SessionManagerInterface $session, - \Magento\Framework\Session\SidResolverInterface $sidResolver, - \Magento\Framework\UrlInterface $urlBuilder, - \Laminas\Uri\Uri $uri = null, - $canUseSessionIdInParam = true + RequestInterface $request, + StoreManagerInterface $storeManager, + UrlCoder $urlCoder, + SessionManagerInterface $session, + SidResolverInterface $sidResolver, + UrlInterface $urlBuilder, + Uri $uri = null, + $canUseSessionIdInParam = true, + ?State $appState = null, + ?ScopeConfigInterface $scopeConfig = null ) { $this->_canUseSessionIdInParam = $canUseSessionIdInParam; $this->_request = $request; @@ -84,20 +118,22 @@ public function __construct( $this->_session = $session; $this->_sidResolver = $sidResolver; $this->_urlBuilder = $urlBuilder; - $this->uri = $uri ?: ObjectManager::getInstance()->get(\Laminas\Uri\Uri::class); + $this->uri = $uri ?: ObjectManager::getInstance()->get(Uri::class); + $this->appState = $appState ?: ObjectManager::getInstance()->get(State::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** * Get the referrer url. * * @return string - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException */ protected function _getUrl() { $refererUrl = $this->_request->getServer('HTTP_REFERER'); - $encodedUrl = $this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED) - ?: $this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_BASE64_URL); + $encodedUrl = $this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED) + ?: $this->_request->getParam(ActionInterface::PARAM_NAME_BASE64_URL); if ($encodedUrl) { $refererUrl = $this->_urlCoder->decode($encodedUrl); @@ -113,6 +149,7 @@ protected function _getUrl() } else { $refererUrl = $this->normalizeRefererUrl($refererUrl); } + return $refererUrl; } @@ -130,9 +167,9 @@ public function getRefererUrl() * Set referer url for redirect in response * * @param string $defaultUrl - * @return \Magento\Framework\App\ActionInterface + * @return ActionInterface * - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException */ public function getRedirectUrl($defaultUrl = null) { @@ -149,7 +186,7 @@ public function getRedirectUrl($defaultUrl = null) * @param string $defaultUrl * @return string * - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException */ public function error($defaultUrl) { @@ -160,6 +197,7 @@ public function error($defaultUrl) if (!$this->_isUrlInternal($errorUrl)) { $errorUrl = $this->_storeManager->getStore()->getBaseUrl(); } + return $errorUrl; } @@ -169,17 +207,17 @@ public function error($defaultUrl) * @param string $defaultUrl * @return string * - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException */ public function success($defaultUrl) { $successUrl = $this->_request->getParam(self::PARAM_NAME_SUCCESS_URL); - if (empty($successUrl)) { - $successUrl = $defaultUrl; - } + $successUrl = $successUrl ?: $defaultUrl; + if (!$this->_isUrlInternal($successUrl)) { $successUrl = $this->_storeManager->getStore()->getBaseUrl(); } + return $successUrl; } @@ -194,12 +232,12 @@ public function updatePathParams(array $arguments) /** * Set redirect into response * - * @param \Magento\Framework\App\ResponseInterface $response + * @param ResponseInterface $response * @param string $path * @param array $arguments * @return void */ - public function redirect(\Magento\Framework\App\ResponseInterface $response, $path, $arguments = []) + public function redirect(ResponseInterface $response, $path, $arguments = []) { $arguments = $this->updatePathParams($arguments); $response->setRedirect($this->_urlBuilder->getUrl($path, $arguments)); @@ -213,15 +251,69 @@ public function redirect(\Magento\Framework\App\ResponseInterface $response, $pa */ protected function _isUrlInternal($url) { - if (strpos($url, 'http') !== false) { - $directLinkType = \Magento\Framework\UrlInterface::URL_TYPE_DIRECT_LINK; - $unsecureBaseUrl = $this->_storeManager->getStore()->getBaseUrl($directLinkType, false); - $secureBaseUrl = $this->_storeManager->getStore()->getBaseUrl($directLinkType, true); - return (strpos($url, (string) $unsecureBaseUrl) === 0) || (strpos($url, (string) $secureBaseUrl) === 0); + return strpos($url, 'http') !== false + ? $this->isInternalUrl($url) || $this->isCustomAdminUrl($url) + : false; + } + + /** + * Is `Use Custom Admin URL` config enabled + * + * @return bool + */ + private function isUseCustomAdminUrlEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_USE_CUSTOM_ADMIN_URL, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Returns custom admin url + * + * @return string + */ + private function getCustomAdminUrl(): string + { + return $this->scopeConfig->getValue( + self::XML_PATH_CUSTOM_ADMIN_URL, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Is internal custom admin url + * + * @param string $url + * @return bool + */ + private function isCustomAdminUrl(string $url): bool + { + if ($this->appState->getAreaCode() === Area::AREA_ADMINHTML && $this->isUseCustomAdminUrlEnabled()) { + return strpos($url, $this->getCustomAdminUrl()) === 0; } + return false; } + /** + * Is url internal + * + * @param string $url + * @return bool + */ + private function isInternalUrl(string $url): bool + { + $directLinkType = UrlInterface::URL_TYPE_DIRECT_LINK; + $unsecureBaseUrl = $this->_storeManager->getStore() + ->getBaseUrl($directLinkType, false); + $secureBaseUrl = $this->_storeManager->getStore() + ->getBaseUrl($directLinkType, true); + + return strpos($url, (string) $unsecureBaseUrl) === 0 || strpos($url, (string) $secureBaseUrl) === 0; + } + /** * Normalize path to avoid wrong store change * @@ -264,10 +356,10 @@ protected function normalizeRefererQueryParts($refererQuery) $store = $this->_storeManager->getStore(); if ($store - && !empty($refererQuery[\Magento\Store\Model\StoreManagerInterface::PARAM_NAME]) - && ($refererQuery[\Magento\Store\Model\StoreManagerInterface::PARAM_NAME] !== $store->getCode()) + && !empty($refererQuery[StoreManagerInterface::PARAM_NAME]) + && ($refererQuery[StoreManagerInterface::PARAM_NAME] !== $store->getCode()) ) { - $refererQuery[\Magento\Store\Model\StoreManagerInterface::PARAM_NAME] = $store->getCode(); + $refererQuery[StoreManagerInterface::PARAM_NAME] = $store->getCode(); } return $refererQuery; diff --git a/app/code/Magento/Store/Block/Switcher.php b/app/code/Magento/Store/Block/Switcher.php index 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/Controller/Store/SwitchAction.php b/app/code/Magento/Store/Controller/Store/SwitchAction.php index 41acb1605ec7c..bea7dbbaaa5fb 100644 --- a/app/code/Magento/Store/Controller/Store/SwitchAction.php +++ b/app/code/Magento/Store/Controller/Store/SwitchAction.php @@ -36,7 +36,7 @@ class SwitchAction extends Action implements HttpGetActionInterface, HttpPostAct /** * @var HttpContext - * @deprecated + * @deprecated 100.2.5 */ protected $httpContext; @@ -47,7 +47,7 @@ class SwitchAction extends Action implements HttpGetActionInterface, HttpPostAct /** * @var StoreManagerInterface - * @deprecated + * @deprecated 100.2.5 */ protected $storeManager; diff --git a/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php b/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php index 317e4bf43e42c..2cbd4aeb20d4d 100644 --- a/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php +++ b/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php @@ -51,7 +51,7 @@ class Create implements ProcessorInterface /** * The event manager. * - * @deprecated logic moved inside of "afterSave" method + * @deprecated 100.2.5 logic moved inside of "afterSave" method * \Magento\Store\Model\Website::afterSave * \Magento\Store\Model\Group::afterSave * \Magento\Store\Model\Store::afterSave diff --git a/app/code/Magento/Store/Model/Config/Placeholder.php b/app/code/Magento/Store/Model/Config/Placeholder.php index be84c7f444c44..e04dc81c1359e 100644 --- a/app/code/Magento/Store/Model/Config/Placeholder.php +++ b/app/code/Magento/Store/Model/Config/Placeholder.php @@ -84,7 +84,7 @@ public function process(array $data = []) /** * Process array data recursively * - * @deprecated This method isn't used in process() implementation anymore + * @deprecated 101.0.4 This method isn't used in process() implementation anymore * * @param array &$data * @param string $path @@ -178,7 +178,7 @@ protected function _getValue($path, array $data) /** * Set array value by path * - * @deprecated This method isn't used in process() implementation anymore + * @deprecated 101.0.4 This method isn't used in process() implementation anymore * * @param array &$container * @param string $path diff --git a/app/code/Magento/Store/Model/Config/Processor/Fallback.php b/app/code/Magento/Store/Model/Config/Processor/Fallback.php index 4e8b3bca14c92..537802d312eed 100644 --- a/app/code/Magento/Store/Model/Config/Processor/Fallback.php +++ b/app/code/Magento/Store/Model/Config/Processor/Fallback.php @@ -178,20 +178,18 @@ private function getWebsiteConfig(array $websites, $id) */ private function loadScopes(): void { - $loaded = false; try { if ($this->deploymentConfig->isDbAvailable()) { $this->storeData = $this->storeResource->readAllStores(); $this->websiteData = $this->websiteResource->readAllWebsites(); - $loaded = true; + } else { + $this->storeData = $this->scopes->get('stores'); + $this->websiteData = $this->scopes->get('websites'); } } catch (TableNotFoundException $exception) { // database is empty or not setup - $loaded = false; - } - if (!$loaded) { - $this->storeData = $this->scopes->get('stores'); - $this->websiteData = $this->scopes->get('websites'); + $this->storeData = []; + $this->websiteData = []; } } } diff --git a/app/code/Magento/Store/Model/Data/StoreConfig.php b/app/code/Magento/Store/Model/Data/StoreConfig.php index 6634e2cb05bd9..e68d98b162613 100644 --- a/app/code/Magento/Store/Model/Data/StoreConfig.php +++ b/app/code/Magento/Store/Model/Data/StoreConfig.php @@ -6,7 +6,7 @@ namespace Magento\Store\Model\Data; /** - * Class StoreConfig + * Allows to get and set store config values * * @codeCoverageIgnore */ @@ -188,7 +188,7 @@ public function getBaseUrl() } /** - * set base URL + * Set base URL * * @param string $baseUrl * @return $this @@ -293,7 +293,7 @@ public function getSecureBaseUrl() } /** - * set secure base URL + * Set secure base URL * * @param string $secureBaseUrl * @return $this @@ -367,7 +367,7 @@ public function setSecureBaseMediaUrl($secureBaseMediaUrl) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Store\Api\Data\StoreConfigExtensionInterface|null */ @@ -377,7 +377,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Store\Api\Data\StoreConfigExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Store/Model/Group.php b/app/code/Magento/Store/Model/Group.php index 4cd7bc93d3334..7f1e71c422251 100644 --- a/app/code/Magento/Store/Model/Group.php +++ b/app/code/Magento/Store/Model/Group.php @@ -454,6 +454,7 @@ public function afterDelete() /** * @inheritdoc + * @since 100.2.5 */ public function afterSave() { diff --git a/app/code/Magento/Store/Model/ResourceModel/StoreWebsiteRelation.php b/app/code/Magento/Store/Model/ResourceModel/StoreWebsiteRelation.php index 7fcd9ac28c3ec..875c0e6cb3b05 100644 --- a/app/code/Magento/Store/Model/ResourceModel/StoreWebsiteRelation.php +++ b/app/code/Magento/Store/Model/ResourceModel/StoreWebsiteRelation.php @@ -27,6 +27,8 @@ public function __construct(ResourceConnection $resource) } /** + * Get store by website id + * * @param int $websiteId * @return array */ @@ -41,4 +43,37 @@ public function getStoreByWebsiteId($websiteId) $data = $connection->fetchCol($storeSelect); return $data; } + + /** + * Get website store data + * + * @param int $websiteId + * @param bool $available + * @param int|null $storeGroupId + * @return array + */ + public function getWebsiteStores(int $websiteId, bool $available = false, int $storeGroupId = null): array + { + $connection = $this->resource->getConnection(); + $storeTable = $this->resource->getTableName('store'); + $storeSelect = $connection->select()->from($storeTable)->where( + 'website_id = ?', + $websiteId + ); + + if ($storeGroupId) { + $storeSelect = $storeSelect->where( + 'group_id = ?', + $storeGroupId + ); + } + + if ($available) { + $storeSelect = $storeSelect->where( + 'is_active = 1' + ); + } + + return $connection->fetchAll($storeSelect); + } } diff --git a/app/code/Magento/Store/Model/Service/StoreConfigManager.php b/app/code/Magento/Store/Model/Service/StoreConfigManager.php index b3c2208a58361..ebc73036f7e37 100644 --- a/app/code/Magento/Store/Model/Service/StoreConfigManager.php +++ b/app/code/Magento/Store/Model/Service/StoreConfigManager.php @@ -3,24 +3,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Store\Model\Service; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Api\Data\StoreConfigInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Data\StoreConfig; +use Magento\Store\Model\Data\StoreConfigFactory; +use Magento\Store\Model\ResourceModel\Store\CollectionFactory; +use Magento\Store\Model\Store; + class StoreConfigManager implements \Magento\Store\Api\StoreConfigManagerInterface { /** - * @var \Magento\Store\Model\ResourceModel\Store\CollectionFactory + * @var CollectionFactory */ protected $storeCollectionFactory; /** - * @var \Magento\Store\Model\Data\StoreConfigFactory + * @var StoreConfigFactory */ protected $storeConfigFactory; /** * Core store config * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $scopeConfig; @@ -38,14 +47,14 @@ class StoreConfigManager implements \Magento\Store\Api\StoreConfigManagerInterfa ]; /** - * @param \Magento\Store\Model\ResourceModel\Store\CollectionFactory $storeCollectionFactory - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Store\Model\Data\StoreConfigFactory $storeConfigFactory + * @param CollectionFactory $storeCollectionFactory + * @param ScopeConfigInterface $scopeConfig + * @param StoreConfigFactory $storeConfigFactory */ public function __construct( - \Magento\Store\Model\ResourceModel\Store\CollectionFactory $storeCollectionFactory, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Store\Model\Data\StoreConfigFactory $storeConfigFactory + CollectionFactory $storeCollectionFactory, + ScopeConfigInterface $scopeConfig, + StoreConfigFactory $storeConfigFactory ) { $this->storeCollectionFactory = $storeCollectionFactory; $this->scopeConfig = $scopeConfig; @@ -53,8 +62,10 @@ public function __construct( } /** + * Get store configurations + * * @param string[] $storeCodes list of stores by store codes, will return all if storeCodes is not set - * @return \Magento\Store\Api\Data\StoreConfigInterface[] + * @return StoreConfigInterface[] */ public function getStoreConfigs(array $storeCodes = null) { @@ -71,12 +82,14 @@ public function getStoreConfigs(array $storeCodes = null) } /** - * @param \Magento\Store\Model\Store $store - * @return \Magento\Store\Api\Data\StoreConfigInterface + * Get store specific configs + * + * @param Store|StoreInterface $store + * @return StoreConfigInterface */ protected function getStoreConfig($store) { - /** @var \Magento\Store\Model\Data\StoreConfig $storeConfig */ + /** @var StoreConfig $storeConfig */ $storeConfig = $this->storeConfigFactory->create(); $storeConfig->setId($store->getId()) diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 5187bb8776632..7bcb3282ba552 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -208,7 +208,7 @@ class Store extends AbstractExtensibleModel implements * Flag that shows that backend URLs are secure * * @var boolean|null - * @deprecated unused protected property + * @deprecated 101.0.0 unused protected property */ protected $_isAdminSecure = null; @@ -278,7 +278,7 @@ class Store extends AbstractExtensibleModel implements /** * @var \Magento\Framework\Session\SidResolverInterface - * @deprecated Not used anymore. + * @deprecated 101.0.5 Not used anymore. */ protected $_sidResolver; @@ -1134,6 +1134,7 @@ public function setStoreGroupId($storeGroupId) /** * @inheritdoc + * @since 101.0.0 */ public function getIsActive() { @@ -1142,6 +1143,7 @@ public function getIsActive() /** * @inheritdoc + * @since 101.0.0 */ public function setIsActive($isActive) { @@ -1389,6 +1391,7 @@ public function getStorePath() /** * @inheritdoc + * @since 100.1.0 */ public function getScopeType() { @@ -1397,6 +1400,7 @@ public function getScopeType() /** * @inheritdoc + * @since 100.1.0 */ public function getScopeTypeName() { diff --git a/app/code/Magento/Store/Model/StoreResolver.php b/app/code/Magento/Store/Model/StoreResolver.php index aafdd15138981..2a950b699abe7 100644 --- a/app/code/Magento/Store/Model/StoreResolver.php +++ b/app/code/Magento/Store/Model/StoreResolver.php @@ -28,12 +28,12 @@ class StoreResolver implements \Magento\Store\Api\StoreResolverInterface protected $storeCookieManager; /** - * @deprecated + * @deprecated 101.0.0 */ protected $cache; /** - * @deprecated + * @deprecated 101.0.0 */ protected $readerList; @@ -142,7 +142,7 @@ protected function getStoresData() : array * Read stores data. First element is allowed store ids, second is default store id * * @return array - * @deprecated + * @deprecated 101.0.0 * @see \Magento\Store\Model\StoreResolver::getStoresData */ protected function readStoresData() : array diff --git a/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php b/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php index 3328a21e8f5e1..c907ab14c0137 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php @@ -14,6 +14,8 @@ /** * Remove SID, from_store, store from target url. + * + * Used in store-switching process in HTML frontend. */ class CleanTargetUrl implements StoreSwitcherInterface { @@ -22,42 +24,27 @@ class CleanTargetUrl implements StoreSwitcherInterface */ private $urlHelper; - /** - * @var \Magento\Framework\Session\Generic - */ - private $session; - - /** - * @var \Magento\Framework\Session\SidResolverInterface - */ - private $sidResolver; - /** * @param UrlHelper $urlHelper - * @param \Magento\Framework\Session\Generic $session - * @param \Magento\Framework\Session\SidResolverInterface $sidResolver */ public function __construct( - UrlHelper $urlHelper, - \Magento\Framework\Session\Generic $session, - \Magento\Framework\Session\SidResolverInterface $sidResolver + UrlHelper $urlHelper ) { $this->urlHelper = $urlHelper; - $this->session = $session; - $this->sidResolver = $sidResolver; } /** + * Generate target URL to switch stores through other mechanism then via URL params. + * * @param StoreInterface $fromStore store where we came from * @param StoreInterface $targetStore store where to go to * @param string $redirectUrl original url requested for redirect after switching * @return string redirect url + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string { $targetUrl = $redirectUrl; - $sidName = $this->sidResolver->getSessionIdQueryParam($this->session); - $targetUrl = $this->urlHelper->removeRequestParam($targetUrl, $sidName); $targetUrl = $this->urlHelper->removeRequestParam($targetUrl, '___from_store'); $targetUrl = $this->urlHelper->removeRequestParam($targetUrl, StoreResolverInterface::PARAM_NAME); diff --git a/app/code/Magento/Store/Model/System/Store.php b/app/code/Magento/Store/Model/System/Store.php index d13781b8c146b..a56cdcc37dd54 100644 --- a/app/code/Magento/Store/Model/System/Store.php +++ b/app/code/Magento/Store/Model/System/Store.php @@ -229,6 +229,7 @@ public function getStoresStructure($isAll = false, $storeIds = [], $groupIds = [ * @param array $groupIds * @param array $websiteIds * @return array Format: array(array('value' => '<value>', 'label' => '<label>'), ...) + * @since 101.1.0 */ public function getStoreOptionsTree($isAll = false, $storeIds = [], $groupIds = [], $websiteIds = []): array { diff --git a/app/code/Magento/Store/Setup/Patch/Schema/InitializeStoresAndWebsites.php b/app/code/Magento/Store/Setup/Patch/Schema/InitializeStoresAndWebsites.php index 05e46e04b5c96..dc1932bdd8943 100644 --- a/app/code/Magento/Store/Setup/Patch/Schema/InitializeStoresAndWebsites.php +++ b/app/code/Magento/Store/Setup/Patch/Schema/InitializeStoresAndWebsites.php @@ -142,7 +142,7 @@ public function apply() /** * Get default category. * - * @deprecated 100.1.0 + * @deprecated 101.0.0 * @return DefaultCategory */ private function getDefaultCategory() 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/ActionGroup/AdminSystemStoreOpenPageActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSystemStoreOpenPageActionGroup.xml new file mode 100644 index 0000000000000..7e898cd1d8f78 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSystemStoreOpenPageActionGroup.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="AdminSystemStoreOpenPageActionGroup"> + <annotations> + <description>Go to admin system store page.</description> + </annotations> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToSystemStore"/> + <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml new file mode 100644 index 0000000000000..4a403364a91e3 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.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="StorefrontSwitchStoreActionGroup"> + <annotations> + <description>Switch the Storefront to the provided Store.</description> + </annotations> + <arguments> + <argument name="storeName" type="string"/> + </arguments> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickOnSwitchStoreButton"/> + <click selector="{{StorefrontFooterSection.storeLink(storeName)}}" stepKey="selectStoreToSwitchOn"/> + </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/AdminCreateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml index 4171aa6f08915..9de820baa93bf 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml @@ -33,7 +33,7 @@ </after> <!--Filter grid and see created store view--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="navigateToStoresIndex"/> <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> <fillField selector="{{AdminStoresGridSection.storeFilterTextField}}" userInput="{{customStore.name}}" stepKey="fillStoreViewFilterField"/> <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearch"/> 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/App/Response/RedirectTest.php b/app/code/Magento/Store/Test/Unit/App/Response/RedirectTest.php index 9648af9ddf61f..20879bd31ac16 100644 --- a/app/code/Magento/Store/Test/Unit/App/Response/RedirectTest.php +++ b/app/code/Magento/Store/Test/Unit/App/Response/RedirectTest.php @@ -5,110 +5,139 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Store\Test\Unit\App\Response; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http; -use Magento\Framework\Encryption\UrlCoder; -use Magento\Framework\Session\SessionManagerInterface; -use Magento\Framework\Session\SidResolverInterface; -use Magento\Framework\UrlInterface; +use Magento\Framework\App\State; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\App\Response\Redirect; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\Area; +use Magento\Store\Model\ScopeInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Store\App\Response\Redirect. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class RedirectTest extends TestCase { - /** - * @var Redirect - */ - protected $_model; + private const XML_PATH_USE_CUSTOM_ADMIN_URL = 'admin/url/use_custom'; + private const XML_PATH_CUSTOM_ADMIN_URL = 'admin/url/custom'; + + private const STUB_INTERNAL_URL = 'http://internalurl.com/'; + private const STUB_EXTERNAL_URL = 'http://externalurl.com/'; + private const STUB_CUSTOM_ADMIN_URL = 'http://externalurl.com/admin/'; /** - * @var MockObject + * @var Redirect */ - protected $_requestMock; + private $model; /** - * @var MockObject + * @var Http|MockObject */ - protected $_storeManagerMock; + private $requestMock; /** - * @var MockObject + * @var StoreManagerInterface|MockObject */ - protected $_urlCoderMock; + private $storeManagerMock; /** - * @var MockObject + * @var State|MockObject */ - protected $_sessionMock; + private $appStateMock; /** - * @var MockObject + * @var ScopeConfigInterface|MockObject */ - protected $_sidResolverMock; + private $scopeConfigMock; /** - * @var MockObject + * @inheritDoc */ - protected $_urlBuilderMock; - protected function setUp(): void { - $this->_requestMock = $this->getMockBuilder(Http::class) - ->disableOriginalConstructor() - ->getMock(); - $this->_storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); - $this->_urlCoderMock = $this->createMock(UrlCoder::class); - $this->_sessionMock = $this->getMockForAbstractClass(SessionManagerInterface::class); - $this->_sidResolverMock = $this->getMockForAbstractClass(SidResolverInterface::class); - $this->_urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class); - - $this->_model = new Redirect( - $this->_requestMock, - $this->_storeManagerMock, - $this->_urlCoderMock, - $this->_sessionMock, - $this->_sidResolverMock, - $this->_urlBuilderMock + $objectManager = new ObjectManager($this); + + $this->requestMock = $this->createMock(Http::class); + $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->appStateMock = $this->createMock(State::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + + $this->model = $objectManager->getObject( + Redirect::class, + [ + 'request' => $this->requestMock, + 'storeManager' => $this->storeManagerMock, + 'appState' => $this->appStateMock, + 'scopeConfig' => $this->scopeConfigMock, + ] ); } /** + * Success url test + * * @dataProvider urlAddresses - * @param string $baseUrl - * @param string $successUrl + * + * @param string $url + * @param string $area + * @param bool $isCustomAdminUrlEnabled + * @param string $expectedUrl + * @return void */ - public function testSuccessUrl($baseUrl, $successUrl) - { + public function testSuccessUrl( + string $url, + string $area, + bool $isCustomAdminUrlEnabled, + string $expectedUrl + ): void { $testStoreMock = $this->createMock(Store::class); - $testStoreMock->expects($this->any())->method('getBaseUrl')->willReturn($baseUrl); - $this->_requestMock->expects($this->any())->method('getParam')->willReturn(null); - $this->_storeManagerMock->expects($this->any())->method('getStore') + $testStoreMock->expects($this->atLeastOnce()) + ->method('getBaseUrl') + ->willReturn(self::STUB_INTERNAL_URL); + $this->requestMock->expects($this->once()) + ->method('getParam') + ->willReturn(null); + $this->storeManagerMock->expects($this->atLeastOnce()) + ->method('getStore') ->willReturn($testStoreMock); - $this->assertEquals($baseUrl, $this->_model->success($successUrl)); + $this->appStateMock->expects($this->once()) + ->method('getAreaCode') + ->willReturn($area); + $this->scopeConfigMock->expects($this->any()) + ->method('isSetFlag') + ->with(self::XML_PATH_USE_CUSTOM_ADMIN_URL, ScopeInterface::SCOPE_STORE) + ->willReturn($isCustomAdminUrlEnabled); + $this->scopeConfigMock->expects($this->any()) + ->method('getValue') + ->with(self::XML_PATH_CUSTOM_ADMIN_URL, ScopeInterface::SCOPE_STORE) + ->willReturn(self::STUB_CUSTOM_ADMIN_URL); + + $this->assertEquals($expectedUrl, $this->model->success($url)); } /** - * DataProvider with the test urls + * Data provider for testSuccessUrlWithCustomAdminUrl * * @return array */ - public function urlAddresses() + public function urlAddresses(): array { return [ - [ - 'http://externalurl.com/', - 'http://internalurl.com/', - ], - [ - 'http://internalurl.com/', - 'http://internalurl.com/' - ] + [self::STUB_CUSTOM_ADMIN_URL, Area::AREA_ADMINHTML, true, self::STUB_CUSTOM_ADMIN_URL], + [self::STUB_CUSTOM_ADMIN_URL, Area::AREA_ADMINHTML, false, self::STUB_INTERNAL_URL], + [self::STUB_CUSTOM_ADMIN_URL, Area::AREA_FRONTEND, true, self::STUB_INTERNAL_URL], + [self::STUB_EXTERNAL_URL, Area::AREA_ADMINHTML, true, self::STUB_INTERNAL_URL], + [self::STUB_EXTERNAL_URL, Area::AREA_FRONTEND, true, self::STUB_INTERNAL_URL], ]; } } 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/Store/etc/config.xml b/app/code/Magento/Store/etc/config.xml index 07e4c8b0b6529..83bb4432ac18f 100644 --- a/app/code/Magento/Store/etc/config.xml +++ b/app/code/Magento/Store/etc/config.xml @@ -132,6 +132,7 @@ <shtml>shtml</shtml> <phpt>phpt</phpt> <pht>pht</pht> + <svg>svg</svg> </protected_extensions> <public_files_valid_paths> <protected> diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 5bd8f6e2349fc..2da9e91e1fddd 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -65,7 +65,6 @@ <preference for="Magento\Framework\App\Router\PathConfigInterface" type="Magento\Store\Model\PathConfig" /> <type name="Magento\Framework\App\ActionInterface"> <plugin name="storeCheck" type="Magento\Store\App\Action\Plugin\StoreCheck"/> - <plugin name="designLoader" type="Magento\Framework\App\Action\Plugin\LoadDesignPlugin"/> <plugin name="eventDispatch" type="Magento\Framework\App\Action\Plugin\EventDispatchPlugin"/> <plugin name="actionFlagNoDispatch" type="Magento\Framework\App\Action\Plugin\ActionFlagNoDispatchPlugin"/> </type> diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/AvailableStoresResolver.php b/app/code/Magento/StoreGraphQl/Model/Resolver/AvailableStoresResolver.php new file mode 100644 index 0000000000000..5e49a8a0b7ff0 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/AvailableStoresResolver.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider; + +/** + * AvailableStores page field resolver, used for GraphQL request processing. + */ +class AvailableStoresResolver implements ResolverInterface +{ + /** + * @var StoreConfigDataProvider + */ + private $storeConfigDataProvider; + + /** + * @param StoreConfigDataProvider $storeConfigsDataProvider + */ + public function __construct( + StoreConfigDataProvider $storeConfigsDataProvider + ) { + $this->storeConfigDataProvider = $storeConfigsDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $storeGroupId = !empty($args['useCurrentGroup']) ? + (int)$context->getExtensionAttributes()->getStore()->getStoreGroupId() : + null; + return $this->storeConfigDataProvider->getAvailableStoreConfig( + (int)$context->getExtensionAttributes()->getStore()->getWebsiteId(), + $storeGroupId + ); + } +} diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php index 59f9831789a35..8378b3bc7a4be 100644 --- a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php @@ -8,7 +8,9 @@ namespace Magento\StoreGraphQl\Model\Resolver\Store; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Api\Data\StoreConfigInterface; use Magento\Store\Api\StoreConfigManagerInterface; +use Magento\Store\Model\ResourceModel\StoreWebsiteRelation; use Magento\Store\Model\ScopeInterface; use Magento\Store\Api\Data\StoreInterface; @@ -32,19 +34,27 @@ class StoreConfigDataProvider */ private $extendedConfigData; + /** + * @var StoreWebsiteRelation + */ + private $storeWebsiteRelation; + /** * @param StoreConfigManagerInterface $storeConfigManager * @param ScopeConfigInterface $scopeConfig + * @param StoreWebsiteRelation $storeWebsiteRelation * @param array $extendedConfigData */ public function __construct( StoreConfigManagerInterface $storeConfigManager, ScopeConfigInterface $scopeConfig, + StoreWebsiteRelation $storeWebsiteRelation, array $extendedConfigData = [] ) { $this->storeConfigManager = $storeConfigManager; $this->scopeConfig = $scopeConfig; $this->extendedConfigData = $extendedConfigData; + $this->storeWebsiteRelation = $storeWebsiteRelation; } /** @@ -55,24 +65,43 @@ public function __construct( */ public function getStoreConfigData(StoreInterface $store): array { - $storeConfigData = array_merge( - $this->getBaseConfigData($store), - $this->getExtendedConfigData((int)$store->getId()) - ); - return $storeConfigData; + $defaultStoreConfig = $this->storeConfigManager->getStoreConfigs([$store->getCode()]); + return $this->prepareStoreConfigData(current($defaultStoreConfig), $store->getName()); } /** - * Get base config data + * Get available website stores * - * @param StoreInterface $store + * @param int $websiteId + * @param int|null $storeGroupId * @return array */ - private function getBaseConfigData(StoreInterface $store) : array + public function getAvailableStoreConfig(int $websiteId, int $storeGroupId = null): array { - $storeConfig = current($this->storeConfigManager->getStoreConfigs([$store->getCode()])); + $websiteStores = $this->storeWebsiteRelation->getWebsiteStores($websiteId, true, $storeGroupId); + $storeCodes = array_column($websiteStores, 'code'); + + $storeConfigs = $this->storeConfigManager->getStoreConfigs($storeCodes); + $storesConfigData = []; + + foreach ($storeConfigs as $storeConfig) { + $key = array_search($storeConfig->getCode(), array_column($websiteStores, 'code'), true); + $storesConfigData[] = $this->prepareStoreConfigData($storeConfig, $websiteStores[$key]['name']); + } - $storeConfigData = [ + return $storesConfigData; + } + + /** + * Prepare store config data + * + * @param StoreConfigInterface $storeConfig + * @param string $storeName + * @return array + */ + private function prepareStoreConfigData(StoreConfigInterface $storeConfig, string $storeName): array + { + return array_merge([ 'id' => $storeConfig->getId(), 'code' => $storeConfig->getCode(), 'website_id' => $storeConfig->getWebsiteId(), @@ -83,14 +112,14 @@ private function getBaseConfigData(StoreInterface $store) : array 'weight_unit' => $storeConfig->getWeightUnit(), 'base_url' => $storeConfig->getBaseUrl(), 'base_link_url' => $storeConfig->getBaseLinkUrl(), - 'base_static_url' => $storeConfig->getSecureBaseStaticUrl(), + 'base_static_url' => $storeConfig->getBaseStaticUrl(), 'base_media_url' => $storeConfig->getBaseMediaUrl(), 'secure_base_url' => $storeConfig->getSecureBaseUrl(), 'secure_base_link_url' => $storeConfig->getSecureBaseLinkUrl(), 'secure_base_static_url' => $storeConfig->getSecureBaseStaticUrl(), - 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl() - ]; - return $storeConfigData; + 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl(), + 'store_name' => $storeName, + ], $this->getExtendedConfigData((int)$storeConfig->getId())); } /** @@ -99,7 +128,7 @@ private function getBaseConfigData(StoreInterface $store) : array * @param int $storeId * @return array */ - private function getExtendedConfigData(int $storeId) + private function getExtendedConfigData(int $storeId): array { $extendedConfigData = []; foreach ($this->extendedConfigData as $key => $path) { diff --git a/app/code/Magento/StoreGraphQl/Plugin/LocalizeEmail.php b/app/code/Magento/StoreGraphQl/Plugin/LocalizeEmail.php new file mode 100644 index 0000000000000..f3d3924b15280 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Plugin/LocalizeEmail.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\StoreGraphQl\Plugin; + +use Magento\Framework\App\AreaInterface; +use Magento\Framework\App\AreaList; +use Magento\Framework\App\State; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Store\Model\App\Emulation; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Emulate the correct store when GraphQL is sending an email + */ +class LocalizeEmail +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Emulation + */ + private $emulation; + + /** + * @var AreaList + */ + private $areaList; + + /** + * @var State + */ + private $appState; + + /** + * @param StoreManagerInterface $storeManager + * @param Emulation $emulation + * @param AreaList $areaList + * @param State $appState + */ + public function __construct( + StoreManagerInterface $storeManager, + Emulation $emulation, + AreaList $areaList, + State $appState + ) { + $this->storeManager = $storeManager; + $this->emulation = $emulation; + $this->areaList = $areaList; + $this->appState = $appState; + } + + /** + * Emulate the correct store during email preparation + * + * @param TransportBuilder $subject + * @param \Closure $proceed + * @return mixed + * @throws NoSuchEntityException|LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundGetTransport(TransportBuilder $subject, \Closure $proceed) + { + // Load translations for the app + $area = $this->areaList->getArea($this->appState->getAreaCode()); + $area->load(AreaInterface::PART_TRANSLATE); + + $currentStore = $this->storeManager->getStore(); + $this->emulation->startEnvironmentEmulation($currentStore->getId()); + $output = $proceed(); + $this->emulation->stopEnvironmentEmulation(); + + return $output; + } +} diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml index f3771b704c3e9..e7f7da57aaebe 100644 --- a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml @@ -26,8 +26,11 @@ <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> <arguments> <argument name="extendedConfigData" xsi:type="array"> - <item name="store_name" xsi:type="string">store/information/name</item> + <item name="use_store_in_url" xsi:type="string">web/url/use_store</item> </argument> </arguments> </type> + <type name="Magento\Framework\Mail\Template\TransportBuilder"> + <plugin name="graphQlEmulateEmail" type="Magento\StoreGraphQl\Plugin\LocalizeEmail" /> + </type> </config> diff --git a/app/code/Magento/StoreGraphQl/etc/schema.graphqls b/app/code/Magento/StoreGraphQl/etc/schema.graphqls index 919c94684eb21..1106987cc72c1 100644 --- a/app/code/Magento/StoreGraphQl/etc/schema.graphqls +++ b/app/code/Magento/StoreGraphQl/etc/schema.graphqls @@ -2,6 +2,9 @@ # See COPYING.txt for license details. type Query { storeConfig : StoreConfig @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\StoreConfigResolver") @doc(description: "The store config query") @cache(cacheable: false) + availableStores( + useCurrentGroup: Boolean @doc(description: "Filter store views by current store group") + ): [StoreConfig] @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\AvailableStoresResolver") @doc(description: "Get a list of available store views and their config information.") } type Website @doc(description: "Website is deprecated because it is should not be used on storefront. The type contains information about a website") { @@ -31,4 +34,5 @@ type StoreConfig @doc(description: "The type contains information about a store secure_base_static_url : String @doc(description: "Secure base static URL for the store") secure_base_media_url : String @doc(description: "Secure base media URL for the store") store_name : String @doc(description: "Name of the store") + use_store_in_url: Boolean @doc(description: "The configuration determines if the store code should be used in the URL") } diff --git a/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php b/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php index f1bc6fcc105dc..f7167f6494312 100644 --- a/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php +++ b/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php @@ -14,6 +14,7 @@ * Swagger Schema Type. * * @api + * @since 100.2.4 */ interface SchemaTypeInterface extends ArgumentInterface { @@ -21,6 +22,7 @@ interface SchemaTypeInterface extends ArgumentInterface * Retrieve the available types of Swagger schema. * * @return string + * @since 100.2.4 */ public function getCode(); @@ -29,6 +31,7 @@ public function getCode(); * * @param string|null $store * @return string + * @since 100.2.4 */ public function getSchemaUrlPath($store = null); } diff --git a/app/code/Magento/Swagger/Block/Index.php b/app/code/Magento/Swagger/Block/Index.php index 549495190ef34..8eecfbb24935d 100644 --- a/app/code/Magento/Swagger/Block/Index.php +++ b/app/code/Magento/Swagger/Block/Index.php @@ -17,6 +17,7 @@ * @method SchemaTypeInterface[] getSchemaTypes() * @method bool hasSchemaTypes() * @method string getDefaultSchemaTypeCode() + * @since 100.2.1 */ class Index extends Template { @@ -53,6 +54,7 @@ private function getSchemaType() /** * @return string|null + * @since 100.2.1 */ public function getSchemaUrl() { diff --git a/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php b/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php index fc13372520945..9ba1083adab74 100644 --- a/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php +++ b/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php @@ -5,11 +5,17 @@ */ namespace Magento\Swatches\Block\LayeredNavigation; -use Magento\Eav\Model\Entity\Attribute; +use Magento\Catalog\Model\Layer\Filter\AbstractFilter; +use Magento\Catalog\Model\Layer\Filter\Item as FilterItem; use Magento\Catalog\Model\ResourceModel\Layer\Filter\AttributeFactory; -use Magento\Framework\View\Element\Template; +use Magento\Eav\Model\Entity\Attribute; use Magento\Eav\Model\Entity\Attribute\Option; -use Magento\Catalog\Model\Layer\Filter\Item as FilterItem; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Magento\Swatches\Helper\Data; +use Magento\Swatches\Helper\Media; +use Magento\Theme\Block\Html\Pager; /** * Class RenderLayered Render Swatches at Layered Navigation @@ -37,7 +43,7 @@ class RenderLayered extends Template protected $eavAttribute; /** - * @var \Magento\Catalog\Model\Layer\Filter\AbstractFilter + * @var AbstractFilter */ protected $filter; @@ -47,41 +53,52 @@ class RenderLayered extends Template protected $layerAttribute; /** - * @var \Magento\Swatches\Helper\Data + * @var Data */ protected $swatchHelper; /** - * @var \Magento\Swatches\Helper\Media + * @var Media */ protected $mediaHelper; /** - * @param Template\Context $context + * @var Pager + */ + private $htmlPagerBlock; + + /** + * @param Context $context * @param Attribute $eavAttribute * @param AttributeFactory $layerAttribute - * @param \Magento\Swatches\Helper\Data $swatchHelper - * @param \Magento\Swatches\Helper\Media $mediaHelper + * @param Data $swatchHelper + * @param Media $mediaHelper * @param array $data + * @param Pager|null $htmlPagerBlock */ public function __construct( - \Magento\Framework\View\Element\Template\Context $context, + Context $context, Attribute $eavAttribute, AttributeFactory $layerAttribute, - \Magento\Swatches\Helper\Data $swatchHelper, - \Magento\Swatches\Helper\Media $mediaHelper, - array $data = [] + Data $swatchHelper, + Media $mediaHelper, + array $data = [], + ?Pager $htmlPagerBlock = null ) { $this->eavAttribute = $eavAttribute; $this->layerAttribute = $layerAttribute; $this->swatchHelper = $swatchHelper; $this->mediaHelper = $mediaHelper; + $this->htmlPagerBlock = $htmlPagerBlock ?? ObjectManager::getInstance()->get(Pager::class); parent::__construct($context, $data); } /** + * Set filter and attribute objects + * * @param \Magento\Catalog\Model\Layer\Filter\AbstractFilter $filter + * * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ @@ -94,6 +111,8 @@ public function setSwatchFilter(\Magento\Catalog\Model\Layer\Filter\AbstractFilt } /** + * Get attribute swatch data + * * @return array */ public function getSwatchData() @@ -114,30 +133,46 @@ public function getSwatchData() $attributeOptionIds = array_keys($attributeOptions); $swatches = $this->swatchHelper->getSwatchesByOptionsId($attributeOptionIds); - $data = [ + return [ 'attribute_id' => $this->eavAttribute->getId(), 'attribute_code' => $this->eavAttribute->getAttributeCode(), 'attribute_label' => $this->eavAttribute->getStoreLabel(), 'options' => $attributeOptions, 'swatches' => $swatches, ]; - - return $data; } /** + * Build filter option url + * * @param string $attributeCode * @param int $optionId + * * @return string */ public function buildUrl($attributeCode, $optionId) { - $query = [$attributeCode => $optionId]; - return $this->_urlBuilder->getUrl('*/*/*', ['_current' => true, '_use_rewrite' => true, '_query' => $query]); + $query = [ + $attributeCode => $optionId, + // exclude current page from urls + $this->htmlPagerBlock->getPageVarName() => null + ]; + + return $this->_urlBuilder->getUrl( + '*/*/*', + [ + '_current' => true, + '_use_rewrite' => true, + '_query' => $query + ] + ); } /** + * Get view data for option with no results + * * @param Option $swatchOption + * * @return array */ protected function getUnusedOption(Option $swatchOption) @@ -150,8 +185,11 @@ protected function getUnusedOption(Option $swatchOption) } /** + * Get option data if visible + * * @param FilterItem[] $filterItems * @param Option $swatchOption + * * @return array */ protected function getFilterOption(array $filterItems, Option $swatchOption) @@ -166,8 +204,11 @@ protected function getFilterOption(array $filterItems, Option $swatchOption) } /** + * Get view data for option + * * @param FilterItem $filterItem * @param Option $swatchOption + * * @return array */ protected function getOptionViewData(FilterItem $filterItem, Option $swatchOption) @@ -187,15 +228,20 @@ protected function getOptionViewData(FilterItem $filterItem, Option $swatchOptio } /** + * Check if option should be visible + * * @param FilterItem $filterItem + * * @return bool */ protected function isOptionVisible(FilterItem $filterItem) { - return $this->isOptionDisabled($filterItem) && $this->isShowEmptyResults() ? false : true; + return !($this->isOptionDisabled($filterItem) && $this->isShowEmptyResults()); } /** + * Check if attribute values should be visible with no results + * * @return bool */ protected function isShowEmptyResults() @@ -204,7 +250,10 @@ protected function isShowEmptyResults() } /** + * Check if option should be disabled + * * @param FilterItem $filterItem + * * @return bool */ protected function isOptionDisabled(FilterItem $filterItem) @@ -213,8 +262,11 @@ protected function isOptionDisabled(FilterItem $filterItem) } /** + * Retrieve filter item by id + * * @param FilterItem[] $filterItems * @param integer $id + * * @return bool|FilterItem */ protected function getFilterItemById(array $filterItems, $id) @@ -228,14 +280,15 @@ protected function getFilterItemById(array $filterItems, $id) } /** + * Get swatch image path + * * @param string $type * @param string $filename + * * @return string */ public function getSwatchPath($type, $filename) { - $imagePath = $this->mediaHelper->getSwatchAttributeImage($type, $filename); - - return $imagePath; + return $this->mediaHelper->getSwatchAttributeImage($type, $filename); } } diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php index a2cae7f7b5a20..32df3daf4599b 100644 --- a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php +++ b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php @@ -81,7 +81,7 @@ class Configurable extends \Magento\ConfigurableProduct\Block\Product\View\Type\ /** * Indicate if product has one or more Swatch attributes * - * @deprecated 100.1.5 unused + * @deprecated 100.1.0 unused * * @var boolean */ @@ -250,7 +250,7 @@ protected function getSwatchAttributesData() /** * Init isProductHasSwatchAttribute. * - * @deprecated 100.1.5 Method isProductHasSwatchAttribute() is used instead of this. + * @deprecated 100.2.0 Method isProductHasSwatchAttribute() is used instead of this. * * @codeCoverageIgnore * @return void @@ -510,6 +510,7 @@ public function getIdentities() * Get Swatch image size config data. * * @return string + * @since 100.2.5 */ public function getJsonSwatchSizeConfig() { diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php index 6e0a1e8d01360..e9b813003a09b 100644 --- a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php +++ b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php @@ -158,6 +158,7 @@ public function getJsonConfig() * Composes configuration for js price format * * @return string + * @since 100.2.3 */ public function getPriceFormatJson() { @@ -168,6 +169,7 @@ public function getPriceFormatJson() * Composes configuration for js price * * @return string + * @since 100.2.3 */ public function getPricesJson() { diff --git a/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php b/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php index 9ad62265be21f..121d85ecc181d 100644 --- a/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php +++ b/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php @@ -47,6 +47,7 @@ public function saveDefaultSwatchOption($id, $defaultValue) * @param array $optionIDs * @param int $type * @throws \Magento\Framework\Exception\LocalizedException + * @since 100.2.4 */ public function clearSwatchOptionByOptionIdAndType($optionIDs, $type = null) { diff --git a/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php index 795c48f12ebcc..43a44534aa942 100644 --- a/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php +++ b/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php @@ -8,6 +8,9 @@ namespace Magento\Swatches\Plugin\Eav\Model\Entity\Attribute; use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Model\Product\Attribute\OptionManagement as CatalogOptionManagement; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Eav\Model\AttributeRepository; use Magento\Store\Model\Store; use Magento\Swatches\Helper\Data; @@ -41,28 +44,61 @@ public function __construct( /** * Add swatch value to the attribute option * - * @param \Magento\Catalog\Model\Product\Attribute\OptionManagement $subject + * @param CatalogOptionManagement $subject * @param string $attributeCode - * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @param AttributeOptionInterface $option * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeAdd( - \Magento\Catalog\Model\Product\Attribute\OptionManagement $subject, + CatalogOptionManagement $subject, ?string $attributeCode, - \Magento\Eav\Api\Data\AttributeOptionInterface $option + AttributeOptionInterface $option ) { - if (empty($attributeCode)) { + $attribute = $this->initAttribute($attributeCode); + if (!$attribute) { return; } - $attribute = $this->attributeRepository->get( - ProductAttributeInterface::ENTITY_TYPE_CODE, - $attributeCode - ); - if (!$attribute || !$this->swatchHelper->isSwatchAttribute($attribute)) { + + $optionId = $this->getNewOptionId($option); + $this->setSwatchAttributeOption($attribute, $option, $optionId); + } + + /** + * Update swatch value of attribute option + * + * @param CatalogOptionManagement $subject + * @param string $attributeCode + * @param int $optionId + * @param AttributeOptionInterface $option + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeUpdate( + CatalogOptionManagement $subject, + $attributeCode, + $optionId, + AttributeOptionInterface $option + ) { + $attribute = $this->initAttribute($attributeCode); + if (!$attribute) { return; } - $optionId = $this->getOptionId($option); - $optionsValue = $option->getValue(); + + $this->setSwatchAttributeOption($attribute, $option, (string)$optionId); + } + + /** + * Set attribute swatch option + * + * @param AttributeInterface $attribute + * @param AttributeOptionInterface $option + * @param string $optionId + */ + private function setSwatchAttributeOption( + AttributeInterface $attribute, + AttributeOptionInterface $option, + string $optionId + ): void { + $optionsValue = trim($option->getValue() ?: ''); if ($this->swatchHelper->isVisualSwatch($attribute)) { $attribute->setData('swatchvisual', ['value' => [$optionId => $optionsValue]]); } else { @@ -80,13 +116,40 @@ public function beforeAdd( } /** - * Returns option id + * Init swatch attribute * - * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @param string $attributeCode + * @return bool|AttributeInterface + */ + private function initAttribute($attributeCode) + { + if (empty($attributeCode)) { + return false; + } + $attribute = $this->attributeRepository->get( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeCode + ); + if (!$attribute || !$this->swatchHelper->isSwatchAttribute($attribute)) { + return false; + } + + return $attribute; + } + + /** + * Get option id to create new option + * + * @param AttributeOptionInterface $option * @return string */ - private function getOptionId(\Magento\Eav\Api\Data\AttributeOptionInterface $option) : string + private function getNewOptionId(AttributeOptionInterface $option): string { - return 'id_' . ($option->getValue() ?: 'new_option'); + $optionId = trim($option->getValue() ?: ''); + if (empty($optionId)) { + $optionId = 'new_option'; + } + + return 'id_' . $optionId; } } diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml index 97a391137d8e3..5f3ec07bd4983 100644 --- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml @@ -19,6 +19,7 @@ <argument name="option2" defaultValue="textSwatchOption2" type="string"/> <argument name="option3" defaultValue="textSwatchOption3" type="string"/> <argument name="usedInProductListing" defaultValue="No" type="string"/> + <argument name="usedInLayeredNavigation" defaultValue="No" type="string"/> </arguments> <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> @@ -41,6 +42,7 @@ <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" stepKey="waitForTabSwitch"/> <selectOption selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" userInput="{{usedInProductListing}}" stepKey="useInProductListing"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="{{usedInLayeredNavigation}}" stepKey="useInLayeredNavigation"/> <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSave"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup.xml new file mode 100644 index 0000000000000..cc1b8fc4249bf --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup.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"> + <!-- Add Configurable Product with Swatch attribute to the cart --> + <actionGroup name="StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup"> + <annotations> + <description>Select Product product option. Fills in the provided Product Quantity. Clicks on Add To Cart. Validates that the Success Message is present.</description> + </annotations> + <arguments> + <argument name="product"/> + <argument name="productOption" type="string"/> + <argument name="productQty" type="string" /> + </arguments> + + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <click selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel(productOption)}}" stepKey="clickSwatchOption"/> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="{{productQty}}" stepKey="fillProduct1Quantity"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="clickOnAddToCartButton"/> + <waitForPageLoad stepKey="waitForProductToAddInCart"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeSuccessSaveMessage"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index a4bff2227ffbb..811d3af735321 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -16,6 +16,7 @@ <element name="nthSwatchOptionText" type="button" selector="div.swatch-option.text:nth-of-type({{n}})" parameterized="true"/> <element name="productSwatch" type="button" selector="//div[@class='swatch-option'][@aria-label='{{var1}}']" parameterized="true"/> <element name="visualSwatchOption" type="button" selector=".swatch-option[data-option-tooltip-value='#{{visualSwatchOption}}']" parameterized="true"/> + <element name="visualSwatchOptionText" type="button" selector=".swatch-option.text[data-option-tooltip-value='{{visualSwatchOptionText}}']" parameterized="true"/> <element name="swatchOptionTooltip" type="block" selector="div.swatch-option-tooltip"/> <element name="swatchAttributeSelectedOption" type="text" selector="#product-options-wrapper .swatch-option.selected"/> </section> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckColorUploadChooserVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckColorUploadChooserVisualSwatchTest.xml index a4fc0bdcfd1fb..9833ee79a297a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckColorUploadChooserVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckColorUploadChooserVisualSwatchTest.xml @@ -19,7 +19,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="addNewProductAttribute"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="addNewProductAttribute"/> <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="{{visualSwatchAttribute.input_type}}" stepKey="fillInputType"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml index e67d0c763308c..a972456e22ac5 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml @@ -35,8 +35,7 @@ </after> <!-- Begin creating a new product attribute of type "Image Swatch" --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> <!-- Select visual swatch --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml index 6b2a29d8ec451..683251b88a8bd 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml @@ -25,8 +25,7 @@ </after> <!-- Create a new product attribute of type "Text Swatch" --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="swatch_text" stepKey="selectInputType"/> <click selector="{{AdminManageSwatchSection.addSwatchText}}" stepKey="clickAddSwatch0"/> @@ -51,8 +50,7 @@ <seeInField selector="{{AdminManageSwatchSection.nthSwatchAdminDescription('3')}}" userInput="Something blue." stepKey="seeDescription2"/> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="waitForProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateConfigurableProduct"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml index 1a6c0341c0704..d59800c2bc0cd 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml @@ -24,8 +24,7 @@ </before> <after> <!-- Clean up our modifications to the existing color attribute --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> @@ -39,8 +38,7 @@ </after> <!-- Go to the edit page for the "color" attribute --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> @@ -102,8 +100,7 @@ </actionGroup> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml index e93a27d377a52..0d58ba8fc9917 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="navigateToNewProductAttributePage"/> <!-- Set attribute properties --> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml index 8fd21acbd51d9..b48f181c8d199 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -26,8 +26,7 @@ </before> <after> <!-- Clean up our modifications to the existing color attribute --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> @@ -44,12 +43,13 @@ <!-- Enable swatch tooltips --> <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 1" stepKey="disableTooltips"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterEnabling"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnabling"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Go to the edit page for the "color" attribute --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> @@ -83,8 +83,7 @@ </actionGroup> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> @@ -149,7 +148,9 @@ <!-- Disable swatch tooltips --> <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 0" stepKey="disableTooltips"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterDisabling"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisabling"> + <argument name="tags" value=""/> + </actionGroup> <!-- Verify swatch tooltips are not visible --> <reloadPage stepKey="refreshPage"/> 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/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchUpdateCartItemTierPriceTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchUpdateCartItemTierPriceTest.xml new file mode 100644 index 0000000000000..e89d3157e4624 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchUpdateCartItemTierPriceTest.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontConfigurableProductSwatchUpdateCartItemTierPriceTest"> + <annotations> + <features value="Swatches"/> + <stories value="Configurable product with swatch attribute"/> + <title value="Swatch option should show the tier price on product page when Cart Item edited."/> + <description value="Configurable product with swatch attribute should show the tier price on product page when added Cart Item."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-36047"/> + <group value="Swatches"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createConfigurableProduct" stepKey="deleteConfigurableProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteColorAttribute"> + <argument name="ProductAttribute" value="ProductColorAttribute"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <actionGroup ref="AddTextSwatchToProductActionGroup" stepKey="addColorAttribute"> + <argument name="attributeName" value="{{ProductColorAttribute.frontend_label}}"/> + <argument name="attributeCode" value="{{ProductColorAttribute.attribute_code}}"/> + <argument name="option1" value="Black"/> + <argument name="option2" value="White"/> + <argument name="option3" value="Blue"/> + </actionGroup> + + <amOnPage url="{{AdminProductEditPage.url($createConfigurableProduct.id$)}}" stepKey="goToConfigurableProduct"/> + + <actionGroup ref="GenerateConfigurationsByAttributeCodeActionGroup" stepKey="createProductConfigurations"> + <argument name="attributeCode" value="{{ProductColorAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveConfigurableProductAddToCurrentAttributeSetActionGroup" stepKey="saveConfigurableProduct"/> + + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="$$createConfigurableProduct.sku$$-White"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricingActionGroup" stepKey="addTierPriceToSimpleProduct"> + <argument name="group" value="ALL GROUPS"/> + <argument name="quantity" value="5"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="50"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openConfigurableProductPage"> + <argument name="productUrl" value="$createConfigurableProduct.custom_attributes[url_key]$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForConfigurableProductPage"/> + + <actionGroup ref="StorefrontSelectSwatchOptionOnProductPageActionGroup" stepKey="selectWhiteOption"> + <argument name="optionName" value="White"/> + </actionGroup> + + <actionGroup ref="AssertStorefrontProductDetailPageTierPriceActionGroup" stepKey="assertProductTierPriceText"> + <argument name="tierProductPriceDiscountQuantity" value="5"/> + <argument name="productPriceWithAppliedTierPriceDiscount" value="61.50"/> + <argument name="productSavedPricePercent" value="50"/> + </actionGroup> + + <actionGroup ref="StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup" stepKey="addConfigurableProductToTheCart"> + <argument name="productQty" value="1"/> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="productOption" value="Blue"/> + </actionGroup> + + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> + + <actionGroup ref="StorefrontUpdateCartItemEditParametersProductActionGroup" stepKey="updateCartItem"> + <argument name="rowNumber" value="1"/> + </actionGroup> + + <actionGroup ref="StorefrontSelectSwatchOptionOnProductPageActionGroup" stepKey="selectWhiteOption2"> + <argument name="optionName" value="White"/> + </actionGroup> + + <actionGroup ref="AssertStorefrontProductDetailPageTierPriceActionGroup" stepKey="assertProductTierPriceText2"> + <argument name="tierProductPriceDiscountQuantity" value="5"/> + <argument name="productPriceWithAppliedTierPriceDiscount" value="61.50"/> + <argument name="productSavedPricePercent" value="50"/> + </actionGroup> + + <actionGroup ref="StorefrontSelectSwatchOptionOnProductPageActionGroup" stepKey="selectWhiteOption3"> + <argument name="optionName" value="Blue"/> + </actionGroup> + + <dontSee selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="dontSeeTierPriceForOption"/> + + <actionGroup ref="StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup" stepKey="addUpdatedConfigurableProductToTheCart"> + <argument name="productQty" value="10"/> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="productOption" value="White"/> + </actionGroup> + + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage2"/> + + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml index 0999b43c48820..b661ecb338bde 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml @@ -30,7 +30,9 @@ <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('3')}}" userInput="123456789012345678901BrownD" stepKey="fillDescription3" after="fillSwatch3"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <see selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '3')}}" userInput="123456789012345678901" stepKey="seeGreen" after="seeBlue"/> <see selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '4')}}" userInput="123456789012345678901" stepKey="seeBrown" after="seeGreen"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml index 2ca26d84d45c7..4ed8824d9e39b 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml @@ -34,8 +34,7 @@ </after> <!-- Begin creating a new product attribute --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> <!-- Select visual swatch --> @@ -54,8 +53,8 @@ </actionGroup> <click selector="{{AdminManageSwatchSection.nthUploadFile('1')}}" stepKey="clickUploadFile1"/> <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1"/> + <waitForPageLoad stepKey="waitFileAttached1"/> <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('0')}}" userInput="adobe-thumb" stepKey="fillAdmin1"/> - <click selector="{{AdminManageSwatchSection.swatchWindow('0')}}" stepKey="clicksWatchWindow1"/> <!-- Set swatch #2 image using the file upload --> <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch2"/> @@ -64,6 +63,7 @@ </actionGroup> <click selector="{{AdminManageSwatchSection.nthUploadFile('2')}}" stepKey="clickUploadFile2"/> <attachFile selector="input[name='datafile']" userInput="adobe-small.jpg" stepKey="attachFile2"/> + <waitForPageLoad stepKey="waitFileAttached2"/> <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('1')}}" userInput="adobe-small" stepKey="fillAdmin2"/> <!-- Set scope to global --> @@ -80,8 +80,7 @@ <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="waitForProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -105,7 +104,9 @@ </actionGroup> <!-- Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml index 82dbff950d62f..4a78b4380fb68 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml @@ -32,8 +32,7 @@ </after> <!-- Begin creating a new product attribute --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> <!-- Select text swatch --> @@ -67,8 +66,7 @@ <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateConfigurableProduct"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> @@ -83,7 +81,9 @@ </actionGroup> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml index bf820863cf9b6..2a986463a3d14 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml @@ -34,8 +34,7 @@ </after> <!-- Begin creating a new product attribute --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> <!-- Select visual swatch --> @@ -79,8 +78,7 @@ <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="waitForProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateConfigurableProduct"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> @@ -95,7 +93,9 @@ </actionGroup> <!-- Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml index 551a91f47c165..734294ba977ba 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml @@ -71,8 +71,12 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductForm"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Select any option in the Layered navigation and verify product image--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml new file mode 100644 index 0000000000000..c6266e034bffc --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontRedirectToFirstPageOnFilteringBySwatchTest"> + <annotations> + <features value="Swatches"/> + <stories value="Filter by swatch attribute on plp layered navigation"/> + <title value="Customers are redirected to first plp page after filtering by swatch"/> + <description value="Customers are redirected to first plp page after filtering by swatch"/> + <severity value="MINOR"/> + <group value="Swatches"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProduct3"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <magentoCLI command="config:set catalog/frontend/grid_per_page 1" stepKey="setOneProductPerPage"/> + <magentoCLI command="config:set catalog/frontend/grid_per_page_values 1" stepKey="setGridPerPage"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AddTextSwatchToProductActionGroup" stepKey="addSwatchAttribute"> + <argument name="usedInLayeredNavigation" value="1"/> + </actionGroup> + </before> + + <after> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteSwatchAttribute"> + <argument name="ProductAttribute" value="textSwatchAttribute"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + + <magentoCLI command="config:set catalog/frontend/grid_per_page 12" stepKey="setDefaultProductsPerPage"/> + <magentoCLI command="config:set catalog/frontend/grid_per_page_values 12,24,36" stepKey="setDefaultGridPerPage"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createSimpleProduct3" stepKey="deleteSimpleProduct3"/> + </after> + + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/{{AddToDefaultSet.attributeSetId}}/" stepKey="onAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{textSwatchAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="SaveAttributeSet"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndexPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFiltersOnProductIndexPage"/> + + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="goToProduct1EditPage"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption1" stepKey="selectProduct1AttributeOption"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct1"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductsGridPage2"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="goToProduct2EditPage"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption1" stepKey="selectProduct2AttributeOption"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct2"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductsGridPage3"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="goToProduct3EditPage"> + <argument name="product" value="$$createSimpleProduct3$$"/> + </actionGroup> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption2" stepKey="selectProduct3AttributeOption"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct3"/> + + <magentoCron groups="index" stepKey="runCronIndexer"/> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="StorefrontNavigateCategoryNextPageActionGroup" stepKey="navigateToCategoryNextPage"/> + + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle(textSwatchAttribute.default_label)}}" stepKey="expandAttribute"/> + <click selector="{{StorefrontCategorySidebarSection.attributeNthOption(textSwatchAttribute.attribute_code, '1')}}" stepKey="filterBySwatch1"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + + <actionGroup ref="AssertStorefrontCategoryCurrentPageIsNthActionGroup" stepKey="assertCurrentPageIsFirst"> + <argument name="expectedPage" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml index 43944ceef33ef..27cbb01eafff0 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml @@ -38,7 +38,7 @@ </after> <!-- Begin creating a new product attribute --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <actionGroup ref="AdminFillProductAttributePropertiesActionGroup" stepKey="fillProductAttributeProperties"> <argument name="attributeName" value="{{VisualSwatchProductAttribute.attribute_code}}"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml index 5ae53858374e7..77a16e639c8a4 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml @@ -33,8 +33,7 @@ <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteAttribute"> <argument name="productAttributeLabel" value="{{visualSwatchAttribute.default_label}}"/> @@ -55,8 +54,7 @@ <!--Login--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdmin"/> <!--Create a configurable swatch product via the UI --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProductPage"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml index 1b77e773ef283..0e25a14d6c170 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml @@ -32,8 +32,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Create a configurable swatch product via the UI --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProductPage"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> @@ -45,7 +44,7 @@ <actionGroup ref="AddVisualSwatchToProductActionGroup" stepKey="addSwatchToProduct"/> <!--Add custom option to configurable product--> <actionGroup ref="AddProductCustomOptionFileActionGroup" stepKey="addCustomOptionToProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!--Go to storefront--> <amOnPage url="" stepKey="goToHomePage"/> diff --git a/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php b/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php index 4056bf27f571e..06960c409b476 100644 --- a/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php @@ -18,6 +18,7 @@ use Magento\Swatches\Block\LayeredNavigation\RenderLayered; use Magento\Swatches\Helper\Data; use Magento\Swatches\Helper\Media; +use Magento\Theme\Block\Html\Pager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -28,35 +29,60 @@ */ class RenderLayeredTest extends TestCase { - /** @var MockObject */ - protected $contextMock; - - /** @var MockObject */ - protected $requestMock; - - /** @var MockObject */ - protected $urlBuilder; - - /** @var MockObject */ - protected $eavAttributeMock; - - /** @var MockObject */ - protected $layerAttributeFactoryMock; - - /** @var MockObject */ - protected $layerAttributeMock; - - /** @var MockObject */ - protected $swatchHelperMock; - - /** @var MockObject */ - protected $mediaHelperMock; - - /** @var MockObject */ - protected $filterMock; - - /** @var MockObject */ - protected $block; + /** + * @var RenderLayered|MockObject + */ + private $block; + + /** + * @var Context|MockObject + */ + private $contextMock; + + /** + * @var RequestInterface|MockObject + */ + private $requestMock; + + /** + * @var Url|MockObject + */ + private $urlBuilder; + + /** + * @var Attribute|MockObject + */ + private $eavAttributeMock; + + /** + * @var AttributeFactory|MockObject + */ + private $layerAttributeFactoryMock; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Layer\Filter\Attribute|MockObject + */ + private $layerAttributeMock; + + /** + * @var Data|MockObject + */ + private $swatchHelperMock; + + /** + * @var Media|MockObject + */ + private $mediaHelperMock; + + /** + * @var AbstractFilter|MockObject + */ + private $filterMock; + + /** + * @var Pager|MockObject + */ + private $htmlBlockPagerMock; protected function setUp(): void { @@ -66,8 +92,8 @@ protected function setUp(): void Url::class, ['getCurrentUrl', 'getRedirectUrl', 'getUrl'] ); - $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilder); + $this->contextMock->method('getRequest')->willReturn($this->requestMock); + $this->contextMock->method('getUrlBuilder')->willReturn($this->urlBuilder); $this->eavAttributeMock = $this->createMock(Attribute::class); $this->layerAttributeFactoryMock = $this->createPartialMock( AttributeFactory::class, @@ -80,6 +106,7 @@ protected function setUp(): void $this->swatchHelperMock = $this->createMock(Data::class); $this->mediaHelperMock = $this->createMock(Media::class); $this->filterMock = $this->createMock(AbstractFilter::class); + $this->htmlBlockPagerMock = $this->createMock(Pager::class); $this->block = $this->getMockBuilder(RenderLayered::class) ->setMethods(['filter', 'eavAttribute']) @@ -91,6 +118,7 @@ protected function setUp(): void $this->swatchHelperMock, $this->mediaHelperMock, [], + $this->htmlBlockPagerMock ] ) ->getMock(); @@ -114,7 +142,7 @@ public function testGetSwatchData() $item3 = $this->createMock(Item::class); $item4 = $this->createMock(Item::class); - $item1->expects($this->any())->method('__call')->withConsecutive( + $item1->method('__call')->withConsecutive( ['getValue'], ['getCount'], ['getValue'], @@ -128,9 +156,9 @@ public function testGetSwatchData() 'Yellow' ); - $item2->expects($this->any())->method('__call')->with('getValue')->willReturn('blue'); + $item2->method('__call')->with('getValue')->willReturn('blue'); - $item3->expects($this->any())->method('__call')->withConsecutive( + $item3->method('__call')->withConsecutive( ['getValue'], ['getCount'] )->willReturnOnConsecutiveCalls( @@ -138,7 +166,7 @@ public function testGetSwatchData() 0 ); - $item4->expects($this->any())->method('__call')->withConsecutive( + $item4->method('__call')->withConsecutive( ['getValue'], ['getCount'], ['getValue'], @@ -162,22 +190,22 @@ public function testGetSwatchData() $this->block->method('filter')->willReturn($this->filterMock); $option1 = $this->createMock(Option::class); - $option1->expects($this->any())->method('getValue')->willReturn('yellow'); + $option1->method('getValue')->willReturn('yellow'); $option2 = $this->createMock(Option::class); - $option2->expects($this->any())->method('getValue')->willReturn(null); + $option2->method('getValue')->willReturn(null); $option3 = $this->createMock(Option::class); - $option3->expects($this->any())->method('getValue')->willReturn('red'); + $option3->method('getValue')->willReturn('red'); $option4 = $this->createMock(Option::class); - $option4->expects($this->any())->method('getValue')->willReturn('green'); + $option4->method('getValue')->willReturn('green'); $eavAttribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $eavAttribute->expects($this->once()) ->method('getOptions') ->willReturn([$option1, $option2, $option3, $option4]); - $eavAttribute->expects($this->any())->method('getIsFilterable')->willReturn(0); + $eavAttribute->method('getIsFilterable')->willReturn(0); $this->filterMock->expects($this->once())->method('getAttributeModel')->willReturn($eavAttribute); $this->block->method('eavAttribute')->willReturn($eavAttribute); @@ -200,7 +228,7 @@ public function testGetSwatchDataException() { $this->block->method('filter')->willReturn($this->filterMock); $this->block->setSwatchFilter($this->filterMock); - $this->expectException('\RuntimeException'); + $this->expectException(\RuntimeException::class); $this->block->getSwatchData(); } diff --git a/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js index ad5926d451e88..84389083447ae 100644 --- a/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js @@ -311,6 +311,7 @@ define([ if ($(this.element).attr('data-rendered')) { return; } + $(this.element).attr('data-rendered', true); if (_.isEmpty(this.options.jsonConfig.images)) { @@ -320,6 +321,8 @@ define([ this._debouncedLoadProductMedia = _.debounce(this._LoadProductMedia.bind(this), 500); } + this.options.tierPriceTemplate = $(this.options.tierPriceTemplateSelector).html(); + if (this.options.jsonConfig !== '' && this.options.jsonSwatchConfig !== '') { // store unsorted attributes this.options.jsonConfig.mappedAttributes = _.clone(this.options.jsonConfig.attributes); @@ -330,7 +333,6 @@ define([ } else { console.log('SwatchRenderer: No input data received'); } - this.options.tierPriceTemplate = $(this.options.tierPriceTemplateSelector).html(); }, /** diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml index 70eb51651f663..bae3820042de0 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml @@ -8,6 +8,8 @@ // phpcs:disable Generic.WhiteSpace.ScopeIndent /** @var $block \Magento\Swatches\Block\LayeredNavigation\RenderLayered */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +/** @var \Magento\Framework\Escaper $escaper */ ?> <?php $swatchData = $block->getSwatchData(); ?> <div class="swatch-attribute swatch-layered <?= $block->escapeHtmlAttr($swatchData['attribute_code']) ?>" @@ -49,11 +51,18 @@ data-option-id="<?= $block->escapeHtmlAttr($option) ?>" data-option-label="<?= $block->escapeHtmlAttr($label['label']) ?>" data-option-tooltip-thumb="<?= $block->escapeUrl($swatchThumbPath) ?>" - data-option-tooltip-value="" - style="background: url(<?= - /* @noEscape */ $escapedUrl - ?>) no-repeat center; background-size: initial;"> + data-option-tooltip-value=""> </div> + <?php + $element = 'swatchImageOption' .$escaper->escapeJs($option); + $script = 'var ' .$element + .' = document.querySelector(\'div[data-option-id="' .$escaper->escapeJs($option) + .'"]\');' .PHP_EOL; + $script .= $element .'.style.background = "url(\'' + .$escapedUrl .'\') no-repeat center";' .PHP_EOL; + $script .= $element .'.style.backgroundSize = "initial";'; + ?> + <?= /* @noEscape*/ $secureRenderer->renderTag('script', [], $script, false); ?> <?php break; case '1': ?> @@ -65,11 +74,21 @@ data-option-tooltip-thumb="" data-option-tooltip-value="<?= $block->escapeHtmlAttr( $swatchData['swatches'][$option]['value'] - ) ?>" - style="background: <?= $block->escapeHtmlAttr( - $swatchData['swatches'][$option]['value'] - ) ?> no-repeat center; background-size: initial;"> + ) ?>"> </div> + <?php + $element = 'swatchImageOption' .$escaper->escapeJs($option); + $backgroundValue = $escaper->escapeJs( + str_replace('\'', '\\\'', $swatchData['swatches'][$option]['value']) + ); + $script = 'var ' .$element + .' = document.querySelector(\'div[data-option-id="' .$escaper->escapeJs($option) + .'"]\');' .PHP_EOL; + $script .= $element .'.style.background = "' .$backgroundValue + .' no-repeat center";' .PHP_EOL; + $script .= $element .'.style.backgroundSize = "initial";'; + ?> + <?= /* @noEscape*/ $secureRenderer->renderTag('script', [], $script, false); ?> <?php break; case '0': default: @@ -89,11 +108,14 @@ <?php endforeach; ?> </div> </div> +<?php $scriptString = <<<script -<script> require(["jquery", "Magento_Swatches/js/swatch-renderer"], function ($) { - $('.swatch-layered.<?= $block->escapeJs($swatchData['attribute_code']) ?>') + $('.swatch-layered.{$block->escapeJs($swatchData['attribute_code'])}') .find('[data-option-type="1"], [data-option-type="2"], [data-option-type="0"], [data-option-type="3"]') .SwatchRendererTooltip(); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml index 5838ba9625c6a..5a051d8ecf675 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml @@ -3,43 +3,50 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Catalog\Model\Product; +use Magento\Swatches\Block\Product\Renderer\Listing\Configurable; +use Magento\Swatches\ViewModel\Product\Renderer\Configurable as ConfigurableViewModel; + +/** @var Configurable $block */ +/** @var Product $product */ +$product = $block->getProduct() ?> -<?php -/** @var $block \Magento\Swatches\Block\Product\Renderer\Listing\Configurable */ -$productId = $block->getProduct()->getId(); -/** @var \Magento\Swatches\ViewModel\Product\Renderer\Configurable $configurableViewModel */ -$configurableViewModel = $block->getConfigurableViewModel() -?> -<div class="swatch-opt-<?= $block->escapeHtmlAttr($productId) ?>" - data-role="swatch-option-<?= $block->escapeHtmlAttr($productId) ?>"></div> +<?php if ($product && $product->isAvailable()): ?> + <?php $productId = $product->getId() ?> + <?php /** @var ConfigurableViewModel $configurableViewModel */ ?> + <?php $configurableViewModel = $block->getConfigurableViewModel() ?> + <div class="swatch-opt-<?= $block->escapeHtmlAttr($productId) ?>" + data-role="swatch-option-<?= $block->escapeHtmlAttr($productId) ?>"></div> -<script type="text/x-magento-init"> - { - "[data-role=swatch-option-<?= $block->escapeJs($productId) ?>]": { - "Magento_Swatches/js/swatch-renderer": { - "selectorProduct": ".product-item-details", - "onlySwatches": true, - "enableControlLabel": false, - "numberToShow": <?= $block->escapeJs($block->getNumberSwatchesPerProduct()) ?>, - "jsonConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>, - "jsonSwatchConfig": <?= /* @noEscape */ $block->getJsonSwatchConfig() ?>, - "mediaCallback": "<?= $block->escapeJs($block->escapeUrl($block->getMediaCallback())) ?>", - "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?>, - "showTooltip": <?= $block->escapeJs($configurableViewModel->getShowSwatchTooltip()) ?> + <script type="text/x-magento-init"> + { + "[data-role=swatch-option-<?= $block->escapeJs($productId) ?>]": { + "Magento_Swatches/js/swatch-renderer": { + "selectorProduct": ".product-item-details", + "onlySwatches": true, + "enableControlLabel": false, + "numberToShow": <?= $block->escapeJs($block->getNumberSwatchesPerProduct()) ?>, + "jsonConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>, + "jsonSwatchConfig": <?= /* @noEscape */ $block->getJsonSwatchConfig() ?>, + "mediaCallback": "<?= $block->escapeJs($block->escapeUrl($block->getMediaCallback())) ?>", + "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?>, + "showTooltip": <?= $block->escapeJs($configurableViewModel->getShowSwatchTooltip()) ?> + } } } - } -</script> + </script> -<script type="text/x-magento-init"> - { - "[data-role=priceBox][data-price-box=product-id-<?= $block->escapeJs($productId) ?>]": { - "priceBox": { - "priceConfig": { - "priceFormat": <?= /* @noEscape */ $block->getPriceFormatJson(); ?>, - "prices": <?= /* @noEscape */ $block->getPricesJson(); ?> + <script type="text/x-magento-init"> + { + "[data-role=priceBox][data-price-box=product-id-<?= $block->escapeJs($productId) ?>]": { + "priceBox": { + "priceConfig": { + "priceFormat": <?= /* @noEscape */ $block->getPriceFormatJson() ?>, + "prices": <?= /* @noEscape */ $block->getPricesJson() ?> + } } } } - } -</script> + </script> +<?php endif; ?> diff --git a/app/code/Magento/SwatchesGraphQl/composer.json b/app/code/Magento/SwatchesGraphQl/composer.json index 383575302e6ae..1b98b4044a2ff 100644 --- a/app/code/Magento/SwatchesGraphQl/composer.json +++ b/app/code/Magento/SwatchesGraphQl/composer.json @@ -6,9 +6,7 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-swatches": "*", - "magento/module-catalog": "*" - }, - "suggest": { + "magento/module-catalog": "*", "magento/module-catalog-graph-ql": "*" }, "license": [ diff --git a/app/code/Magento/Tax/Block/Adminhtml/Frontend/Region/Updater.php b/app/code/Magento/Tax/Block/Adminhtml/Frontend/Region/Updater.php index 7b66c4fd964c6..ae5da9e15cf53 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Frontend/Region/Updater.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Frontend/Region/Updater.php @@ -5,7 +5,9 @@ */ namespace Magento\Tax\Block\Adminhtml\Frontend\Region; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class Updater extends \Magento\Config\Block\System\Config\Form\Field { @@ -14,21 +16,31 @@ class Updater extends \Magento\Config\Block\System\Config\Form\Field */ protected $_directoryHelper; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Directory\Helper\Data $directoryHelper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Directory\Helper\Data $directoryHelper, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_directoryHelper = $directoryHelper; parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** + * Return element html. + * * @param AbstractElement $element * @return string */ @@ -36,8 +48,7 @@ protected function _getElementHtml(AbstractElement $element) { $html = parent::_getElementHtml($element); - $js = '<script> - require(["prototype", "mage/adminhtml/form"], function(){ + $js = 'require(["prototype", "mage/adminhtml/form"], function(){ updater = new RegionUpdater("tax_defaults_country", "none", "tax_defaults_region", %s, "nullify"); if(updater.lastCountryId) { var tmpRegionId = $("tax_defaults_region").value; @@ -49,10 +60,12 @@ protected function _getElementHtml(AbstractElement $element) } else { updater.update(); } - }); - </script>'; + });'; + + $scriptString = sprintf($js, $this->_directoryHelper->getRegionJson()); + + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); - $html .= sprintf($js, $this->_directoryHelper->getRegionJson()); return $html; } } diff --git a/app/code/Magento/Tax/Block/Adminhtml/Items/Price/Renderer.php b/app/code/Magento/Tax/Block/Adminhtml/Items/Price/Renderer.php index 376adba63db62..a1f538e0b0c70 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Items/Price/Renderer.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Items/Price/Renderer.php @@ -22,7 +22,7 @@ class Renderer extends \Magento\Backend\Block\Template { /** * @var \Magento\Tax\Helper\Data - * @deprecated + * @deprecated 100.3.0 * Marked as deprecated as it is unused. */ protected $taxHelper; diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php index 1884b247e530a..7ec16fd7f5373 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php @@ -13,6 +13,8 @@ namespace Magento\Tax\Block\Adminhtml\Rate; +use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Tax\Controller\RegistryConstants; @@ -86,6 +88,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Tax\Model\TaxRateCollection $taxRateCollection * @param \Magento\Tax\Model\Calculation\Rate\Converter $taxRateConverter * @param array $data + * @param DirectoryHelper|null $directoryHelper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -99,7 +102,8 @@ public function __construct( \Magento\Tax\Api\TaxRateRepositoryInterface $taxRateRepository, \Magento\Tax\Model\TaxRateCollection $taxRateCollection, \Magento\Tax\Model\Calculation\Rate\Converter $taxRateConverter, - array $data = [] + array $data = [], + ?DirectoryHelper $directoryHelper = null ) { $this->_regionFactory = $regionFactory; $this->_country = $country; @@ -108,6 +112,7 @@ public function __construct( $this->_taxRateRepository = $taxRateRepository; $this->_taxRateCollection = $taxRateCollection; $this->_taxRateConverter = $taxRateConverter; + $data['directoryHelper'] = $directoryHelper ?? ObjectManager::getInstance()->get(DirectoryHelper::class); parent::__construct($context, $registry, $formFactory, $data); } diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php index 87e9d9e006064..8ba846dc710b2 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php @@ -133,7 +133,7 @@ protected function _prepareLayout() ) . '\', \'' . $this->getUrl( 'tax/*/delete', ['rate' => $rate] - ) . '\')', + ) . '\', {data: {}})', 'class' => 'delete' ] ); diff --git a/app/code/Magento/Tax/Block/Checkout/Tax.php b/app/code/Magento/Tax/Block/Checkout/Tax.php index 0a86c0312ab1c..a53db42be2ad6 100644 --- a/app/code/Magento/Tax/Block/Checkout/Tax.php +++ b/app/code/Magento/Tax/Block/Checkout/Tax.php @@ -9,8 +9,48 @@ */ namespace Magento\Tax\Block\Checkout; +use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Sales\Model\ConfigInterface; + +/** + * Class for manage tax amount. + */ class Tax extends \Magento\Checkout\Block\Total\DefaultTotal { + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Checkout\Model\Session $checkoutSession + * @param ConfigInterface $salesConfig + * @param array $layoutProcessors + * @param array $data + * @param CheckoutHelper|null $checkoutHelper + * @param TaxHelper|null $taxHelper + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Customer\Model\Session $customerSession, + \Magento\Checkout\Model\Session $checkoutSession, + ConfigInterface $salesConfig, + array $layoutProcessors = [], + array $data = [], + ?CheckoutHelper $checkoutHelper = null, + ?TaxHelper $taxHelper = null + ) { + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); + parent::__construct( + $context, + $customerSession, + $checkoutSession, + $salesConfig, + $layoutProcessors, + $data, + $checkoutHelper + ); + } + /** * @var string */ diff --git a/app/code/Magento/Tax/Controller/Adminhtml/Rate/Delete.php b/app/code/Magento/Tax/Controller/Adminhtml/Rate/Delete.php index 1c5013c34c6e9..a77216cd3b46a 100644 --- a/app/code/Magento/Tax/Controller/Adminhtml/Rate/Delete.php +++ b/app/code/Magento/Tax/Controller/Adminhtml/Rate/Delete.php @@ -8,8 +8,9 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\App\Action\HttpPostActionInterface; -class Delete extends \Magento\Tax\Controller\Adminhtml\Rate +class Delete extends \Magento\Tax\Controller\Adminhtml\Rate implements HttpPostActionInterface { /** * Delete Rate and Data diff --git a/app/code/Magento/Tax/Model/System/Message/Notifications.php b/app/code/Magento/Tax/Model/System/Message/Notifications.php index ca59ab9eec3bf..163054d9e9394 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notifications.php +++ b/app/code/Magento/Tax/Model/System/Message/Notifications.php @@ -16,7 +16,7 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface * Store manager object * * @var \Magento\Store\Model\StoreManagerInterface - * @deprecated 100.1.3 + * @deprecated 100.1.0 */ protected $storeManager; @@ -36,7 +36,7 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface * Stores with invalid display settings * * @var array - * @deprecated 100.1.3 + * @deprecated 100.1.0 * @see \Magento\Tax\Model\System\Message\Notification\RoundingErrors */ protected $storesWithInvalidDisplaySettings; @@ -45,7 +45,7 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface * Websites with invalid discount settings * * @var array - * @deprecated 100.1.3 + * @deprecated 100.1.0 * @see \Magento\Tax\Model\System\Message\Notification\DiscountErrors */ protected $storesWithInvalidDiscountSettings; 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/ActionGroup/AdminClickAddTaxRuleButtonActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminClickAddTaxRuleButtonActionGroup.xml new file mode 100644 index 0000000000000..ea5c4cb74d19e --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminClickAddTaxRuleButtonActionGroup.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="AdminClickAddTaxRuleButtonActionGroup"> + <annotations> + <description>Click button for creating new tax rule.</description> + </annotations> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> + <waitForPageLoad stepKey="waitForTaxRuleGridLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminDeleteTaxRateActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminDeleteTaxRateActionGroup.xml new file mode 100644 index 0000000000000..1aab6ea2c4eec --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminDeleteTaxRateActionGroup.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="AdminDeleteTaxRateActionGroup"> + <annotations> + <description>Delete Tax Rate.</description> + </annotations> + <click selector="{{AdminMainActionsSection.delete}}" stepKey="clickDeleteRate"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickOk"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminFilterTaxRateByCodeActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminFilterTaxRateByCodeActionGroup.xml new file mode 100644 index 0000000000000..2b110e969b113 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminFilterTaxRateByCodeActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFilterTaxRateByCodeActionGroup"> + <annotations> + <description>Filter Tax Rates by tax rate code.</description> + </annotations> + <arguments> + <argument name="taxRateCode" type="string"/> + </arguments> + + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCode}}" stepKey="fillNameFilter"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRateGridOpenPageActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRateGridOpenPageActionGroup.xml new file mode 100644 index 0000000000000..58762e4fa02ef --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRateGridOpenPageActionGroup.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="AdminTaxRateGridOpenPageActionGroup"> + <annotations> + <description>Go to tax rate grid page.</description> + </annotations> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatePage"/> + <waitForPageLoad stepKey="waitForTaxRatePage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRuleGridOpenPageActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRuleGridOpenPageActionGroup.xml new file mode 100644 index 0000000000000..768dcd6cb42a8 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRuleGridOpenPageActionGroup.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="AdminTaxRuleGridOpenPageActionGroup"> + <annotations> + <description>Go to tax rule grid page.</description> + </annotations> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleGridPage"/> + <waitForPageLoad stepKey="waitForTaxRulePage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml index fc4b6dd8b84c5..c873e14797470 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml @@ -50,7 +50,7 @@ <!-- Reset admin order filter --> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="{{defaultTaxRule.code}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml index 01e1677ec8d8a..84278468a0590 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml @@ -106,7 +106,7 @@ <deleteData createDataKey="createSecondProductTaxClass" stepKey="deleteSecondProductTaxClass"/> <!-- Clear filter Product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilterProduct"/> <!-- Delete Customer and clear filter --> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml index 3a5f905d89dd5..5e7ce53a3a3fc 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml @@ -29,10 +29,8 @@ <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAddNewTaxRuleButton"/> <!-- Create a tax rule with defaults --> <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$initialTaxRate.code$$" stepKey="fillTaxRateSearch"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml index e132b86ab4417..ceb04a9c42e66 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml @@ -22,17 +22,16 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> - <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> - <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> - <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> - <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> - <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> - <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clickClearFilters"/> + <actionGroup ref="AdminFilterTaxRateByCodeActionGroup" stepKey="filterByCode"> + <argument name="taxRateCode" value="{{SimpleTaxRate.code}}" /> + </actionGroup> + <actionGroup ref="AdminSelectFirstGridRowActionGroup" stepKey="clickFirstRow"/> + <actionGroup ref="AdminDeleteTaxRateActionGroup" stepKey="deleteTaxRate"/> </after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <!-- Create a tax rate with * for postcodes --> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> @@ -43,8 +42,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify the tax rate grid page shows the tax rate we just created --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> <selectOption selector="{{AdminTaxRateGridSection.filterByCountry}}" userInput="Australia" stepKey="fillCountryFilter"/> @@ -55,8 +53,7 @@ <see selector="{{AdminTaxRateGridSection.grid}}" userInput="*" stepKey="seePostCode"/> <!-- Go to the tax rate edit page for our new tax rate --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex3"/> - <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex3"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter2"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> @@ -69,8 +66,7 @@ <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="20.0000" stepKey="seeRate"/> <!-- Go to the tax rule grid page and verify our tax rate can be used in the rule --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminGridMainControls.add}}" stepKey="clickAddNewTaxRule"/> <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> </test> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateInvalidPostcodeTestLengthTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateInvalidPostcodeTestLengthTest.xml index 3a6e4dfef5bac..6f8379e460c34 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateInvalidPostcodeTestLengthTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateInvalidPostcodeTestLengthTest.xml @@ -22,8 +22,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <!-- Create a tax rate for large postcodes --> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateWithInvalidPostCodeLength.code}}" stepKey="fillRuleName"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml index 0f1b5b08ffcec..7497b950a8c0e 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml @@ -22,17 +22,17 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> - <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> - <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> - <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> - <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> - <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> - <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clickClearFilters"/> + <actionGroup ref="AdminFilterTaxRateByCodeActionGroup" stepKey="filterByCode"> + <argument name="taxRateCode" value="{{SimpleTaxRate.code}}" /> + </actionGroup> + <actionGroup ref="AdminSelectFirstGridRowActionGroup" stepKey="clickFirstRow"/> + + <actionGroup ref="AdminDeleteTaxRateActionGroup" stepKey="deleteTaxRate"/> </after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <!-- Create a tax rate for large postcodes --> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> @@ -43,8 +43,7 @@ <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <!-- Create a tax rate for large postcodes and verify we see expected values on the tax rate grid page --> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> @@ -60,9 +59,8 @@ <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="999.0000" stepKey="seeRate"/> <!-- Verify we see expected values on the tax rule form page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex4"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAdd"/> <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> </test> </tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml index 379164d134448..da89ad3e9337c 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml @@ -22,16 +22,16 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> - <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> - <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> - <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> - <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> - <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> - <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clickClearFilters"/> + <actionGroup ref="AdminFilterTaxRateByCodeActionGroup" stepKey="filterByCode"> + <argument name="taxRateCode" value="{{SimpleTaxRate.code}}" /> + </actionGroup> + <actionGroup ref="AdminSelectFirstGridRowActionGroup" stepKey="clickFirstRow"/> + <actionGroup ref="AdminDeleteTaxRateActionGroup" stepKey="deleteTaxRate"/> </after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <waitForPageLoad stepKey="waitForTaxRateIndex1"/> <!-- Create a tax rate with specific postcode --> @@ -43,8 +43,7 @@ <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <!-- Verify the tax rate grid page shows the specific postcode we just created --> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> @@ -58,9 +57,8 @@ <seeInField selector="{{AdminTaxRateFormSection.country}}" userInput="Canada" stepKey="seeCountry2"/> <!-- Verify we see expected values on the tax rule form page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex4"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAdd"/> <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> </test> </tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml index d276a6a276b2c..da30157d94182 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml @@ -22,17 +22,16 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> - <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> - <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> - <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> - <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> - <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> - <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clickClearFilters"/> + <actionGroup ref="AdminFilterTaxRateByCodeActionGroup" stepKey="filterByCode"> + <argument name="taxRateCode" value="{{SimpleTaxRate.code}}" /> + </actionGroup> + <actionGroup ref="AdminSelectFirstGridRowActionGroup" stepKey="clickFirstRow"/> + <actionGroup ref="AdminDeleteTaxRateActionGroup" stepKey="deleteTaxRate"/> </after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <!-- Create a tax rate with range from 1-7800935 for zipCodes --> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> @@ -44,8 +43,7 @@ <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <!-- Create a tax rate for zipCodeRange and verify we see expected values on the tax rate grid page --> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> @@ -61,9 +59,8 @@ <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="United Kingdom" stepKey="seeCountry2"/> <!-- Verify we see expected values on the tax rule form page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex4"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAdd"/> <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> </test> </tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml index 998c948d869d0..93e0f6514e83b 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml @@ -22,17 +22,16 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> - <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> - <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> - <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> - <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> - <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> - <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clickClearFilters"/> + <actionGroup ref="AdminFilterTaxRateByCodeActionGroup" stepKey="filterByCode"> + <argument name="taxRateCode" value="{{SimpleTaxRate.code}}" /> + </actionGroup> + <actionGroup ref="AdminSelectFirstGridRowActionGroup" stepKey="clickFirstRow"/> + <actionGroup ref="AdminDeleteTaxRateActionGroup" stepKey="deleteTaxRate"/> </after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <!-- Create a tax rate with range from 90001-96162 for zipCodes --> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> @@ -45,8 +44,7 @@ <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <!-- Create a tax rate for zipCodeRange and verify we see expected values on the tax rate grid page --> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> @@ -63,9 +61,8 @@ <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="United States" stepKey="seeCountry2"/> <!-- Verify we see expected values on the tax rule form page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex4"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAdd"/> <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> </test> </tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml index 6e2ca794379f6..f1a48af741cd6 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml @@ -39,10 +39,8 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAddNewTaxRuleButton"/> <!-- Create a tax rule with customer and product class --> <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> @@ -90,4 +88,4 @@ <seeInField selector="{{AdminTaxRuleFormSection.priority}}" userInput="{{taxRuleWithCustomPriorityPosition.priority}}" stepKey="seePriority"/> <seeInField selector="{{AdminTaxRuleFormSection.sortOrder}}" userInput="{{taxRuleWithCustomPriorityPosition.position}}" stepKey="seeSortOrder"/> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml index 895bb920973c8..de7a0fb2d9144 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml @@ -40,10 +40,8 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAddNewTaxRuleButton"/> <!-- Create a tax rule with new and existing tax rate, customer tax class, product tax class --> <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml index 43ce4059ad84e..4798ec60ab898 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml @@ -40,10 +40,8 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAddNewTaxRuleButton"/> <!-- Create a tax rule with new tax classes and tax rate --> <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml index 0293e04293daf..a08c878ba2063 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml @@ -40,10 +40,8 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAddNewTaxRuleButton"/> <!-- Create a tax rule with new tax classes and tax rate --> <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml index 770cdd1e3b2c4..37b90300aad28 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml @@ -30,8 +30,7 @@ <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> @@ -46,8 +45,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="The tax rule has been deleted." stepKey="seeAssertTaxRuleDeleteMessage"/> <!-- Confirm Deleted Tax Rule(from the above step) on the tax rule grid page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex2"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch2"/> <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml index 61b09eabe7d35..fd445326976e4 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml @@ -38,8 +38,7 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml index b7ffe05ebf5c2..3a5607ea598ca 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml @@ -39,8 +39,7 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml index 14df3f8987f5e..fa42ce5ddafa3 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml @@ -43,8 +43,7 @@ <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml index 4b34121b10829..751989497d10e 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml @@ -24,30 +24,26 @@ </before> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> - <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> - <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> - <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> - <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clickClearFilters"/> + <actionGroup ref="AdminFilterTaxRateByCodeActionGroup" stepKey="filterByCode"> + <argument name="taxRateCode" value="$$initialTaxRate.code$$" /> + </actionGroup> + <actionGroup ref="AdminSelectFirstGridRowActionGroup" stepKey="clickFirstRow"/> + <actionGroup ref="AdminDeleteTaxRateActionGroup" stepKey="deleteTaxRate"/> - <!-- Delete values on the tax rate form page --> - <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> - <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> <see selector="{{AdminMessagesSection.success}}" userInput="You Deleted the tax rate." stepKey="seeSuccess1"/> <!-- Confirm Deleted TaxIdentifier(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{defaultTaxRate.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> <see selector="{{AdminTaxRateGridSection.emptyText}}" userInput="We couldn't find any records." stepKey="seeSuccess"/> <!-- Confirm Deleted TaxIdentifier on the tax rule grid page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex3"/> - <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex3"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAddNewTaxRuleButton"/> <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$initialTaxRate.code$$" stepKey="fillTaxRateSearch"/> <wait stepKey="waitForSearch" time="5" /> <dontSee selector="{{AdminTaxRuleFormSection.fieldTaxRate}}" userInput="$$initialTaxRate.code$$" stepKey="dontSeeInTaxRuleForm"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml index b1e91886960c5..f7d23baa534fb 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml index 8a04156f3d857..e08d366a37cd8 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct1"/> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml index b76f015679ae2..bab6b7c45ff60 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -59,16 +58,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -84,7 +81,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml index 5f98093ec874f..b29b1a127189e 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -58,16 +57,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -83,7 +80,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct1"/> </after> 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 d005f4b657448..d2f1b1aa44393 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> </after> @@ -82,15 +79,13 @@ <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"> <argument name="Address" value="US_Address_CA"/> </actionGroup> - <click stepKey="clickNext" selector="{{CheckoutShippingSection.next}}"/> - <waitForPageLoad stepKey="waitForAddressToLoad"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <see stepKey="seeAddress" selector="{{CheckoutShippingSection.defaultShipping}}" userInput="{{SimpleTaxCA.state}}"/> @@ -109,8 +104,7 @@ <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress2"> <argument name="Address" value="US_Address_NY"/> </actionGroup> - <click stepKey="clickNext2" selector="{{CheckoutShippingSection.next}}"/> - <waitForPageLoad stepKey="waitForShippingToLoad"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext2"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> <see stepKey="seeShipTo2" selector="{{CheckoutPaymentSection.shipToInformation}}" userInput="{{SimpleTaxNY.state}}"/> 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 d1fc0654fc496..5441664d7c530 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct1"/> </after> @@ -82,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 18a1a11d35fd2..76f4dcd8e161e 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> </after> @@ -97,11 +94,9 @@ <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}}"/> - <click stepKey="clickNext" selector="{{CheckoutShippingSection.next}}"/> - <waitForPageLoad stepKey="waitForReviewAndPayments"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElementVisible stepKey="waitForOverviewVisible" selector="{{CheckoutPaymentSection.tax}}"/> <see stepKey="seeTax" selector="{{CheckoutPaymentSection.tax}}" userInput="$10.30"/> @@ -123,8 +118,7 @@ <waitForPageLoad stepKey="waitForAddressSaved"/> <see stepKey="seeAddress2" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{SimpleTaxCA.state}}"/> - <click stepKey="clickNext2" selector="{{CheckoutShippingSection.next}}"/> - <waitForPageLoad stepKey="waitForReviewAndPayments2"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext2"/> <!-- Assert that taxes are applied correctly for CA --> <waitForElementVisible stepKey="waitForOverviewVisible2" selector="{{CheckoutPaymentSection.tax}}"/> 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 35a483da7f690..c98765976f36f 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct1"/> </after> @@ -97,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/Test/Mftf/Test/Update01TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml index 306216422adea..ff75b1e95646a 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -45,8 +44,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated 0.1 tax rate(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex4"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex4"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateFrance.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml index c22bab774de29..ebfa1288b59dd 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -44,8 +43,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated TaxIdentifier(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex4"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex4"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateUS.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml index 6f93d07b76eed..ed1c126930df8 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax identifier on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode1"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -45,8 +44,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated tax rate(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateUK.code}}" stepKey="fillTaxIdentifierField2"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml index c3986e6a8d0cc..7a2f0664d7757 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -44,8 +43,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated any region tax rate(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateCanada.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml index fb1eff1d74067..03aba8da8ae19 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -46,8 +45,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated tax rate(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{defaultTaxRateWithZipRange.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml index 1f0406244a926..37b8bb8d95618 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -43,8 +42,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated large tax rate(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex4"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex4"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{defaultTaxRateWithLargeRate.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/composer.json b/app/code/Magento/Tax/composer.json index 65c668553cd14..2fe0597c85a63 100644 --- a/app/code/Magento/Tax/composer.json +++ b/app/code/Magento/Tax/composer.json @@ -19,7 +19,8 @@ "magento/module-reports": "*", "magento/module-sales": "*", "magento/module-shipping": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-ui": "*" }, "suggest": { "magento/module-tax-sample-data": "*" diff --git a/app/code/Magento/Tax/view/adminhtml/templates/rate/js.phtml b/app/code/Magento/Tax/view/adminhtml/templates/rate/js.phtml index fec108d53948f..d7b04f8e29f27 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/rate/js.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/rate/js.phtml @@ -4,15 +4,20 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** @var \Magento\Tax\Block\Adminhtml\Rate\Form $tmpBlock */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php /** @var \Magento\Directory\Helper\Data $jsonHelper */ +$jsonHelper = $tmpBlock->getData('directoryHelper'); +$regionJson = /* @noEscape */ $jsonHelper->getRegionJson(); +$scriptString = <<<script require([ "jquery", "mage/adminhtml/form" ], function(jQuery){ - var updater = new RegionUpdater('tax_country_id', 'tax_region', 'tax_region_id', <?= /* @noEscape */ $this->helper(\Magento\Directory\Helper\Data::class)->getRegionJson() ?>, 'disable'); + var updater = new RegionUpdater('tax_country_id', 'tax_region', 'tax_region_id', {$regionJson}, 'disable'); updater.disableRegionValidation(); (function ($) { @@ -54,4 +59,6 @@ require([ window.updater = updater; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml b/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml index 3558d359aa4d6..0141101ef5a78 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml @@ -5,8 +5,12 @@ */ /** @var $block \Magento\Tax\Block\Adminhtml\Rule\Edit\Form */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $formElementId = /* @noEscape */ \Magento\Tax\Block\Adminhtml\Rate\Form::FORM_ELEMENT_ID; +$jsId = /* @noEscape */ $block->getJsId(); +//phpcs:ignore Magento2.SQL.RawQuery +$scriptString = <<<script require([ 'jquery', 'Magento_Ui/js/modal/alert', @@ -77,7 +81,7 @@ require([ $.ajax({ type: "POST", data: {id:id}, - url: '<?= $block->escapeJs($block->escapeUrl($block->getTaxRateLoadUrl())) ?>', + url: '{$block->escapeJs($block->getTaxRateLoadUrl())}', success: function(result, status) { $('body').trigger('processStop'); if (result.success) { @@ -94,14 +98,14 @@ require([ }); else alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); } }, error: function () { $('body').trigger('processStop'); alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); }, dataType: "json" @@ -112,9 +116,9 @@ require([ var options = { mselectContainer: '#tax_rate + section.mselect-list', toggleAddButton:false, - addText: '<?= $block->escapeJs($block->escapeHtml(__('Add New Tax Rate'))) ?>', + addText: '{$block->escapeJs(__('Add New Tax Rate'))}', parse: null, - nextPageUrl: '<?= $block->escapeHtml($block->getTaxRatesPageUrl()) ?>', + nextPageUrl: '{$block->escapeJs($block->getTaxRatesPageUrl())}', selectedValues: this.settings.selected_values, mselectInputSubmitCallback: function (value, options) { var select = $('#tax_rate'); @@ -137,7 +141,7 @@ require([ var taxRate = $('#tax_rate'), taxRateField = taxRate.parent(), taxRateForm = $('#tax-rate-form'), - taxRateFormElement = $('#<?= /* @noEscape */ \Magento\Tax\Block\Adminhtml\Rate\Form::FORM_ELEMENT_ID ?>'); + taxRateFormElement = $('#{$formElementId}'); if (!this.isEntityEditable) { // Override default layout of editable multiselect @@ -162,11 +166,16 @@ require([ .on('click.mselect-edit', '.mselect-edit', this.edit) .on("click.mselect-delete", ".mselect-delete", function () { var that = $(this), - select = that.closest('.mselect-list').prev(), + +script; + // phpcs:ignore Magento2.SQL.RawQuery + $scriptString .= "select = that.closest('.mselect-list').prev()," . PHP_EOL; + $scriptString .= <<<script + rateValue = that.parent().find('input[type="checkbox"]').val(); confirm({ - content: '<?= $block->escapeJs(__('Do you really want to delete this tax rate?')) ?>', + content: '{$block->escapeJs(__('Do you really want to delete this tax rate?'))}', actions: { /** * Confirm action. @@ -180,7 +189,7 @@ require([ form_key: $('input[name="form_key"]').val() }, dataType: 'json', - url: '<?= $block->escapeJs($block->escapeUrl($block->getTaxRateDeleteUrl())) ?>', + url: '{$block->escapeJs($block->getTaxRateDeleteUrl())}', success: function(result, status) { $('body').trigger('processStop'); if (result.success) { @@ -198,14 +207,14 @@ require([ }); else alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); } }, error: function () { $('body').trigger('processStop'); alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); } }; @@ -228,15 +237,15 @@ require([ taxRateFormElement.mage('form').mage('validation'); taxRateForm.dialogRates({ - title: '<?= $block->escapeJs($block->escapeHtml(__('Tax Rate'))) ?>', + title: '{$block->escapeJs(__('Tax Rate'))}', type: 'slide', - id: '<?= /* @noEscape */ $block->getJsId() ?>', + id: '{$jsId}', modalClass: 'tax-rate-popup', closed: function () { taxRateFormElement.data('validation').clearError(); }, buttons: [{ - text: '<?= $block->escapeJs($block->escapeHtml(__('Save'))) ?>', + text: '{$block->escapeJs(__('Save'))}', 'class': 'action-save action-primary', click: function() { this.updateItemRate(); @@ -244,7 +253,12 @@ require([ itemRateData = $.extend({}, itemRate); if (itemRateData.itemElement) { - delete itemRateData.itemElement; + +script; + //phpcs:ignore Magento2.SQL.RawQuerys + $scriptString .= ' delete itemRateData.itemElement;'; +$scriptString.= <<<script + } if (!taxRateFormElement.validation().valid()) { @@ -256,7 +270,7 @@ require([ type: 'POST', data: itemRateData, dataType: 'json', - url: '<?= $block->escapeJs($block->escapeUrl($block->getTaxRateSaveUrl())) ?>', + url: '{$block->escapeJs($block->getTaxRateSaveUrl())}', success: function(result, status) { $('body').trigger('processStop'); if (result.success) { @@ -281,14 +295,14 @@ require([ }); else alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); } }, error: function () { $('body').trigger('processStop'); alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); } }; @@ -302,4 +316,6 @@ require([ window.TaxRateEditableMultiselect = TaxRateEditableMultiselect; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/rule/rate/form.phtml b/app/code/Magento/Tax/view/adminhtml/templates/rule/rate/form.phtml index f09af05303f36..e6b7282cf4d27 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/rule/rate/form.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/rule/rate/form.phtml @@ -10,7 +10,7 @@ <div class="grid-loader"></div> </div> -<div class="form-inline" id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" style="display:none"> +<div class="form-inline no-display" id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>"> <?= $block->getFormHtml() ?> <?= $block->getChildHtml('form_after') ?> </div> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/add.phtml b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/add.phtml index d5017f83affe4..8a4cfd9d6b574 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/add.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/add.phtml @@ -4,9 +4,15 @@ * See COPYING.txt for license details. */ // @deprecated +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> - <button type="button" onclick="window.location.href='<?= $block->escapeUrl($createUrl) ?>'"> + <button type="button" id="addNewClass"> <?= $block->escapeHtml(__('Add New Class')) ?> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.location.href='" . $block->escapeJs($createUrl) . "'", + 'button#addNewClass' + ) ?> </div> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/save.phtml b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/save.phtml index fa9fcb8fbcfcd..91860c70fd086 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/save.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/save.phtml @@ -4,19 +4,23 @@ * See COPYING.txt for license details. */ // @deprecated + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> <?= $block->getBackButtonHtml() ?> <?= $block->getResetButtonHtml() ?> <?= $block->getSaveButtonHtml() ?> </div> -<?php if ($form) : ?> +<?php if ($form): ?> <?= $form->toHtml() ?> - <script> + <?php $scriptString = <<<script require(['jquery', "mage/mage"], function(jQuery){ - jQuery('#<?= $block->escapeJs($form->getForm()->getId()) ?>').mage('form').mage('validation'); + jQuery('#{$block->escapeJs($form->getForm()->getId())}').mage('form').mage('validation'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rate/save.phtml b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rate/save.phtml index 58c79bbfe9715..7053cd61e29ac 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rate/save.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rate/save.phtml @@ -3,16 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Tax\Block\Adminhtml\Rate\Form $form */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($form) : ?> +<?php if ($form): ?> <?= $form->toHtml() ?> - <script> + <?php $scriptString = <<<script require([ "jquery", "mage/mage" ], function($){ - $('#<?= $block->escapeJs($form->getForm()->getId()) ?>').mage('form').mage('validation'); + $('#{$block->escapeJs($form->getForm()->getId())}').mage('form').mage('validation'); $(document).ready(function () { 'use strict'; @@ -42,5 +45,7 @@ }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/add.phtml b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/add.phtml index e21dbb099ff5d..f7af88b8207dc 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/add.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/add.phtml @@ -4,9 +4,15 @@ * See COPYING.txt for license details. */ // @deprecated +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> - <button type="button" onclick="window.location.href='<?= $block->escapeUrl($createUrl) ?>'"> + <button type="button" id="addNewTaxRule"> <?= $block->escapeHtml(__('Add New Tax Rule')) ?> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.location.href='" . $block->escapeJs($createUrl) . "'", + 'button#addNewClass' + ) ?> </div> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/save.phtml b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/save.phtml index 10251e2805f2f..3830be23d80a6 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/save.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/save.phtml @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ // @deprecated + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> <?= $block->getBackButtonHtml() ?> @@ -11,13 +13,15 @@ <?= $block->getSaveButtonHtml() ?> <?= $block->getDeleteButtonHtml() ?> </div> -<?php if ($form) : ?> +<?php if ($form): ?> <?= $form->toHtml() ?> - <script> + <?php $scriptString = <<<script require(['jquery', "mage/mage"], function(jQuery){ - jQuery('#<?= $block->escapeJs($form->getForm()->getId()) ?>').mage('form').mage('validation'); + jQuery('#{$block->escapeJs($form->getForm()->getId())}').mage('form').mage('validation'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> 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/Tax/view/frontend/templates/checkout/grandtotal.phtml b/app/code/Magento/Tax/view/frontend/templates/checkout/grandtotal.phtml index df177b6180511..e9a92120f8cf1 100644 --- a/app/code/Magento/Tax/view/frontend/templates/checkout/grandtotal.phtml +++ b/app/code/Magento/Tax/view/frontend/templates/checkout/grandtotal.phtml @@ -4,41 +4,54 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** * @var $block \Magento\Tax\Block\Checkout\Grandtotal + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $style = $block->escapeHtmlAttr($block->getStyle()); $colspan = (int) $block->getColspan(); +/** @var \Magento\Checkout\Helper\Data $checkoutHelper */ +$checkoutHelper = $block->getData('checkoutHelper'); ?> -<?php if ($block->includeTax() && $block->getTotalExclTax() >= 0) : ?> +<?php if ($block->includeTax() && $block->getTotalExclTax() >= 0): ?> <tr class="grand totals excl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <strong><?= $block->escapeHtml(__('Grand Total Excl. Tax')) ?></strong> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr(__('Grand Total Excl. Tax')) ?>"> - <strong><?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotalExclTax()) ?></strong> + <td class="amount" data-th="<?= $block->escapeHtmlAttr(__('Grand Total Excl. Tax')) ?>"> + <strong><?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotalExclTax()) ?></strong> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals.excl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals.excl td.amount') ?> + <?php endif; ?> <?= /* @noEscape */ $block->renderTotals('taxes', $colspan) ?> <tr class="grand totals incl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <strong><?= $block->escapeHtml(__('Grand Total Incl. Tax')) ?></strong> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr(__('Grand Total Incl. Tax')) ?>"> - <strong><?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValue()) ?></strong> + <td class="amount" data-th="<?= $block->escapeHtmlAttr(__('Grand Total Incl. Tax')) ?>"> + <strong><?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()->getValue()) ?></strong> </td> </tr> -<?php else : ?> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals.incl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals.incl td.amount') ?> + <?php endif; ?> +<?php else: ?> <tr class="grand totals"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <strong><?= $block->escapeHtml($block->getTotal()->getTitle()) ?></strong> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> - <strong><?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValue()) ?></strong> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <strong><?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()->getValue()) ?></strong> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals td.amount') ?> + <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/frontend/templates/checkout/shipping.phtml b/app/code/Magento/Tax/view/frontend/templates/checkout/shipping.phtml index 3f5a55e5fa325..e2989d8313283 100644 --- a/app/code/Magento/Tax/view/frontend/templates/checkout/shipping.phtml +++ b/app/code/Magento/Tax/view/frontend/templates/checkout/shipping.phtml @@ -4,52 +4,70 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** * @var $block \Magento\Tax\Block\Checkout\Shipping * @see \Magento\Tax\Block\Checkout\Shipping + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->displayShipping()) : ?> +<?php if ($block->displayShipping()): ?> <?php $style = $block->escapeHtmlAttr($block->getStyle()); $colspan = (int) $block->getColspan(); + /** @var \Magento\Checkout\Helper\Data $checkoutHelper */ + $checkoutHelper = $block->getData('checkoutHelper'); ?> - <?php if ($block->displayBoth()) : ?> + <?php if ($block->displayBoth()): ?> <tr class="totals shipping excl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml($block->getExcludeTaxLabel()) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getExcludeTaxLabel()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getShippingExcludeTax()) ?> + + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getExcludeTaxLabel()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getShippingExcludeTax()) ?> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.excl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.excl td.amount') ?> + <?php endif; ?> <tr class="totals shipping incl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml($block->getIncludeTaxLabel()) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getIncludeTaxLabel()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getShippingIncludeTax()) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getIncludeTaxLabel()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getShippingIncludeTax()) ?> </td> </tr> - <?php elseif ($block->displayIncludeTax()) : ?> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.incl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.incl td.amount') ?> + <?php endif; ?> + <?php elseif ($block->displayIncludeTax()): ?> <tr class="totals shipping incl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getShippingIncludeTax()) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getShippingIncludeTax()) ?> </td> </tr> - <?php else : ?> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.incl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.incl td.amount') ?> + <?php endif; ?> + <?php else: ?> <tr class="totals shipping excl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getShippingExcludeTax()) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getShippingExcludeTax()) ?> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.excl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.excl td.amount') ?> + <?php endif; ?> <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/frontend/templates/checkout/subtotal.phtml b/app/code/Magento/Tax/view/frontend/templates/checkout/subtotal.phtml index 010a7b8dcfe4a..dc9034fc9f694 100644 --- a/app/code/Magento/Tax/view/frontend/templates/checkout/subtotal.phtml +++ b/app/code/Magento/Tax/view/frontend/templates/checkout/subtotal.phtml @@ -4,41 +4,54 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** * @var $block \Magento\Tax\Block\Checkout\Subtotal * @see \Magento\Tax\Block\Checkout\Subtotal + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $style = $block->escapeHtmlAttr($block->getStyle()); $colspan = (int) $block->getColspan(); +/** @var \Magento\Checkout\Helper\Data $checkoutHelper */ +$checkoutHelper = $block->getData('checkoutHelper'); ?> -<?php if ($block->displayBoth()) : ?> +<?php if ($block->displayBoth()): ?> <tr class="totals sub excl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml(__('Subtotal (Excl. Tax)')) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr(__('Subtotal (Excl. Tax)')) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValueExclTax()) ?> + <tdclass="amount" data-th="<?= $block->escapeHtmlAttr(__('Subtotal (Excl. Tax)')) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()->getValueExclTax()) ?> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub.excl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub.excl td.amount') ?> + <?php endif; ?> <tr class="totals sub incl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml(__('Subtotal (Incl. Tax)')) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr(__('Subtotal (Incl. Tax)')) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValueInclTax()) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr(__('Subtotal (Incl. Tax)')) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()->getValueInclTax()) ?> </td> </tr> -<?php else : ?> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub.incl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub.incl td.amount') ?> + <?php endif; ?> +<?php else: ?> <tr class="totals sub"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValue()) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()->getValue()) ?> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub td.amount') ?> + <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/frontend/templates/checkout/tax.phtml b/app/code/Magento/Tax/view/frontend/templates/checkout/tax.phtml index 0329db406fa16..e265c029578a6 100644 --- a/app/code/Magento/Tax/view/frontend/templates/checkout/tax.phtml +++ b/app/code/Magento/Tax/view/frontend/templates/checkout/tax.phtml @@ -4,60 +4,72 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** * @var $block \Magento\Tax\Block\Checkout\Tax * @see \Magento\Tax\Block\Checkout\Tax + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_value = $block->getTotal()->getValue(); $_style = $block->escapeHtmlAttr($block->getTotal()->getStyle()); - + /** @var \Magento\Tax\Helper\Data $taxHelper */ + $taxHelper = $block->getData('taxHelper'); + /** @var \Magento\Checkout\Helper\Data $checkoutHelper */ + $checkoutHelper = $block->getData('checkoutHelper'); $attributes = 'class="totals-tax"'; -if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary() && $_value != 0) { - $attributes = 'class="totals-tax-summary" data-mage-init=\'{"toggleAdvanced": {"selectorsToggleClass": "shown", "baseToggleClass": "expanded", "toggleContainers": ".totals-tax-details"}}\''; +if ($taxHelper->displayFullSummary() && $_value != 0) { + $attributes = 'class="totals-tax-summary" data-mage-init=\'{"toggleAdvanced": {"selectorsToggleClass": "shown", + "baseToggleClass": "expanded", "toggleContainers": ".totals-tax-details"}}\''; } ?> <tr <?= /* @noEscape */ $attributes ?>> - <th style="<?= /* @noEscape */ $_style ?>" class="mark" colspan="<?= (int) $block->getColspan() ?>" scope="row"> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + <th class="mark" colspan="<?= (int) $block->getColspan() ?>" scope="row"> + <?php if ($taxHelper->displayFullSummary()): ?> <span class="detailed"><?= $block->escapeHtml($block->getTotal()->getTitle()) ?></span> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> <?php endif; ?> </th> - <td style="<?= /* @noEscape */ $_style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($_value) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($_value) ?> </td> </tr> +<?php if ($_style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($_style, 'tr.totals-tax th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($_style, 'tr.totals-tax td.amount') ?> +<?php endif; ?> -<?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary() && $_value != 0) : ?> - <?php foreach ($block->getTotal()->getFullInfo() as $info) : ?> +<?php if ($taxHelper->displayFullSummary() && $_value != 0): ?> + <?php foreach ($block->getTotal()->getFullInfo() as $info): ?> <?php if (isset($info['hidden']) && $info['hidden']) { continue; } ?> <?php $percent = $info['percent']; ?> <?php $amount = $info['amount']; ?> <?php $rates = $info['rates']; ?> <?php $isFirst = 1; ?> - <?php foreach ($rates as $rate) : ?> + <?php foreach ($rates as $rate): ?> <tr class="totals-tax-details"> - <th class="mark" style="<?= /* @noEscape */ $_style ?>" colspan="<?= (int) $block->getColspan() ?>" scope="row"> + <th class="mark" colspan="<?= (int) $block->getColspan() ?>" scope="row"> <?= $block->escapeHtml($rate['title']) ?> - <?php if ($rate['percent'] !== null) : ?> + <?php if ($rate['percent'] !== null): ?> (<?= (float) $rate['percent'] ?>%) <?php endif; ?> </th> - <?php if ($isFirst) : ?> - <td style="<?= /* @noEscape */ $_style ?>" class="amount" rowspan="<?= count($rates) ?>" - data-th="<?= $block->escapeHtmlAttr($rate['title']) ?><?php if ($rate['percent'] !== null) : ?>(<?= (float) $rate['percent'] ?>%)<?php endif; ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($amount) ?> + <?php if ($isFirst): ?> + <td class="amount" rowspan="<?= count($rates) ?>" + data-th="<?= $block->escapeHtmlAttr($rate['title']) ?> + <?php if ($rate['percent'] !== null): ?>(<?= (float) $rate['percent'] ?>%)<?php endif; ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($amount) ?> </td> <?php endif; ?> </tr> + <?php if ($_style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($_style, 'tr.totals-tax-details th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($_style, 'tr.totals-tax-details td.amount') ?> + <?php endif; ?> <?php $isFirst = 0; ?> <?php endforeach; ?> <?php endforeach; ?> diff --git a/app/code/Magento/TaxImportExport/composer.json b/app/code/Magento/TaxImportExport/composer.json index ee24deb9d3246..01c069b4299c1 100644 --- a/app/code/Magento/TaxImportExport/composer.json +++ b/app/code/Magento/TaxImportExport/composer.json @@ -10,7 +10,8 @@ "magento/module-backend": "*", "magento/module-directory": "*", "magento/module-store": "*", - "magento/module-tax": "*" + "magento/module-tax": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml index 1c6b267cd9289..79d833771768d 100644 --- a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml +++ b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml @@ -5,11 +5,12 @@ */ /** @var $block \Magento\TaxImportExport\Block\Adminhtml\Rate\ImportExport */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="import-export-tax-rates"> - <?php if (!$block->getIsReadonly()) :?> + <?php if (!$block->getIsReadonly()):?> <div class="import-tax-rates"> - <?php if ($block->getUseContainer()) :?> + <?php if ($block->getUseContainer()):?> <form id="import-form" class="admin__fieldset" action="<?= $block->escapeUrl($block->getUrl('tax/rate/importPost')) ?>" @@ -18,7 +19,9 @@ <?php endif; ?> <?= $block->getBlockHtml('formkey') ?> <div class="fieldset admin__field"> - <label for="import_rates_file" class="admin__field-label"><span><?= $block->escapeHtml(__('Import Tax Rates')) ?></span></label> + <label for="import_rates_file" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Import Tax Rates')) ?></span> + </label> <div class="admin__field-control"> <input type="file" id="import_rates_file" @@ -27,11 +30,13 @@ <?= $block->getButtonHtml(__('Import Tax Rates'), '', 'import-submit') ?> </div> </div> - <?php if ($block->getUseContainer()) :?> + <?php if ($block->getUseContainer()):?> </form> <?php endif; ?> - <script> -require(['jquery', 'Magento_Ui/js/modal/alert', "mage/mage", "loadingPopup", 'mage/translate'], function(jQuery, uiAlert){ + <?php $scriptString = <<<script + + require(['jquery', 'Magento_Ui/js/modal/alert', "mage/mage", "loadingPopup", 'mage/translate'], + function(jQuery, uiAlert){ jQuery('#import-form').mage('form').mage('validation'); (function ($) { @@ -51,11 +56,14 @@ require(['jquery', 'Magento_Ui/js/modal/alert', "mage/mage", "loadingPopup", 'ma })(jQuery); }); -</script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> <?php endif; ?> <div class="export-tax-rates <?= ($block->getIsReadonly()) ? 'box-left' : 'box-right' ?>"> - <?php if ($block->getUseContainer()) :?> + <?php if ($block->getUseContainer()):?> <form id="export_form" class="admin__fieldset" action="<?= $block->escapeUrl($block->getUrl('tax/rate/exportPost')) ?>" @@ -69,7 +77,7 @@ require(['jquery', 'Magento_Ui/js/modal/alert', "mage/mage", "loadingPopup", 'ma <?= $block->getButtonHtml(__('Export Tax Rates'), "this.form.submit()") ?> </div> </div> - <?php if ($block->getUseContainer()) :?> + <?php if ($block->getUseContainer()):?> </form> <?php endif; ?> </div> diff --git a/app/code/Magento/Theme/Block/Html/Footer.php b/app/code/Magento/Theme/Block/Html/Footer.php index cdb350336f38f..7f9b9cf86a809 100644 --- a/app/code/Magento/Theme/Block/Html/Footer.php +++ b/app/code/Magento/Theme/Block/Html/Footer.php @@ -127,6 +127,7 @@ public function getIdentities() * Get block cache life time * * @return int + * @since 100.2.4 */ protected function getCacheLifetime() { diff --git a/app/code/Magento/Theme/Block/Html/Header/Logo.php b/app/code/Magento/Theme/Block/Html/Header/Logo.php index 626a771b4e309..792ee95de4995 100644 --- a/app/code/Magento/Theme/Block/Html/Header/Logo.php +++ b/app/code/Magento/Theme/Block/Html/Header/Logo.php @@ -43,7 +43,7 @@ public function __construct( /** * Check if current url is url for home page * - * @deprecated This function is no longer used. It was previously used by + * @deprecated 101.0.1 This function is no longer used. It was previously used by * Magento/Theme/view/frontend/templates/html/header/logo.phtml * to check if the logo should be clickable on the homepage. * diff --git a/app/code/Magento/Theme/Block/Html/Pager.php b/app/code/Magento/Theme/Block/Html/Pager.php index 5798b94e31a70..764b2e9ca42f0 100644 --- a/app/code/Magento/Theme/Block/Html/Pager.php +++ b/app/code/Magento/Theme/Block/Html/Pager.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Theme\Block\Html; /** @@ -466,7 +467,26 @@ public function getPageUrl($page) */ public function getLimitUrl($limit) { - return $this->getPagerUrl([$this->getLimitVarName() => $limit]); + return $this->getPagerUrl($this->getPageLimitParams($limit)); + } + + /** + * Return page limit params + * + * @param int $limit + * @return array + */ + private function getPageLimitParams(int $limit): array + { + $data = [$this->getLimitVarName() => $limit]; + + $currentPage = $this->getCurrentPage(); + $availableCount = (int) ceil($this->getTotalNum() / $limit); + if ($currentPage !== 1 && $availableCount < $currentPage) { + $data = array_merge($data, [$this->getPageVarName() => $availableCount === 1 ? null : $availableCount]); + } + + return $data; } /** diff --git a/app/code/Magento/Theme/Block/Html/Title.php b/app/code/Magento/Theme/Block/Html/Title.php index 9059afe19ab05..a2ef83117ccf5 100644 --- a/app/code/Magento/Theme/Block/Html/Title.php +++ b/app/code/Magento/Theme/Block/Html/Title.php @@ -101,7 +101,7 @@ public function setPageTitle($pageTitle) private function shouldTranslateTitle(): bool { return $this->scopeConfig->isSetFlag( - static::XML_PATH_HEADER_TRANSLATE_TITLE, + self::XML_PATH_HEADER_TRANSLATE_TITLE, ScopeInterface::SCOPE_STORE ); } diff --git a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php index fc396615e71e7..fb2ceb0a91fc9 100644 --- a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php +++ b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php @@ -11,7 +11,7 @@ /** * Class UploadJs - * @deprecated + * @deprecated 101.0.0 */ class UploadJs extends \Magento\Theme\Controller\Adminhtml\System\Design\Theme implements HttpGetActionInterface { diff --git a/app/code/Magento/Theme/Controller/Result/AsyncCssPlugin.php b/app/code/Magento/Theme/Controller/Result/AsyncCssPlugin.php index 70ea478004b9d..dfa83f1765041 100644 --- a/app/code/Magento/Theme/Controller/Result/AsyncCssPlugin.php +++ b/app/code/Magento/Theme/Controller/Result/AsyncCssPlugin.php @@ -10,6 +10,9 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\Response\Http; +use Magento\Framework\App\Response\HttpInterface as HttpResponseInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\View\Result\Layout; /** * Plugin for asynchronous CSS loading. @@ -32,48 +35,94 @@ public function __construct(ScopeConfigInterface $scopeConfig) } /** - * Load CSS asynchronously if it is enabled in configuration. + * Extracts styles to head after critical css if critical path feature is enabled. * - * @param Http $subject - * @return void + * @param Layout $subject + * @param Layout $result + * @param HttpResponseInterface|ResponseInterface $httpResponse + * @return Layout (That should be void, actually) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeSendResponse(Http $subject): void + public function afterRenderResult(Layout $subject, Layout $result, ResponseInterface $httpResponse) { - $content = $subject->getContent(); + if (!$this->isCssCriticalEnabled()) { + return $result; + } - if (\is_string($content) && strpos($content, '</body') !== false && $this->scopeConfig->isSetFlag( - self::XML_PATH_USE_CSS_CRITICAL_PATH, - ScopeInterface::SCOPE_STORE - )) { - $cssMatches = []; - // add link rel preload to style sheets - $content = preg_replace_callback( - '@<link\b.*?rel=("|\')stylesheet\1.*?/>@', - function ($matches) use (&$cssMatches) { - $cssMatches[] = $matches[0]; - preg_match('@href=("|\')(.*?)\1@', $matches[0], $hrefAttribute); - $href = $hrefAttribute[2]; - if (preg_match('@media=("|\')(.*?)\1@', $matches[0], $mediaAttribute)) { - $media = $mediaAttribute[2]; - } - $media = $media ?? 'all'; - $loadCssAsync = sprintf( - '<link rel="preload" as="style" media="%s"' . - ' onload="this.onload=null;this.rel=\'stylesheet\'"' . - ' href="%s" />', - $media, - $href - ); - - return $loadCssAsync; - }, - $content - ); + $content = (string)$httpResponse->getContent(); + $headCloseTag = '</head>'; - if (!empty($cssMatches)) { - $content = str_replace('</body', implode("\n", $cssMatches) . "\n</body", $content); - $subject->setContent($content); + $headEndTagFound = strpos($content, $headCloseTag) !== false; + + if ($headEndTagFound) { + $styles = $this->extractLinkTags($content); + if ($styles) { + $newHeadEndTagPosition = strrpos($content, $headCloseTag); + $content = substr_replace($content, $styles . "\n", $newHeadEndTagPosition, 0); + $httpResponse->setContent($content); } } + + return $result; + } + + /** + * Extracts link tags found in given content. + * + * @param string $content + */ + private function extractLinkTags(string &$content): string + { + $styles = ''; + $styleOpen = '<link'; + $styleClose = '>'; + $styleOpenPos = strpos($content, $styleOpen); + + while ($styleOpenPos !== false) { + $styleClosePos = strpos($content, $styleClose, $styleOpenPos); + $style = substr($content, $styleOpenPos, $styleClosePos - $styleOpenPos + strlen($styleClose)); + + if (!preg_match('@rel=["\']stylesheet["\']@', $style)) { + // Link is not a stylesheet, search for another one after it. + $styleOpenPos = strpos($content, $styleOpen, $styleClosePos); + continue; + } + // Remove the link from HTML to add it before </head> tag later. + $content = str_replace($style, '', $content); + + if (!preg_match('@href=("|\')(.*?)\1@', $style, $hrefAttribute)) { + throw new \RuntimeException("Invalid link {$style} syntax provided"); + } + $href = $hrefAttribute[2]; + + if (preg_match('@media=("|\')(.*?)\1@', $style, $mediaAttribute)) { + $media = $mediaAttribute[2]; + } + $media = $media ?? 'all'; + + $style = sprintf( + '<link rel="stylesheet" media="print" onload="this.onload=null;this.media=\'%s\'" href="%s">', + $media, + $href + ); + $styles .= "\n" . $style; + // Link was cut out, search for the next one at its former position. + $styleOpenPos = strpos($content, $styleOpen, $styleOpenPos); + } + + return $styles; + } + + /** + * Returns information whether css critical path is enabled + * + * @return bool + */ + private function isCssCriticalEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_USE_CSS_CRITICAL_PATH, + ScopeInterface::SCOPE_STORE + ); } } diff --git a/app/code/Magento/Theme/Controller/Result/MessagePlugin.php b/app/code/Magento/Theme/Controller/Result/MessagePlugin.php index 83172df748a47..10cba6e869030 100644 --- a/app/code/Magento/Theme/Controller/Result/MessagePlugin.php +++ b/app/code/Magento/Theme/Controller/Result/MessagePlugin.php @@ -14,6 +14,8 @@ /** * Plugin for putting messages to cookies + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class MessagePlugin { @@ -116,7 +118,6 @@ public function afterRenderResult( * ], * ] * - * * @param array $messages List of Magento messages that must be set as 'mage-messages' cookie. * @return void */ diff --git a/app/code/Magento/Theme/Helper/Storage.php b/app/code/Magento/Theme/Helper/Storage.php index e41bc8b145e38..aff70c5d1ee97 100644 --- a/app/code/Magento/Theme/Helper/Storage.php +++ b/app/code/Magento/Theme/Helper/Storage.php @@ -11,6 +11,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem\DriverInterface; /** * Handles the storage of media files like images and fonts. @@ -97,6 +98,10 @@ class Storage extends \Magento\Framework\App\Helper\AbstractHelper * @var \Magento\Framework\Filesystem\Io\File */ private $file; + /** + * @var DriverInterface + */ + private $filesystemDriver; /** * @param \Magento\Framework\App\Helper\Context $context @@ -105,6 +110,7 @@ class Storage extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\View\Design\Theme\FlyweightFactory $themeFactory * @param \Magento\Framework\Filesystem\Io\File|null $file * + * @param DriverInterface|null $filesystemDriver * @throws \Magento\Framework\Exception\FileSystemException * @throws \Magento\Framework\Exception\ValidatorException */ @@ -113,7 +119,8 @@ public function __construct( \Magento\Framework\Filesystem $filesystem, \Magento\Backend\Model\Session $session, \Magento\Framework\View\Design\Theme\FlyweightFactory $themeFactory, - \Magento\Framework\Filesystem\Io\File $file = null + \Magento\Framework\Filesystem\Io\File $file = null, + DriverInterface $filesystemDriver = null ) { parent::__construct($context); $this->filesystem = $filesystem; @@ -124,6 +131,7 @@ public function __construct( $this->file = $file ?: ObjectManager::getInstance()->get( \Magento\Framework\Filesystem\Io\File::class ); + $this->filesystemDriver = $filesystemDriver ?: ObjectManager::getInstance()->get(DriverInterface::class); } /** @@ -247,7 +255,16 @@ public function getCurrentPath() if ($path && $path !== self::NODE_ROOT) { $path = $this->convertIdToPath($path); - if ($this->mediaDirectoryWrite->isDirectory($path) && 0 === strpos($path, (string) $currentPath)) { + $path = $this->filesystemDriver->getRealPathSafety($path); + + if (strpos($path, $currentPath) !== 0) { + $path = $currentPath; + } + + if ($this->mediaDirectoryWrite->isDirectory($path) + && strpos($path, $currentPath) === 0 + && $path !== $currentPath + ) { $currentPath = $this->mediaDirectoryWrite->getRelativePath($path); } } diff --git a/app/code/Magento/Theme/Model/Config/Customization.php b/app/code/Magento/Theme/Model/Config/Customization.php index 6a6872d794b1b..7430730451110 100644 --- a/app/code/Magento/Theme/Model/Config/Customization.php +++ b/app/code/Magento/Theme/Model/Config/Customization.php @@ -5,23 +5,34 @@ */ namespace Magento\Theme\Model\Config; +use Magento\Framework\App\Area; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Theme\Model\ResourceModel\Theme\Collection; +use Magento\Theme\Model\Theme\StoreThemesResolverInterface; +use Magento\Theme\Model\Theme\StoreUserAgentThemeResolver; + /** * Theme customization config model */ class Customization { /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Framework\View\DesignInterface + * @var DesignInterface */ protected $_design; /** - * @var \Magento\Framework\View\Design\Theme\ThemeProviderInterface + * @var ThemeProviderInterface */ protected $themeProvider; @@ -40,20 +51,28 @@ class Customization * @see self::_prepareThemeCustomizations() */ protected $_unassignedTheme; + /** + * @var StoreUserAgentThemeResolver|mixed|null + */ + private $storeThemesResolver; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\View\DesignInterface $design - * @param \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider + * @param StoreManagerInterface $storeManager + * @param DesignInterface $design + * @param ThemeProviderInterface $themeProvider + * @param StoreThemesResolverInterface|null $storeThemesResolver */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\View\DesignInterface $design, - \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider + StoreManagerInterface $storeManager, + DesignInterface $design, + ThemeProviderInterface $themeProvider, + ?StoreThemesResolverInterface $storeThemesResolver = null ) { $this->_storeManager = $storeManager; $this->_design = $design; $this->themeProvider = $themeProvider; + $this->storeThemesResolver = $storeThemesResolver + ?? ObjectManager::getInstance()->get(StoreThemesResolverInterface::class); } /** @@ -93,13 +112,14 @@ public function getStoresByThemes() { $storesByThemes = []; $stores = $this->_storeManager->getStores(); - /** @var $store \Magento\Store\Model\Store */ + /** @var $store Store */ foreach ($stores as $store) { - $themeId = $this->_getConfigurationThemeId($store); - if (!isset($storesByThemes[$themeId])) { - $storesByThemes[$themeId] = []; + foreach ($this->storeThemesResolver->getThemes($store) as $themeId) { + if (!isset($storesByThemes[$themeId])) { + $storesByThemes[$themeId] = []; + } + $storesByThemes[$themeId][] = $store; } - $storesByThemes[$themeId][] = $store; } return $storesByThemes; } @@ -107,8 +127,8 @@ public function getStoresByThemes() /** * Check if current theme has assigned to any store * - * @param \Magento\Framework\View\Design\ThemeInterface $theme - * @param null|\Magento\Store\Model\Store $store + * @param ThemeInterface $theme + * @param null|Store $store * @return bool */ public function isThemeAssignedToStore($theme, $store = null) @@ -133,8 +153,8 @@ public function hasThemeAssigned() /** * Is theme assigned to specific store * - * @param \Magento\Framework\View\Design\ThemeInterface $theme - * @param \Magento\Store\Model\Store $store + * @param ThemeInterface $theme + * @param Store $store * @return bool */ protected function _isThemeAssignedToSpecificStore($theme, $store) @@ -145,21 +165,21 @@ protected function _isThemeAssignedToSpecificStore($theme, $store) /** * Get configuration theme id * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return int */ protected function _getConfigurationThemeId($store) { return $this->_design->getConfigurationDesignTheme( - \Magento\Framework\App\Area::AREA_FRONTEND, + Area::AREA_FRONTEND, ['store' => $store] ); } /** * Fetch theme customization and sort them out to arrays: - * self::_assignedTheme and self::_unassignedTheme. * + * Set self::_assignedTheme and self::_unassignedTheme. * NOTE: To get into "assigned" list theme customization not necessary should be assigned to store-view directly. * It can be set to website or as default theme and be used by store-view via config fallback mechanism. * @@ -167,15 +187,15 @@ protected function _getConfigurationThemeId($store) */ protected function _prepareThemeCustomizations() { - /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection */ - $themeCollection = $this->themeProvider->getThemeCustomizations(\Magento\Framework\App\Area::AREA_FRONTEND); + /** @var Collection $themeCollection */ + $themeCollection = $this->themeProvider->getThemeCustomizations(Area::AREA_FRONTEND); $assignedThemes = $this->getStoresByThemes(); $this->_assignedTheme = []; $this->_unassignedTheme = []; - /** @var $theme \Magento\Framework\View\Design\ThemeInterface */ + /** @var $theme ThemeInterface */ foreach ($themeCollection as $theme) { if (isset($assignedThemes[$theme->getId()])) { $theme->setAssignedStores($assignedThemes[$theme->getId()]); diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index 8f81ace8c9047..143889364781f 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -4,10 +4,12 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Theme\Model\Design\Backend; -use Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface; use Magento\Config\Model\Config\Backend\File as BackendFile; +use Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface; use Magento\Framework\App\Cache\TypeListInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; @@ -15,13 +17,14 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Io\File as IoFileSystem; use Magento\Framework\Model\Context; use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Registry; use Magento\Framework\UrlInterface; +use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\MediaStorage\Model\File\UploaderFactory; use Magento\Theme\Model\Design\Config\FileUploader\FileProcessor; -use Magento\MediaStorage\Helper\File\Storage\Database; /** * File Backend @@ -40,6 +43,11 @@ class File extends BackendFile */ private $mime; + /** + * @var IoFileSystem + */ + private $ioFileSystem; + /** * @var Database */ @@ -58,6 +66,7 @@ class File extends BackendFile * @param AbstractDb|null $resourceCollection * @param array $data * @param Database $databaseHelper + * @param IoFileSystem $ioFileSystem * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -72,7 +81,8 @@ public function __construct( AbstractResource $resource = null, AbstractDb $resourceCollection = null, array $data = [], - Database $databaseHelper = null + Database $databaseHelper = null, + IoFileSystem $ioFileSystem = null ) { parent::__construct( $context, @@ -88,6 +98,7 @@ public function __construct( ); $this->urlBuilder = $urlBuilder; $this->databaseHelper = $databaseHelper ?: ObjectManager::getInstance()->get(Database::class); + $this->ioFileSystem = $ioFileSystem ?: ObjectManager::getInstance()->get(IoFileSystem::class); } /** @@ -108,11 +119,21 @@ public function beforeSave() __('%1 does not contain field \'file\'', $this->getData('field_config/field')) ); } + + if (!empty($this->getAllowedExtensions()) && + (!isset($this->ioFileSystem->getPathInfo($file)['extension']) || + !in_array($this->ioFileSystem->getPathInfo($file)['extension'], $this->getAllowedExtensions())) + ) { + throw new LocalizedException( + __('Something is wrong with the file upload settings.') + ); + } + if (isset($value['exists'])) { $this->setValue($file); return $this; } - + //phpcs:ignore Magento2.Functions.DiscouragedFunction $this->updateMediaDirectory(basename($file), $value['url']); @@ -196,7 +217,7 @@ protected function getStoreMediaUrl($fileName) $urlType = ['_type' => empty($baseUrl['type']) ? 'link' : (string)$baseUrl['type']]; $baseUrl = $baseUrl['value'] . '/'; } - return $this->urlBuilder->getBaseUrl($urlType) . $baseUrl . $fileName; + return $this->urlBuilder->getBaseUrl($urlType) . $baseUrl . $fileName; } /** diff --git a/app/code/Magento/Theme/Model/Indexer/Design/Config/Plugin/Store.php b/app/code/Magento/Theme/Model/Indexer/Design/Config/Plugin/Store.php index ca4a238fd1ff4..18ce1be2e68d1 100644 --- a/app/code/Magento/Theme/Model/Indexer/Design/Config/Plugin/Store.php +++ b/app/code/Magento/Theme/Model/Indexer/Design/Config/Plugin/Store.php @@ -6,7 +6,7 @@ namespace Magento\Theme\Model\Indexer\Design\Config\Plugin; use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Store\Model\Store as StoreStore; +use Magento\Store\Model\Store as StoreModel; use Magento\Theme\Model\Data\Design\Config; class Store @@ -19,42 +19,41 @@ class Store /** * @param IndexerRegistry $indexerRegistry */ - public function __construct( - IndexerRegistry $indexerRegistry - ) { + public function __construct(IndexerRegistry $indexerRegistry) + { $this->indexerRegistry = $indexerRegistry; } /** * Invalidate design config grid indexer on store creation * - * @param StoreStore $subject - * @param \Closure $proceed - * @return StoreStore + * @param StoreModel $subject + * @param StoreModel $result + * @return StoreModel * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function aroundSave(StoreStore $subject, \Closure $proceed) + public function afterSave(StoreModel $subject, StoreModel $result) { - $isObjectNew = $subject->getId() == 0; - $result = $proceed(); - if ($isObjectNew) { + if ($result->isObjectNew()) { $this->indexerRegistry->get(Config::DESIGN_CONFIG_GRID_INDEXER_ID)->invalidate(); } + return $result; } /** * Invalidate design config grid indexer on store removal * - * @param StoreStore $subject - * @param StoreStore $result - * @return StoreStore + * @param StoreModel $subject + * @param StoreModel $result + * @return StoreModel * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function afterDelete(StoreStore $subject, $result) + public function afterDelete(StoreModel $subject, $result) { $this->indexerRegistry->get(Config::DESIGN_CONFIG_GRID_INDEXER_ID)->invalidate(); + return $result; } } diff --git a/app/code/Magento/Theme/Model/ResourceModel/Theme/Grid/Collection.php b/app/code/Magento/Theme/Model/ResourceModel/Theme/Grid/Collection.php index c4a7bb11a78f7..4ee6880c8190d 100644 --- a/app/code/Magento/Theme/Model/ResourceModel/Theme/Grid/Collection.php +++ b/app/code/Magento/Theme/Model/ResourceModel/Theme/Grid/Collection.php @@ -7,7 +7,7 @@ /** * Theme grid collection - * @deprecated + * @deprecated 101.0.0 * @see \Magento\Theme\Ui\Component\Theme\DataProvider\SearchResult */ class Collection extends \Magento\Theme\Model\ResourceModel\Theme\Collection diff --git a/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php new file mode 100644 index 0000000000000..26bd5604294d1 --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Framework\App\Area; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; + +/** + * Store default theme resolver. + * + * Use system config fallback mechanism if no theme is directly assigned to the store-view. + */ +class StoreDefaultThemeResolver implements StoreThemesResolverInterface +{ + /** + * @var CollectionFactory + */ + private $themeCollectionFactory; + /** + * @var DesignInterface + */ + private $design; + /** + * @var ThemeInterface[] + */ + private $registeredThemes; + + /** + * @param CollectionFactory $themeCollectionFactory + * @param DesignInterface $design + */ + public function __construct( + CollectionFactory $themeCollectionFactory, + DesignInterface $design + ) { + $this->design = $design; + $this->themeCollectionFactory = $themeCollectionFactory; + } + + /** + * @inheritDoc + */ + public function getThemes(StoreInterface $store): array + { + $theme = $this->design->getConfigurationDesignTheme( + Area::AREA_FRONTEND, + ['store' => $store] + ); + $themes = []; + if ($theme) { + if (!is_numeric($theme)) { + $registeredThemes = $this->getRegisteredThemes(); + if (isset($registeredThemes[$theme])) { + $themes[] = $registeredThemes[$theme]->getId(); + } + } else { + $themes[] = $theme; + } + } + return $themes; + } + + /** + * Get system registered themes. + * + * @return ThemeInterface[] + */ + private function getRegisteredThemes(): array + { + if ($this->registeredThemes === null) { + $this->registeredThemes = []; + /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $collection */ + $collection = $this->themeCollectionFactory->create(); + $themes = $collection->loadRegisteredThemes(); + /** @var ThemeInterface $theme */ + foreach ($themes as $theme) { + $this->registeredThemes[$theme->getCode()] = $theme; + } + } + return $this->registeredThemes; + } +} diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php new file mode 100644 index 0000000000000..5be86c08f7c51 --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use InvalidArgumentException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; + +/** + * Store associated themes resolver. + */ +class StoreThemesResolver implements StoreThemesResolverInterface +{ + /** + * @var StoreThemesResolverInterface[] + */ + private $resolvers; + + /** + * @param StoreThemesResolverInterface[] $resolvers + */ + public function __construct( + array $resolvers + ) { + foreach ($resolvers as $resolver) { + if (!$resolver instanceof StoreThemesResolverInterface) { + throw new InvalidArgumentException( + sprintf( + 'Instance of %s is expected, got %s instead.', + StoreThemesResolverInterface::class, + get_class($resolver) + ) + ); + } + } + $this->resolvers = $resolvers; + } + + /** + * @inheritDoc + */ + public function getThemes(StoreInterface $store): array + { + $themes = []; + foreach ($this->resolvers as $resolver) { + foreach ($resolver->getThemes($store) as $theme) { + $themes[] = $theme; + } + } + return array_values(array_unique($themes)); + } +} diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php new file mode 100644 index 0000000000000..bb2cd73300c02 --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Store\Api\Data\StoreInterface; + +/** + * Store associated themes resolver. + */ +interface StoreThemesResolverInterface +{ + /** + * Get themes associated with a store view + * + * @param StoreInterface $store + * @return int[] + */ + public function getThemes(StoreInterface $store): array; +} diff --git a/app/code/Magento/Theme/Model/Theme/StoreUserAgentThemeResolver.php b/app/code/Magento/Theme/Model/Theme/StoreUserAgentThemeResolver.php new file mode 100644 index 0000000000000..fb5d68e37c99b --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreUserAgentThemeResolver.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; + +/** + * Store associated themes in user-agent rules resolver, + */ +class StoreUserAgentThemeResolver implements StoreThemesResolverInterface +{ + private const XML_PATH_THEME_USER_AGENT = 'design/theme/ua_regexp'; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** + * @var Json + */ + private $serializer; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param Json $serializer + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Json $serializer + ) { + $this->scopeConfig = $scopeConfig; + $this->serializer = $serializer; + } + + /** + * @inheritDoc + */ + public function getThemes(StoreInterface $store): array + { + $config = $this->scopeConfig->getValue( + self::XML_PATH_THEME_USER_AGENT, + ScopeInterface::SCOPE_STORE, + $store + ); + $rules = $config ? $this->serializer->unserialize($config) : []; + $themes = []; + if ($rules) { + $themes = array_values(array_unique(array_column($rules, 'value'))); + } + return $themes; + } +} diff --git a/app/code/Magento/Theme/Model/Wysiwyg/Storage.php b/app/code/Magento/Theme/Model/Wysiwyg/Storage.php index edf8c148f8e68..5c38d99dd6a22 100644 --- a/app/code/Magento/Theme/Model/Wysiwyg/Storage.php +++ b/app/code/Magento/Theme/Model/Wysiwyg/Storage.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem\DriverInterface; /** * Theme wysiwyg storage model @@ -18,11 +19,15 @@ class Storage { /** * Type font + * + * Represents the font type */ const TYPE_FONT = 'font'; /** * Type image + * + * Represents the image type */ const TYPE_IMAGE = 'image'; @@ -82,6 +87,11 @@ class Storage */ private $file; + /** + * @var DriverInterface + */ + private $filesystemDriver; + /** * Initialize dependencies * @@ -92,6 +102,7 @@ class Storage * @param \Magento\Framework\Url\EncoderInterface $urlEncoder * @param \Magento\Framework\Url\DecoderInterface $urlDecoder * @param \Magento\Framework\Filesystem\Io\File|null $file + * @param DriverInterface|null $filesystemDriver * * @throws \Magento\Framework\Exception\FileSystemException */ @@ -102,7 +113,8 @@ public function __construct( \Magento\Framework\Image\AdapterFactory $imageFactory, \Magento\Framework\Url\EncoderInterface $urlEncoder, \Magento\Framework\Url\DecoderInterface $urlDecoder, - \Magento\Framework\Filesystem\Io\File $file = null + \Magento\Framework\Filesystem\Io\File $file = null, + DriverInterface $filesystemDriver = null ) { $this->mediaWriteDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->_helper = $helper; @@ -113,6 +125,8 @@ public function __construct( $this->file = $file ?: ObjectManager::getInstance()->get( \Magento\Framework\Filesystem\Io\File::class ); + $this->filesystemDriver = $filesystemDriver ?: ObjectManager::getInstance() + ->get(DriverInterface::class); } /** @@ -327,8 +341,12 @@ public function deleteDirectory($path) { $rootCmp = rtrim($this->_helper->getStorageRoot(), '/'); $pathCmp = rtrim($path, '/'); + $absolutePath = rtrim( + $this->filesystemDriver->getRealPathSafety($this->mediaWriteDirectory->getAbsolutePath($path)), + '/' + ); - if ($rootCmp == $pathCmp) { + if ($rootCmp == $pathCmp || $rootCmp === $absolutePath) { throw new \Magento\Framework\Exception\LocalizedException( __('We can\'t delete root directory %1 right now.', $path) ); diff --git a/app/code/Magento/Theme/Plugin/Data/Collection.php b/app/code/Magento/Theme/Plugin/Data/Collection.php new file mode 100644 index 0000000000000..11ff95db25769 --- /dev/null +++ b/app/code/Magento/Theme/Plugin/Data/Collection.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Plugin\Data; + +use Magento\Framework\Data\Collection as DataCollection; + +/** + * Plugin to return last page if current page greater then collection size. + */ +class Collection +{ + /** + * Return last page if current page greater then last page. + * + * @param DataCollection $subject + * @param int $result + * @return int + */ + public function afterGetCurPage(DataCollection $subject, int $result): int + { + if ($result > $subject->getLastPageNumber()) { + $result = 1; + } + + return $result; + } +} diff --git a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php similarity index 88% rename from lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php rename to app/code/Magento/Theme/Plugin/LoadDesignPlugin.php index 2cda49c43c2ce..c4f8d3a905d0f 100644 --- a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php +++ b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -namespace Magento\Framework\App\Action\Plugin; +namespace Magento\Theme\Plugin; use Magento\Framework\App\ActionInterface; use Magento\Framework\Config\Dom\ValidationException; @@ -21,12 +21,12 @@ class LoadDesignPlugin /** * @var DesignLoader */ - protected $_designLoader; + private $designLoader; /** * @var MessageManagerInterface */ - protected $messageManager; + private $messageManager; /** * @param DesignLoader $designLoader @@ -36,7 +36,7 @@ public function __construct( DesignLoader $designLoader, MessageManagerInterface $messageManager ) { - $this->_designLoader = $designLoader; + $this->designLoader = $designLoader; $this->messageManager = $messageManager; } @@ -50,7 +50,7 @@ public function __construct( public function beforeExecute(ActionInterface $subject) { try { - $this->_designLoader->load(); + $this->designLoader->load(); } catch (LocalizedException $e) { if ($e->getPrevious() instanceof ValidationException) { /** @var MessageInterface $message */ 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/Test/Unit/Block/Html/PagerTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php index ac16c56b17f1b..fd0ef1db0219a 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php @@ -91,6 +91,60 @@ public function testGetPages(): void $this->assertEquals($expectedPages, $this->pager->getPages()); } + /** + * Test get limit url. + * + * @dataProvider limitUrlDataProvider + * + * @param int $page + * @param int $size + * @param int $limit + * @param array $expectedParams + * @return void + */ + public function testGetLimitUrl(int $page, int $size, int $limit, array $expectedParams): void + { + $expectedArray = [ + '_current' => true, + '_escape' => true, + '_use_rewrite' => true, + '_fragment' => null, + '_query' => $expectedParams, + ]; + + $collectionMock = $this->createMock(Collection::class); + $collectionMock->expects($this->once()) + ->method('getCurPage') + ->willReturn($page); + $collectionMock->expects($this->once()) + ->method('getSize') + ->willReturn($size); + $this->setCollectionProperty($collectionMock); + + $this->urlBuilderMock->expects($this->once()) + ->method('getUrl') + ->with('*/*/*', $expectedArray); + + $this->pager->getLimitUrl($limit); + } + + /** + * DataProvider for testGetLimitUrl + * + * @return array + */ + public function limitUrlDataProvider(): array + { + return [ + [2, 21, 10, ['limit' => 10]], + [3, 21, 10, ['limit' => 10]], + [2, 21, 20, ['limit' => 20]], + [3, 21, 50, ['limit' => 50, 'p' => null]], + [2, 11, 20, ['limit' => 20, 'p' => null]], + [4, 40, 20, ['limit' => 20, 'p' => 2]], + ]; + } + /** * Set Collection * diff --git a/app/code/Magento/Theme/Test/Unit/Controller/Result/AsyncCssPluginTest.php b/app/code/Magento/Theme/Test/Unit/Controller/Result/AsyncCssPluginTest.php index d433f745400d0..eed3e05c4abdd 100644 --- a/app/code/Magento/Theme/Test/Unit/Controller/Result/AsyncCssPluginTest.php +++ b/app/code/Magento/Theme/Test/Unit/Controller/Result/AsyncCssPluginTest.php @@ -7,13 +7,14 @@ namespace Magento\Theme\Test\Unit\Controller\Result; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\App\Response\Http; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Store\Model\ScopeInterface; use Magento\Theme\Controller\Result\AsyncCssPlugin; -use PHPUnit\Framework\MockObject\MockObject; +use Magento\Framework\App\Response\Http; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\View\Result\Layout; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; /** * Unit test for Magento\Theme\Test\Unit\Controller\Result\AsyncCssPlugin. @@ -37,6 +38,9 @@ class AsyncCssPluginTest extends TestCase */ private $httpMock; + /** @var Layout|MockObject */ + private $layoutMock; + /** * @inheritdoc */ @@ -48,6 +52,7 @@ protected function setUp(): void ->getMockForAbstractClass(); $this->httpMock = $this->createMock(Http::class); + $this->layoutMock = $this->createMock(Layout::class); $objectManager = new ObjectManagerHelper($this); $this->plugin = $objectManager->getObject( @@ -59,87 +64,134 @@ protected function setUp(): void } /** - * Data Provider for before send response + * Data Provider for testAfterRenderResult * * @return array */ - public function sendResponseDataProvider(): array + public function renderResultDataProvider(): array { return [ [ - "content" => "<body><h1>Test Title</h1>" . - "<link rel=\"stylesheet\" href=\"css/critical.css\" />" . - "<p>Test Content</p></body>", + "content" => "<head><link rel=\"stylesheet\" href=\"css/async.css\">" . + "<style>.critical-css{}</style>" . + "</head>", + "flag" => true, + "result" => "<head><style>.critical-css{}</style>\n" . + "<link " . + "rel=\"stylesheet\" media=\"print\" onload=\"this.onload=null;this.media='all'\" " . + "href=\"css/async.css\">\n" . + "</head>", + ], + [ + "content" => "<head><link rel=\"stylesheet\" href=\"css/async.css\">" . + "<link rel=\"preload\" href=\"other-file.html\">" . + "</head>", "flag" => true, - "result" => "<body><h1>Test Title</h1>" . - "<link rel=\"preload\" as=\"style\" media=\"all\"" . - " onload=\"this.onload=null;this.rel='stylesheet'\" href=\"css/critical.css\" />" . - "<p>Test Content</p>" . - "<link rel=\"stylesheet\" href=\"css/critical.css\" />" . - "\n</body>" + "result" => "<head><link rel=\"preload\" href=\"other-file.html\">\n" . + "<link " . + "rel=\"stylesheet\" media=\"print\" onload=\"this.onload=null;this.media='all'\" " . + "href=\"css/async.css\">\n" . + "</head>", ], [ - "content" => "<body><p>Test Content</p></body>", + "content" => "<head><link rel=\"stylesheet\" href=\"css/async.css\">" . + "<link rel=\"preload\" href=\"other-file.html\">" . + "</head>", "flag" => false, - "result" => "<body><p>Test Content</p></body>" + "result" => "<head><link rel=\"stylesheet\" href=\"css/async.css\">" . + "<link rel=\"preload\" href=\"other-file.html\">" . + "</head>", ], [ - "content" => "<body><p>Test Content</p></body>", + "content" => "<head><link rel=\"stylesheet\" href=\"css/first.css\">" . + "<link rel=\"stylesheet\" href=\"css/second.css\">" . + "<style>.critical-css{}</style>" . + "</head>", "flag" => true, - "result" => "<body><p>Test Content</p></body>" + "result" => "<head><style>.critical-css{}</style>\n" . + "<link " . + "rel=\"stylesheet\" media=\"print\" onload=\"this.onload=null;this.media='all'\" " . + "href=\"css/first.css\">\n" . + "<link " . + "rel=\"stylesheet\" media=\"print\" onload=\"this.onload=null;this.media='all'\" " . + "href=\"css/second.css\">\n" . + "</head>", + ], + [ + "content" => "<head><style>.critical-css{}</style></head>", + "flag" => false, + "result" => "<head><style>.critical-css{}</style></head>" + ], + [ + "content" => "<head><style>.critical-css{}</style></head>", + "flag" => true, + "result" => "<head><style>.critical-css{}</style></head>" ] ]; } /** - * Test beforeSendResponse + * Test after render result response * * @param string $content * @param bool $isSetFlag * @param string $result * @return void - * @dataProvider sendResponseDataProvider + * @dataProvider renderResultDataProvider */ - public function testBeforeSendResponse($content, $isSetFlag, $result): void + public function testAfterRenderResult(string $content, bool $isSetFlag, string $result): void { - $this->httpMock->expects($this->once()) - ->method('getContent') + // Given (context) + $this->httpMock->method('getContent') ->willReturn($content); - $this->scopeConfigMock->expects($this->once()) - ->method('isSetFlag') - ->with( - self::STUB_XML_PATH_USE_CSS_CRITICAL_PATH, - ScopeInterface::SCOPE_STORE - ) + $this->scopeConfigMock->method('isSetFlag') + ->with(self::STUB_XML_PATH_USE_CSS_CRITICAL_PATH, ScopeInterface::SCOPE_STORE) ->willReturn($isSetFlag); + // Expects $this->httpMock->expects($this->any()) ->method('setContent') ->with($result); - $this->plugin->beforeSendResponse($this->httpMock); + // When + $this->plugin->afterRenderResult($this->layoutMock, $this->layoutMock, $this->httpMock); } /** - * Test BeforeSendResponse if content is not a string + * Data Provider for testAfterRenderResultIfGetContentIsNotAString() * + * @return array + */ + public function ifGetContentIsNotAStringDataProvider(): array + { + return [ + [ + 'content' => null + ] + ]; + } + + /** + * Test AfterRenderResult if content is not a string + * + * @param $content * @return void + * @dataProvider ifGetContentIsNotAStringDataProvider */ - public function testIfGetContentIsNotAString(): void + public function testAfterRenderResultIfGetContentIsNotAString($content): void { + $this->scopeConfigMock->method('isSetFlag') + ->with(self::STUB_XML_PATH_USE_CSS_CRITICAL_PATH, ScopeInterface::SCOPE_STORE) + ->willReturn(true); + $this->httpMock->expects($this->once()) ->method('getContent') - ->willReturn([]); + ->willReturn($content); - $this->scopeConfigMock->expects($this->any()) - ->method('isSetFlag') - ->with( - self::STUB_XML_PATH_USE_CSS_CRITICAL_PATH, - ScopeInterface::SCOPE_STORE - ) - ->willReturn(false); + $this->httpMock->expects($this->never()) + ->method('setContent'); - $this->plugin->beforeSendResponse($this->httpMock); + $this->plugin->afterRenderResult($this->layoutMock, $this->layoutMock, $this->httpMock); } } diff --git a/app/code/Magento/Theme/Test/Unit/Helper/StorageTest.php b/app/code/Magento/Theme/Test/Unit/Helper/StorageTest.php index 2df86d5263e3d..0d5e5fa393398 100644 --- a/app/code/Magento/Theme/Test/Unit/Helper/StorageTest.php +++ b/app/code/Magento/Theme/Test/Unit/Helper/StorageTest.php @@ -19,9 +19,10 @@ use Magento\Framework\Url\EncoderInterface; use Magento\Framework\View\Design\Theme\Customization; use Magento\Framework\View\Design\Theme\FlyweightFactory; +use Magento\Framework\Filesystem\DriverInterface; use Magento\Theme\Helper\Storage; -use Magento\Theme\Model\Theme; use PHPUnit\Framework\MockObject\MockObject; +use Magento\Theme\Model\Theme; use PHPUnit\Framework\TestCase; /** @@ -91,6 +92,11 @@ class StorageTest extends TestCase protected $requestParams; + /** + * @var DriverInterface|MockObject + */ + private $filesystemDriver; + protected function setUp(): void { $this->customizationPath = '/' . implode('/', ['var', 'theme']); @@ -117,6 +123,7 @@ protected function setUp(): void $this->contextHelper->expects($this->any())->method('getUrlEncoder')->willReturn($this->urlEncoder); $this->contextHelper->expects($this->any())->method('getUrlDecoder')->willReturn($this->urlDecoder); $this->themeFactory->expects($this->any())->method('create')->willReturn($this->theme); + $this->filesystemDriver = $this->createMock(DriverInterface::class); $this->theme->expects($this->any()) ->method('getCustomization') @@ -135,7 +142,9 @@ protected function setUp(): void $this->contextHelper, $this->filesystem, $this->session, - $this->themeFactory + $this->themeFactory, + null, + $this->filesystemDriver ); } @@ -279,6 +288,9 @@ public function testGetThumbnailPathNotFound() { $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('The image not found'); + + $this->filesystemDriver->method('getRealpathSafety') + ->willReturnArgument(0); $image = 'notFoundImage.png'; $root = '/image'; $sourceNode = '/not/a/root'; @@ -456,4 +468,67 @@ public function testGetThemeNotFound() ); $helper->getStorageRoot(); } + + /** + * @dataProvider getCurrentPathDataProvider + */ + public function testGetCurrentPathCachesResult() + { + $this->request->expects($this->once()) + ->method('getParam') + ->with(Storage::PARAM_NODE) + ->willReturn(Storage::NODE_ROOT); + + $actualPath = $this->helper->getCurrentPath(); + self::assertSame('/image', $actualPath); + } + + /** + * @dataProvider getCurrentPathDataProvider + */ + public function testGetCurrentPath( + string $expectedPath, + string $requestedPath, + ?bool $isDirectory = null, + ?string $relativePath = null, + ?string $resolvedPath = null + ) { + $this->directoryWrite->method('isDirectory') + ->willReturn($isDirectory); + + $this->directoryWrite->method('getRelativePath') + ->willReturn($relativePath); + + $this->urlDecoder->method('decode') + ->willReturnArgument(0); + + if ($resolvedPath) { + $this->filesystemDriver->method('getRealpathSafety') + ->willReturn($resolvedPath); + } else { + $this->filesystemDriver->method('getRealpathSafety') + ->willReturnArgument(0); + } + + $this->request->method('getParam') + ->with(Storage::PARAM_NODE) + ->willReturn($requestedPath); + + $actualPath = $this->helper->getCurrentPath(); + + self::assertSame($expectedPath, $actualPath); + } + + public function getCurrentPathDataProvider(): array + { + $rootPath = '/' . \Magento\Theme\Model\Wysiwyg\Storage::TYPE_IMAGE; + + return [ + 'requested path "root" should short-circuit' => [$rootPath, Storage::NODE_ROOT], + 'non-existent directory should default to the base path' => [$rootPath, $rootPath . '/foo'], + 'requested path that resolves to a bad path should default to root' => + [$rootPath, $rootPath . '/something', true, null, '/bar'], + 'real path should resolve to relative path' => ['foo/', $rootPath . '/foo', true, 'foo/'], + ]; + } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php index 82678d4b4277d..438853b9935e6 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php @@ -13,9 +13,10 @@ use Magento\Framework\App\Area; use Magento\Framework\DataObject; use Magento\Framework\View\DesignInterface; +use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Theme\Model\Config\Customization; -use Magento\Theme\Model\ResourceModel\Theme\Collection; +use Magento\Theme\Model\Theme\StoreThemesResolverInterface; use Magento\Theme\Model\Theme\ThemeProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -32,47 +33,37 @@ class CustomizationTest extends TestCase */ protected $designPackage; - /** - * @var Collection - */ - protected $themeCollection; - /** * @var Customization */ protected $model; /** - * @var ThemeProvider|\PHPUnit\Framework\MockObject_MockBuilder + * @var ThemeProvider|MockObject */ protected $themeProviderMock; + /** + * @var StoreThemesResolverInterface|MockObject + */ + private $storeThemesResolver; protected function setUp(): void { - $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) - ->getMock(); - $this->designPackage = $this->getMockBuilder(DesignInterface::class) - ->getMock(); - $this->themeCollection = $this->getMockBuilder(Collection::class) - ->disableOriginalConstructor() - ->getMock(); - - $collectionFactory = $this->getMockBuilder(\Magento\Theme\Model\ResourceModel\Theme\CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $collectionFactory->expects($this->any())->method('create')->willReturn($this->themeCollection); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); + $this->designPackage = $this->getMockBuilder(DesignInterface::class)->getMock(); $this->themeProviderMock = $this->getMockBuilder(ThemeProvider::class) ->disableOriginalConstructor() ->setMethods(['getThemeCustomizations', 'getThemeByFullPath']) ->getMock(); + $this->storeThemesResolver = $this->createMock(StoreThemesResolverInterface::class); + $this->model = new Customization( $this->storeManager, $this->designPackage, - $this->themeProviderMock + $this->themeProviderMock, + $this->storeThemesResolver ); } @@ -84,13 +75,15 @@ protected function setUp(): void */ public function testGetAssignedThemeCustomizations() { - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); - + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); + + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $this->themeProviderMock->expects($this->once()) ->method('getThemeCustomizations') @@ -108,13 +101,15 @@ public function testGetAssignedThemeCustomizations() */ public function testGetUnassignedThemeCustomizations() { + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $this->themeProviderMock->expects($this->once()) ->method('getThemeCustomizations') @@ -131,13 +126,15 @@ public function testGetUnassignedThemeCustomizations() */ public function testGetStoresByThemes() { + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $stores = $this->model->getStoresByThemes(); $this->assertArrayHasKey($this->getAssignedTheme()->getId(), $stores); @@ -148,15 +145,17 @@ public function testGetStoresByThemes() * @covers \Magento\Theme\Model\Config\Customization::_getConfigurationThemeId * @covers \Magento\Theme\Model\Config\Customization::__construct */ - public function testIsThemeAssignedToDefaultStore() + public function testIsThemeAssignedToAnyStore() { + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $this->themeProviderMock->expects($this->once()) ->method('getThemeCustomizations') @@ -198,10 +197,10 @@ protected function getUnassignedTheme() } /** - * @return DataObject + * @return StoreInterface|MockObject */ protected function getStore() { - return new DataObject(['id' => 55]); + return $this->createConfiguredMock(StoreInterface::class, ['getId' => 55]); } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php index 7a48aa968392a..78a56013ae042 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php @@ -11,14 +11,17 @@ use Magento\Framework\App\Cache\TypeListInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Data\Collection\AbstractDb; use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Io\File as IoFileSystem; use Magento\Framework\Model\Context; use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Registry; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + use Magento\Framework\UrlInterface; use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\Theme\Model\Design\Backend\File; @@ -31,13 +34,16 @@ class FileTest extends TestCase { /** @var WriteInterface|MockObject */ - protected $mediaDirectory; + private $mediaDirectory; /** @var UrlInterface|MockObject */ - protected $urlBuilder; + private $urlBuilder; /** @var File */ - protected $fileBackend; + private $fileBackend; + + /** @var IoFileSystem|\PHPUnit\Framework\MockObject\MockObject */ + private $ioFileSystem; /** * @var Mime|MockObject @@ -49,6 +55,9 @@ class FileTest extends TestCase */ private $databaseHelper; + /** + * @inheritdoc + */ protected function setUp(): void { $context = $this->getMockObject(Context::class); @@ -62,16 +71,18 @@ protected function setUp(): void $filesystem = $this->getMockBuilder(Filesystem::class) ->disableOriginalConstructor() ->getMock(); - $this->mediaDirectory = $this->getMockBuilder(WriteInterface::class) + $this->mediaDirectory = $this->getMockBuilder( + WriteInterface::class + ) ->getMockForAbstractClass(); - $filesystem->expects($this->once()) ->method('getDirectoryWrite') ->with(DirectoryList::MEDIA) ->willReturn($this->mediaDirectory); $this->urlBuilder = $this->getMockBuilder(UrlInterface::class) ->getMockForAbstractClass(); - + $this->ioFileSystem = $this->getMockBuilder(IoFileSystem::class) + ->getMockForAbstractClass(); $this->mime = $this->getMockBuilder(Mime::class) ->disableOriginalConstructor() ->getMock(); @@ -86,7 +97,6 @@ protected function setUp(): void $abstractDb = $this->getMockBuilder(AbstractDb::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->fileBackend = new File( $context, $registry, @@ -99,7 +109,8 @@ protected function setUp(): void $abstractResource, $abstractDb, [], - $this->databaseHelper + $this->databaseHelper, + $this->ioFileSystem ); $objectManager = new ObjectManager($this); @@ -110,17 +121,22 @@ protected function setUp(): void ); } + /** + * @inheritdoc + */ protected function tearDown(): void { unset($this->fileBackend); } /** + * Gets the mock object. + * * @param string $class * @param array $methods * @return MockObject */ - protected function getMockObject($class, $methods = []) + private function getMockObject(string $class, array $methods = []): \PHPUnit\Framework\MockObject\MockObject { $builder = $this->getMockBuilder($class) ->disableOriginalConstructor(); @@ -131,15 +147,20 @@ protected function getMockObject($class, $methods = []) } /** + * Gets mock objects for abstract class. + * * @param string $class * @return MockObject */ - protected function getMockObjectForAbstractClass($class) + private function getMockObjectForAbstractClass(string $class): \PHPUnit\Framework\MockObject\MockObject { return $this->getMockBuilder($class) ->getMockForAbstractClass(); } + /** + * Test for afterLoad method. + */ public function testAfterLoad() { $value = 'filename.jpg'; @@ -147,16 +168,18 @@ public function testAfterLoad() $absoluteFilePath = '/absolute_path/' . $value; - $this->fileBackend->setValue($value); - $this->fileBackend->setFieldConfig( + $this->fileBackend->setData( [ - 'upload_dir' => [ - 'value' => 'value', - 'config' => 'system/filesystem/media', - ], - 'base_url' => [ - 'type' => 'media', - 'value' => 'design/file' + 'value' => $value, + 'field_config' => [ + 'upload_dir' => [ + 'value' => 'value', + 'config' => 'system/filesystem/media', + ], + 'base_url' => [ + 'type' => 'media', + 'value' => 'design/file' + ], ], ] ); @@ -169,7 +192,6 @@ public function testAfterLoad() ->method('getAbsolutePath') ->with('value/' . $value) ->willReturn($absoluteFilePath); - $this->urlBuilder->expects($this->once()) ->method('getBaseUrl') ->with(['_type' => UrlInterface::URL_TYPE_MEDIA]) @@ -182,12 +204,10 @@ public function testAfterLoad() ->method('stat') ->with('value/' . $value) ->willReturn(['size' => 234234]); - $this->mime->expects($this->once()) ->method('getMimeType') ->with($absoluteFilePath) ->willReturn($mime); - $this->fileBackend->afterLoad(); $this->assertEquals( [ @@ -205,29 +225,32 @@ public function testAfterLoad() } /** + * Test for beforeSave method. + * * @dataProvider beforeSaveDataProvider * @param string $fileName + * @throws LocalizedException */ - public function testBeforeSave($fileName) + public function testBeforeSave(string $fileName) { $expectedFileName = basename($fileName); $expectedTmpMediaPath = 'tmp/design/file/' . $expectedFileName; - $this->fileBackend->setScope('store'); - $this->fileBackend->setScopeId(1); - $this->fileBackend->setValue( - [ - [ - 'url' => 'http://magento2.com/pub/media/tmp/image/' . $fileName, - 'file' => $fileName, - 'size' => 234234, - ] - ] - ); - $this->fileBackend->setFieldConfig( + $this->fileBackend->setData( [ - 'upload_dir' => [ - 'value' => 'value', - 'config' => 'system/filesystem/media', + 'scope' => 'store', + 'scope_id' => 1, + 'value' => [ + [ + 'url' => 'http://magento2.com/pub/media/tmp/image/' . $fileName, + 'file' => $fileName, + 'size' => 234234, + ] + ], + 'field_config' => [ + 'upload_dir' => [ + 'value' => 'value', + 'config' => 'system/filesystem/media', + ], ], ] ); @@ -250,13 +273,15 @@ public function testBeforeSave($fileName) } /** + * Data provider for testBeforeSave. + * * @return array */ - public function beforeSaveDataProvider() + public function beforeSaveDataProvider(): array { return [ 'Normal file name' => ['filename.jpg'], - 'Vulnerable file name' => ['../../../../../../../../etc/passwd'], + 'Vulnerable file name' => ['../../../../../../../../etc/pass'], ]; } @@ -277,19 +302,27 @@ public function testBeforeSaveWithoutFile() $this->fileBackend->beforeSave(); } + /** + * Test for beforeSave method with existing file. + * + * @throws LocalizedException + */ public function testBeforeSaveWithExistingFile() { $value = 'filename.jpg'; - $this->fileBackend->setValue( + $this->fileBackend->setData( [ - [ - 'url' => 'http://magento2.com/pub/media/tmp/image/' . $value, - 'file' => $value, - 'size' => 234234, - 'exists' => true - ] + 'value' => [ + [ + 'url' => 'http://magento2.com/pub/media/tmp/image/' . $value, + 'file' => $value, + 'size' => 234234, + 'exists' => true + ] + ], ] ); + $this->fileBackend->beforeSave(); $this->assertEquals( $value, @@ -303,6 +336,7 @@ public function testBeforeSaveWithExistingFile() * @param string $path * @param string $filename * @dataProvider getRelativeMediaPathDataProvider + * @throws \ReflectionException */ public function testGetRelativeMediaPath(string $path, string $filename) { @@ -324,7 +358,7 @@ public function getRelativeMediaPathDataProvider(): array { return [ 'Normal path' => ['pub/media/', 'filename.jpg'], - 'Complex path' => ['somepath/pub/media/', 'filename.jpg'], + 'Complex path' => ['some_path/pub/media/', 'filename.jpg'], ]; } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/ImageTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/ImageTest.php new file mode 100644 index 0000000000000..f1c2b1d755971 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/ImageTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Design\Backend; + +use Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use Magento\Framework\UrlInterface; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\MediaStorage\Model\File\UploaderFactory; +use Magento\Theme\Model\Design\Backend\Image; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ImageTest extends \PHPUnit\Framework\TestCase +{ + /** @var Image */ + private $imageBackend; + + /** @var File */ + private $ioFileSystem; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $context = $this->getMockObject(Context::class); + $registry = $this->getMockObject(Registry::class); + $config = $this->getMockObject(ScopeConfigInterface::class); + $cacheTypeList = $this->getMockObject(TypeListInterface::class); + $uploaderFactory = $this->getMockObject(UploaderFactory::class); + $requestData = $this->getMockObject(RequestDataInterface::class); + $filesystem = $this->getMockObject(Filesystem::class); + $urlBuilder = $this->getMockObject(UrlInterface::class); + $databaseHelper = $this->getMockObject(Database::class); + $abstractResource = $this->getMockObject(AbstractResource::class); + $abstractDb = $this->getMockObject(AbstractDb::class); + $this->ioFileSystem = $this->getMockObject(File::class); + $this->imageBackend = new Image( + $context, + $registry, + $config, + $cacheTypeList, + $uploaderFactory, + $requestData, + $filesystem, + $urlBuilder, + $abstractResource, + $abstractDb, + [], + $databaseHelper, + $this->ioFileSystem + ); + } + + /** + * @inheritdoc + */ + public function tearDown(): void + { + unset($this->imageBackend); + } + + /** + * @param string $class + * @param array $methods + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function getMockObject(string $class, array $methods = []): \PHPUnit\Framework\MockObject\MockObject + { + $builder = $this->getMockBuilder($class) + ->disableOriginalConstructor(); + if (count($methods)) { + $builder->setMethods($methods); + } + return $builder->getMock(); + } + + /** + * Test for beforeSave method with invalid file extension. + */ + public function testBeforeSaveWithInvalidExtensionFile() + { + $this->expectException( + \Magento\Framework\Exception\LocalizedException::class + ); + $this->expectExceptionMessage( + 'Something is wrong with the file upload settings.' + ); + + $invalidFileName = 'fileName.invalidExtension'; + $this->imageBackend->setData( + [ + 'value' => [ + [ + 'file' => $invalidFileName, + ] + ], + ] + ); + $expectedPathInfo = [ + 'extension' => 'invalidExtension' + ]; + $this->ioFileSystem + ->expects($this->any()) + ->method('getPathInfo') + ->with($invalidFileName) + ->willReturn($expectedPathInfo); + $this->imageBackend->beforeSave(); + } +} diff --git a/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/Config/Plugin/StoreTest.php b/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/Config/Plugin/StoreTest.php index b61246cc7583f..1d48c0fe04e7a 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/Config/Plugin/StoreTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/Config/Plugin/StoreTest.php @@ -9,6 +9,7 @@ use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Store\Model\Store as StoreModel; use Magento\Theme\Model\Data\Design\Config; use Magento\Theme\Model\Indexer\Design\Config\Plugin\Store; use PHPUnit\Framework\MockObject\MockObject; @@ -17,10 +18,10 @@ class StoreTest extends TestCase { /** @var Store */ - protected $model; + private $model; /** @var IndexerRegistry|MockObject */ - protected $indexerRegistryMock; + private $indexerRegistryMock; protected function setUp(): void { @@ -31,21 +32,15 @@ protected function setUp(): void $this->model = new Store($this->indexerRegistryMock); } - public function testAroundSave() + public function testAfterSave(): void { - $subjectId = 0; - - /** @var \Magento\Store\Model\Store|MockObject $subjectMock */ - $subjectMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + /** @var StoreModel|MockObject $subjectMock */ + $subjectMock = $this->getMockBuilder(StoreModel::class) ->disableOriginalConstructor() ->getMock(); $subjectMock->expects($this->once()) - ->method('getId') - ->willReturn($subjectId); - - $closureMock = function () use ($subjectMock) { - return $subjectMock; - }; + ->method('isObjectNew') + ->willReturn(true); /** @var IndexerInterface|MockObject $indexerMock */ $indexerMock = $this->getMockBuilder(IndexerInterface::class) @@ -58,35 +53,29 @@ public function testAroundSave() ->with(Config::DESIGN_CONFIG_GRID_INDEXER_ID) ->willReturn($indexerMock); - $this->assertEquals($subjectMock, $this->model->aroundSave($subjectMock, $closureMock)); + $this->assertSame($subjectMock, $this->model->afterSave($subjectMock, $subjectMock)); } - public function testAroundSaveWithExistentSubject() + public function testAfterSaveWithExistentSubject(): void { - $subjectId = 1; - - /** @var \Magento\Store\Model\Store|MockObject $subjectMock */ - $subjectMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + /** @var StoreModel|MockObject $subjectMock */ + $subjectMock = $this->getMockBuilder(StoreModel::class) ->disableOriginalConstructor() ->getMock(); $subjectMock->expects($this->once()) - ->method('getId') - ->willReturn($subjectId); - - $closureMock = function () use ($subjectMock) { - return $subjectMock; - }; + ->method('isObjectNew') + ->willReturn(false); $this->indexerRegistryMock->expects($this->never()) ->method('get'); - $this->assertEquals($subjectMock, $this->model->aroundSave($subjectMock, $closureMock)); + $this->assertSame($subjectMock, $this->model->afterSave($subjectMock, $subjectMock)); } - public function testAfterDelete() + public function testAfterDelete(): void { - /** @var \Magento\Store\Model\Store|MockObject $subjectMock */ - $subjectMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + /** @var StoreModel|MockObject $subjectMock */ + $subjectMock = $this->getMockBuilder(StoreModel::class) ->disableOriginalConstructor() ->getMock(); @@ -101,6 +90,6 @@ public function testAfterDelete() ->with(Config::DESIGN_CONFIG_GRID_INDEXER_ID) ->willReturn($indexerMock); - $this->assertEquals($subjectMock, $this->model->afterDelete($subjectMock, $subjectMock)); + $this->assertSame($subjectMock, $this->model->afterDelete($subjectMock, $subjectMock)); } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php new file mode 100644 index 0000000000000..939b47a42ce85 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Theme; + +use ArrayIterator; +use Magento\Framework\App\Area; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\ResourceModel\Theme\Collection; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; +use Magento\Theme\Model\Theme\StoreDefaultThemeResolver; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test store default theme resolver. + */ +class StoreDefaultThemeResolverTest extends TestCase +{ + /** + * @var DesignInterface|MockObject + */ + private $design; + /** + * @var StoreDefaultThemeResolver + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $themeCollectionFactory = $this->createMock(CollectionFactory::class); + $this->design = $this->createMock(DesignInterface::class); + $this->model = new StoreDefaultThemeResolver( + $themeCollectionFactory, + $this->design + ); + $registeredThemes = []; + $registeredThemes[] = $this->createConfiguredMock( + ThemeInterface::class, + [ + 'getId' => 1, + 'getCode' => 'Magento/luma', + ] + ); + $registeredThemes[] = $this->createConfiguredMock( + ThemeInterface::class, + [ + 'getId' => 2, + 'getCode' => 'Magento/blank', + ] + ); + $collection = $this->createMock(Collection::class); + $collection->method('getIterator') + ->willReturn(new ArrayIterator($registeredThemes)); + $collection->method('loadRegisteredThemes') + ->willReturnSelf(); + $themeCollectionFactory->method('create') + ->willReturn($collection); + } + + /** + * Test that method returns default theme associated to given store. + * + * @param string|null $defaultTheme + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(?string $defaultTheme, array $expected): void + { + $store = $this->createMock(StoreInterface::class); + $this->design->expects($this->once()) + ->method('getConfigurationDesignTheme') + ->with( + Area::AREA_FRONTEND, + ['store' => $store] + ) + ->willReturn($defaultTheme); + $this->assertEquals($expected, $this->model->getThemes($store)); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + null, + [] + ], + [ + '1', + [1] + ], + [ + 'Magento/blank', + [2] + ], + [ + 'Magento/theme', + [] + ] + ]; + } +} diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php new file mode 100644 index 0000000000000..b80ec4ae83887 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Theme; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\Theme\StoreThemesResolver; +use Magento\Theme\Model\Theme\StoreThemesResolverInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test store composite themes resolver model. + */ +class StoreThemesResolverTest extends TestCase +{ + /** + * @var StoreThemesResolverInterface[]|MockObject[] + */ + private $resolvers; + /** + * @var StoreThemesResolver + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->resolvers = []; + $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class); + $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class); + $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class); + $this->model = new StoreThemesResolver($this->resolvers); + } + + /** + * Test that constructor SHOULD throw an exception when resolver is not instance of StoreThemesResolverInterface. + */ + public function testInvalidConstructorArguments(): void + { + $resolver = $this->createMock(StoreInterface::class); + $this->expectExceptionObject( + new \InvalidArgumentException( + sprintf( + 'Instance of %s is expected, got %s instead.', + StoreThemesResolverInterface::class, + get_class($resolver) + ) + ) + ); + $this->model = new StoreThemesResolver( + [ + $resolver + ] + ); + } + + /** + * Test that method returns aggregated themes from resolvers + * + * @param array $themes + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(array $themes, array $expected): void + { + $store = $this->createMock(StoreInterface::class); + foreach ($this->resolvers as $key => $resolver) { + $resolver->expects($this->once()) + ->method('getThemes') + ->willReturn($themes[$key]); + } + $this->assertEquals($expected, $this->model->getThemes($store)); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + [ + [], + [], + [] + ], + [] + ], + [ + [ + ['1'], + [], + ['1'] + ], + ['1'] + ], + [ + [ + ['1'], + ['2'], + ['1'] + ], + ['1', '2'] + ] + ]; + } +} diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php new file mode 100644 index 0000000000000..1ef4b17ca6562 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Theme; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Theme\Model\Theme\StoreUserAgentThemeResolver; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test store associated themes in user-agent rules resolver. + */ +class StoreUserAgentThemeResolverTest extends TestCase +{ + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfig; + /** + * @var Json + */ + private $serializer; + /** + * @var StoreUserAgentThemeResolver + */ + private $model; + + protected function setUp(): void + { + parent::setUp(); + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->serializer = new Json(); + $this->model = new StoreUserAgentThemeResolver( + $this->scopeConfig, + $this->serializer + ); + } + + /** + * Test that method returns user-agent rules associated themes. + * + * @param array|null $config + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(?array $config, array $expected): void + { + $store = $this->createMock(StoreInterface::class); + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->with('design/theme/ua_regexp', ScopeInterface::SCOPE_STORE, $store) + ->willReturn($config !== null ? $this->serializer->serialize($config) : $config); + $this->assertEquals($expected, $this->model->getThemes($store)); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + null, + [] + ], + [ + [], + [] + ], + [ + [ + [ + 'search' => '\/Chrome\/i', + 'regexp' => '\/Chrome\/i', + 'value' => '1', + ], + ], + ['1'] + ], + [ + [ + [ + 'search' => '\/Chrome\/i', + 'regexp' => '\/Chrome\/i', + 'value' => '1', + ], + [ + 'search' => '\/mozila\/i', + 'regexp' => '\/mozila\/i', + 'value' => '2', + ], + ], + ['1', '2'] + ] + ]; + } +} diff --git a/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php b/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php index a735ba1927477..8f421ac3121fb 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php @@ -23,6 +23,7 @@ use Magento\Theme\Helper\Storage; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Filesystem\DriverInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -74,6 +75,11 @@ class StorageTest extends TestCase */ protected $urlDecoder; + /** + * @var DriverInterface|MockObject + */ + private $filesystemDriver; + protected function setUp(): void { $this->_filesystem = $this->createMock(Filesystem::class); @@ -114,6 +120,7 @@ function ($path) { $this->directoryWrite = $this->createMock(Write::class); $this->urlEncoder = $this->createPartialMock(EncoderInterface::class, ['encode']); $this->urlDecoder = $this->createPartialMock(DecoderInterface::class, ['decode']); + $this->filesystemDriver = $this->createMock(DriverInterface::class); $this->_filesystem->expects( $this->once() @@ -129,7 +136,9 @@ function ($path) { $this->_objectManager, $this->_imageFactory, $this->urlEncoder, - $this->urlDecoder + $this->urlDecoder, + null, + $this->filesystemDriver ); $this->_storageRoot = '/root'; @@ -577,6 +586,33 @@ public function testDeleteRootDirectory() $this->_storageModel->deleteDirectory($directoryPath); } + /** + * cover \Magento\Theme\Model\Wysiwyg\Storage::deleteDirectory + */ + public function testDeleteRootDirectoryRelative() + { + $this->expectException( + \Magento\Framework\Exception\LocalizedException::class + ); + + $directoryPath = $this->_storageRoot; + $fakePath = 'fake/relative/path'; + + $this->directoryWrite->method('getAbsolutePath') + ->with($fakePath) + ->willReturn($directoryPath); + + $this->filesystemDriver->method('getRealPathSafety') + ->with($directoryPath) + ->willReturn($directoryPath); + + $this->_helperStorage + ->method('getStorageRoot') + ->willReturn($directoryPath); + + $this->_storageModel->deleteDirectory($fakePath); + } + /** * @return array */ diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php similarity index 80% rename from lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php rename to app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php index 549d45a986cf0..4efcc584986d1 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php +++ b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php @@ -3,15 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); - -namespace Magento\Framework\App\Test\Unit\Action\Plugin; +namespace Magento\Theme\Test\Unit\Plugin; use Magento\Framework\App\Action\Action; -use Magento\Framework\App\Action\Plugin\LoadDesignPlugin; use Magento\Framework\App\ActionInterface; use Magento\Framework\Message\ManagerInterface; use Magento\Framework\View\DesignLoader; +use Magento\Theme\Plugin\LoadDesignPlugin; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -26,7 +24,7 @@ public function testBeforeExecute() $designLoaderMock = $this->createMock(DesignLoader::class); /** @var MockObject|ManagerInterface $messageManagerMock */ - $messageManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); + $messageManagerMock = $this->createMock(ManagerInterface::class); $plugin = new LoadDesignPlugin($designLoaderMock, $messageManagerMock); diff --git a/app/code/Magento/Theme/Ui/Component/Design/Config/SearchRobots/ResetButton.php b/app/code/Magento/Theme/Ui/Component/Design/Config/SearchRobots/ResetButton.php index 4b71fc6faba15..f0e668d10c3a6 100644 --- a/app/code/Magento/Theme/Ui/Component/Design/Config/SearchRobots/ResetButton.php +++ b/app/code/Magento/Theme/Ui/Component/Design/Config/SearchRobots/ResetButton.php @@ -14,7 +14,7 @@ * ResetButton field instance * * @api - * @since 100.2.0 + * @since 100.1.9 */ class ResetButton extends Field { @@ -66,7 +66,7 @@ private function getRobotsDefaultCustomInstructions() * * @return void * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 100.1.9 */ public function prepare() { diff --git a/app/code/Magento/Theme/etc/adminhtml/system.xml b/app/code/Magento/Theme/etc/adminhtml/system.xml index af5e952584d1c..caafda9bdd827 100644 --- a/app/code/Magento/Theme/etc/adminhtml/system.xml +++ b/app/code/Magento/Theme/etc/adminhtml/system.xml @@ -19,7 +19,7 @@ <label>Use CSS critical path</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment> - <![CDATA[<strong style="color:red">Warning!</strong> Be sure that you have critical.css file for your theme. Other CSS files will be loaded asynchronously.]]> + <![CDATA[<strong class="colorRed">Warning!</strong> Be sure that you have critical.css file for your theme. Other CSS files will be loaded asynchronously.]]> </comment> </field> </group> diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml index 921e6bfc6ecf1..d6fe3f8fef355 100644 --- a/app/code/Magento/Theme/etc/di.xml +++ b/app/code/Magento/Theme/etc/di.xml @@ -18,6 +18,7 @@ <preference for="Magento\Theme\Api\DesignConfigRepositoryInterface" type="Magento\Theme\Model\DesignConfigRepository"/> <preference for="Magento\Framework\View\Model\PageLayout\Config\BuilderInterface" type="Magento\Theme\Model\PageLayout\Config\Builder"/> <preference for="Magento\Theme\Model\Design\Config\MetadataProviderInterface" type="Magento\Theme\Model\Design\Config\MetadataProvider"/> + <preference for="Magento\Theme\Model\Theme\StoreThemesResolverInterface" type="Magento\Theme\Model\Theme\StoreThemesResolver"/> <type name="Magento\Theme\Model\Config"> <arguments> <argument name="configCache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> @@ -104,6 +105,9 @@ <argument name="scope" xsi:type="const">Magento\Store\Model\ScopeInterface::SCOPE_STORE</argument> </arguments> </virtualType> + <type name="Magento\Framework\App\ActionInterface"> + <plugin name="designLoader" type="Magento\Theme\Plugin\LoadDesignPlugin"/> + </type> <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> <arguments> <argument name="collections" xsi:type="array"> @@ -309,4 +313,20 @@ <argument name="cache" xsi:type="object">configured_design_cache</argument> </arguments> </type> + <type name="Magento\Theme\Model\Theme\StoreThemesResolver"> + <arguments> + <argument name="resolvers" xsi:type="array"> + <item name="storeDefaultTheme" xsi:type="object">Magento\Theme\Model\Theme\StoreDefaultThemeResolver</item> + <item name="storeUserAgentTheme" xsi:type="object">Magento\Theme\Model\Theme\StoreUserAgentThemeResolver</item> + </argument> + </arguments> + </type> + <type name="Magento\Theme\Helper\Storage"> + <arguments> + <argument name="filesystemDriver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\Framework\Data\Collection"> + <plugin name="currentPageDetection" type="Magento\Theme\Plugin\Data\Collection" /> + </type> </config> diff --git a/app/code/Magento/Theme/etc/frontend/di.xml b/app/code/Magento/Theme/etc/frontend/di.xml index d3e5c07861c84..35eb9d4f8b53c 100644 --- a/app/code/Magento/Theme/etc/frontend/di.xml +++ b/app/code/Magento/Theme/etc/frontend/di.xml @@ -26,11 +26,9 @@ <type name="Magento\Framework\Controller\ResultInterface"> <plugin name="result-messages" type="Magento\Theme\Controller\Result\MessagePlugin"/> </type> - <type name="Magento\Framework\App\Response\Http"> - <plugin name="asyncCssLoad" type="Magento\Theme\Controller\Result\AsyncCssPlugin"/> - </type> <type name="Magento\Framework\View\Result\Layout"> - <plugin name="deferJsToFooter" type="Magento\Theme\Controller\Result\JsFooterPlugin" sortOrder="-10"/> + <plugin name="asyncCssLoad" type="Magento\Theme\Controller\Result\AsyncCssPlugin" /> + <plugin name="deferJsToFooter" type="Magento\Theme\Controller\Result\JsFooterPlugin" sortOrder="-10" /> </type> <type name="Magento\Theme\Block\Html\Header\CriticalCss"> <arguments> diff --git a/app/code/Magento/Theme/etc/webapi_rest/di.xml b/app/code/Magento/Theme/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..8abda8502238d --- /dev/null +++ b/app/code/Magento/Theme/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\Data\Collection"> + <plugin name="currentPageDetection" disabled="true"/> + </type> +</config> diff --git a/app/code/Magento/Theme/etc/webapi_soap/di.xml b/app/code/Magento/Theme/etc/webapi_soap/di.xml new file mode 100644 index 0000000000000..8abda8502238d --- /dev/null +++ b/app/code/Magento/Theme/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\Data\Collection"> + <plugin name="currentPageDetection" disabled="true"/> + </type> +</config> diff --git a/app/code/Magento/Theme/view/adminhtml/templates/browser/content/uploader.phtml b/app/code/Magento/Theme/view/adminhtml/templates/browser/content/uploader.phtml index 67c9084b2756e..66456ae403818 100644 --- a/app/code/Magento/Theme/view/adminhtml/templates/browser/content/uploader.phtml +++ b/app/code/Magento/Theme/view/adminhtml/templates/browser/content/uploader.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Theme\Block\Adminhtml\Wysiwyg\Files\Content\Uploader */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="<?= $block->getHtmlId() ?>" class="uploader"> @@ -18,14 +19,18 @@ <div id="<%- data.id %>" class="file-row"> <span class="file-info"><%- data.name %> (<%- data.size %>)</span> <div class="progressbar-container"> - <div class="progressbar upload-progress" style="width: 0%;"></div> + <div class="progressbar upload-progress"></div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width: 0%;", + "div.progressbar-container div.progressbar.upload-progress" + ) ?> <div class="clear"></div> </div> </script> </div> +<?php $scriptString= <<<script -<script> require([ 'jquery', 'mage/template', @@ -41,9 +46,9 @@ require([ form_key: FORM_KEY }, sequentialUploads: true, - maxFileSize: <?= $block->escapeJs($block->getFileSizeService()->getMaxFileSize()) ?> , + maxFileSize: {$block->escapeJs($block->getFileSizeService()->getMaxFileSize())} , add: function (e, data) { - var progressTmpl = mageTemplate('#<?= $block->getHtmlId() ?>-template'), + var progressTmpl = mageTemplate('#{$block->getHtmlId()}-template'), fileSize, tmpl; @@ -62,7 +67,7 @@ require([ } }); - $(tmpl).appendTo('#<?= $block->getHtmlId() ?>'); + $(tmpl).appendTo('#{$block->getHtmlId()}'); }); $(this).fileupload('process', data).done(function () { @@ -91,4 +96,7 @@ require([ }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/adminhtml/templates/tabs/css.phtml b/app/code/Magento/Theme/view/adminhtml/templates/tabs/css.phtml index 902daf98182f0..53228243ffd19 100644 --- a/app/code/Magento/Theme/view/adminhtml/templates/tabs/css.phtml +++ b/app/code/Magento/Theme/view/adminhtml/templates/tabs/css.phtml @@ -3,12 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -/** @var $block \Magento\Theme\Block\Adminhtml\System\Design\Theme\Edit\Tab\Css */ +/** + * @var $block \Magento\Theme\Block\Adminhtml\System\Design\Theme\Edit\Tab\Css + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?= $block->getFormHtml() ?> -<script> +<?php $scriptString = <<<script + require([ "jquery", "Magento_Ui/js/modal/alert", @@ -19,7 +23,7 @@ require([ $( '#css_file_uploader' ).fileupload({ dataType: 'json', replaceFileInput: false, - url : '<?= $block->escapeJs($block->escapeUrl($block->getUrl('*/system_design_theme/uploadcss'))) ?>', + url : '{$block->escapeJs($block->getUrl('*/system_design_theme/uploadcss'))}', acceptFileTypes: /(.|\/)(css)$/i, /** @@ -76,4 +80,7 @@ require([ }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/adminhtml/templates/tabs/fieldset/js.phtml b/app/code/Magento/Theme/view/adminhtml/templates/tabs/fieldset/js.phtml index b50f68cd9353b..e15ac4a088e03 100644 --- a/app/code/Magento/Theme/view/adminhtml/templates/tabs/fieldset/js.phtml +++ b/app/code/Magento/Theme/view/adminhtml/templates/tabs/fieldset/js.phtml @@ -4,18 +4,27 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -/** @var $block \Magento\Backend\Block\Widget\Form\Renderer\Fieldset */ +/** + * @var $block \Magento\Backend\Block\Widget\Form\Renderer\Fieldset + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <div id="js-file-uploader" class="uploader"> </div> -<script id="js-file-uploader-template" type="text/x-magento-template"> +<script id="js-file-uploader-template" type="text/x-magento-template"> <div id="<%- data.id %>" class="file-row"> <span class="file-info"><%- data.name %> (<%- data.size %>)</span> <div class="progressbar-container"> - <div class="progressbar upload-progress" style="width: 0%;"></div> + <div class="progressbar upload-progress""></div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width: 0%;", + "div.progressbar-container div.progressbar.upload-progress" + ) ?> <div class="clear"></div> </div> </script> @@ -40,8 +49,8 @@ </script> <ul id="js-files-container" class="js-files-container ui-sortable" ></ul> +<?php $scriptString = <<<script -<script> require([ "jquery", "jquery/ui", @@ -61,10 +70,13 @@ jQuery(function($) { $('body').trigger( 'refreshJsList', { - jsList: <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getJsFiles()) ?> + jsList: {$jsonHelper->jsonEncode($block->getJsFiles())} } ); }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/adminhtml/templates/tabs/js.phtml b/app/code/Magento/Theme/view/adminhtml/templates/tabs/js.phtml index 1b4633d0965f3..4edc895c559e2 100644 --- a/app/code/Magento/Theme/view/adminhtml/templates/tabs/js.phtml +++ b/app/code/Magento/Theme/view/adminhtml/templates/tabs/js.phtml @@ -4,11 +4,15 @@ * See COPYING.txt for license details. */ -/** @var $block \Magento\Theme\Block\Adminhtml\System\Design\Theme\Edit\Tab\Js */ +/** + * @var $block \Magento\Theme\Block\Adminhtml\System\Design\Theme\Edit\Tab\Js + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?= $block->getFormHtml() ?> -<script> +<?php $scriptString = <<<script + require([ "jquery", "mage/template", @@ -22,7 +26,7 @@ require([ dataType: 'json', replaceFileInput: false, sequentialUploads: true, - url: '<?= $block->escapeJs($block->escapeUrl($block->getJsUploadUrl())) ?>', + url: '{$block->escapeJs($block->getJsUploadUrl())}', /** * Add data @@ -125,4 +129,7 @@ require([ }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index f5580461f7d9e..4bd854f2e4670 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -4,8 +4,8 @@ */ var config = { - 'waitSeconds': 0, - 'map': { + waitSeconds: 0, + map: { '*': { 'ko': 'knockoutjs/knockout', 'knockout': 'knockoutjs/knockout', @@ -13,7 +13,7 @@ var config = { 'rjsResolver': 'mage/requirejs/resolver' } }, - 'shim': { + shim: { 'jquery/jquery-migrate': ['jquery'], 'jquery/jstree/jquery.hotkeys': ['jquery'], 'jquery/hover-intent': ['jquery'], @@ -28,7 +28,7 @@ var config = { }, 'magnifier/magnifier': ['jquery'] }, - 'paths': { + paths: { 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-fp', @@ -40,11 +40,11 @@ var config = { 'tinycolor': 'jquery/spectrum/tinycolor', 'jquery-ui-modules': 'jquery/ui-modules' }, - 'deps': [ + deps: [ 'jquery/jquery-migrate' ], - 'config': { - 'mixins': { + config: { + mixins: { 'jquery/jstree/jquery.jstree': { 'mage/backend/jstree-mixin': true }, @@ -52,7 +52,7 @@ var config = { 'jquery/patches/jquery': true } }, - 'text': { + text: { 'headers': { 'X-Requested-With': 'XMLHttpRequest' } diff --git a/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml b/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml index a4a10ef3f6ee9..1e5a4578602ca 100644 --- a/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml +++ b/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml @@ -10,7 +10,6 @@ <meta name="viewport" content="width=device-width, initial-scale=1"/> <css src="mage/calendar.css"/> <script src="requirejs/require.js"/> - <script src="mage/polyfill.js"/> </head> <body> <referenceBlock name="head.additional"> @@ -19,6 +18,7 @@ <argument name="criticalCssViewModel" xsi:type="object">Magento\Theme\Block\Html\Header\CriticalCss</argument> </arguments> </block> + <!-- Todo: Block css_rel_preload_script will be removed in next release as polyfill isn't used anymore --> <block name="css_rel_preload_script" ifconfig="dev/css/use_css_critical_path" template="Magento_Theme::js/css_rel_preload.phtml"/> </referenceBlock> <referenceContainer name="after.body.start"> diff --git a/app/code/Magento/Theme/view/frontend/requirejs-config.js b/app/code/Magento/Theme/view/frontend/requirejs-config.js index e14c93d329a07..79c8e69d94338 100644 --- a/app/code/Magento/Theme/view/frontend/requirejs-config.js +++ b/app/code/Magento/Theme/view/frontend/requirejs-config.js @@ -49,3 +49,24 @@ var config = { } } }; + +/* eslint-disable max-depth */ +/** + * Adds polyfills only for browser contexts which prevents bundlers from including them. + */ +if (typeof window !== 'undefined' && window.document) { + /** + * Polyfill localStorage and sessionStorage for browsers that do not support them. + */ + try { + if (!window.localStorage || !window.sessionStorage) { + throw new Error(); + } + + localStorage.setItem('storage_test', 1); + localStorage.removeItem('storage_test'); + } catch (e) { + config.deps.push('mage/polyfill'); + } +} +/* eslint-enable max-depth */ diff --git a/app/code/Magento/Theme/view/frontend/templates/html/main_css_preloader.phtml b/app/code/Magento/Theme/view/frontend/templates/html/main_css_preloader.phtml index 2c1c7db75b111..70c2a5f4538fa 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/main_css_preloader.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/main_css_preloader.phtml @@ -3,11 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Element\Template $block + * @var \Magento\Framework\Escaper $escaper + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + ?> <div data-role="main-css-loader" class="loading-mask"> <div class="loader"> - <img src="<?= $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')); ?>" - alt="<?= $block->escapeHtml(__('Loading...')); ?>" - style="position: absolute;"> + <img src="<?= $escaper->escapeUrl($block->getViewFileUrl('images/loader-1.gif')); ?>" + alt="<?= $escaper->escapeHtml(__('Loading...')); ?>"> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "position: absolute;", + "div.loader img" + ) ?> </div> diff --git a/app/code/Magento/Theme/view/frontend/templates/html/notices.phtml b/app/code/Magento/Theme/view/frontend/templates/html/notices.phtml index 1414c21c6e9bc..bb9d5cb2fd2e0 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/notices.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/notices.phtml @@ -6,30 +6,41 @@ /** * @var $block \Magento\Theme\Block\Html\Notices + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->displayNoscriptNotice()) : ?> +<?php if ($block->displayNoscriptNotice()): ?> <noscript> <div class="message global noscript"> <div class="content"> <p> <strong><?= $block->escapeHtml(__('JavaScript seems to be disabled in your browser.')) ?></strong> - <span><?= $block->escapeHtml(__('For the best experience on our site, be sure to turn on Javascript in your browser.')) ?></span> + <span> + <?= $block->escapeHtml( + __('For the best experience on our site, be sure to turn on Javascript in your browser.') + ) ?> + </span> </p> </div> </div> </noscript> <?php endif; ?> -<?php if ($block->displayNoLocalStorageNotice()) : ?> - <div class="notice global site local_storage" style="display: none;"> +<?php if ($block->displayNoLocalStorageNotice()): ?> + <div class="notice global site local_storage"> <div class="content"> <p> - <strong><?= $block->escapeHtml(__('Local Storage seems to be disabled in your browser.')) ?></strong><br /> - <?= $block->escapeHtml(__('For the best experience on our site, be sure to turn on Local Storage in your browser.')) ?> + <strong><?= $block->escapeHtml(__('Local Storage seems to be disabled in your browser.')) ?></strong> + <br /> + <?= $block->escapeHtml( + __('For the best experience on our site, be sure to turn on Local Storage in your browser.') + ) ?> </p> </div> </div> - <script> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'div.notice.global.site.local_storage') ?> + + <?php $scriptString = <<<script + require(['jquery'], function(jQuery){ // <![CDATA[ @@ -45,9 +56,12 @@ require(['jquery'], function(jQuery){ // ]]> }); -</script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> -<?php if ($block->displayDemoNotice()) : ?> +<?php if ($block->displayDemoNotice()): ?> <div class="message global demo"> <div class="content"> <p><?= $block->escapeHtml(__('This is a demo store. No orders will be fulfilled.')) ?></p> diff --git a/app/code/Magento/Theme/view/frontend/templates/html/print.phtml b/app/code/Magento/Theme/view/frontend/templates/html/print.phtml index d05faac66ffd1..e939ad40aafb6 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/print.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/print.phtml @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script + require( [ 'jquery' @@ -15,4 +19,7 @@ }); } ); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml index 55798169cdf75..d2803a741d9a2 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml @@ -4,6 +4,9 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +/** @var \Magento\Framework\View\Element\Html\Calendar $block */ + /** * Calendar localization script. Should be put into page header. * @@ -11,32 +14,33 @@ */ ?> -<script> +<?php $intFirstDay = (int)$firstDay; +$scriptString = <<<script + require([ 'jquery', - 'jquery-ui-modules/datepicker' ], function($){ //<![CDATA[ $.extend(true, $, { calendarConfig: { - dayNames: <?= /* @noEscape */ $days['wide'] ?>, - dayNamesMin: <?= /* @noEscape */ $days['abbreviated'] ?>, - monthNames: <?= /* @noEscape */ $months['wide'] ?>, - monthNamesShort: <?= /* @noEscape */ $months['abbreviated'] ?>, - infoTitle: "<?= $block->escapeJs(__('About the calendar')) ?>", - firstDay: <?= (int)$firstDay ?>, - closeText: "<?= $block->escapeJs(__('Close')) ?>", - currentText: "<?= $block->escapeJs(__('Go Today')) ?>", - prevText: "<?= $block->escapeJs(__('Previous')) ?>", - nextText: "<?= $block->escapeJs(__('Next')) ?>", - weekHeader: "<?= $block->escapeJs(__('WK')) ?>", - timeText: "<?= $block->escapeJs(__('Time')) ?>", - hourText: "<?= $block->escapeJs(__('Hour')) ?>", - minuteText: "<?= $block->escapeJs(__('Minute')) ?>", - dateFormat: $.datepicker.RFC_2822, - showOn: "button", - showAnim: "", + dayNames: {$days['wide']}, + dayNamesMin: {$days['abbreviated']}, + monthNames: {$months['wide']}, + monthNamesShort: {$months['abbreviated']}, + infoTitle: '{$block->escapeJs(__('About the calendar'))}', + firstDay: {$intFirstDay}, + closeText: '{$block->escapeJs(__('Close'))}', + currentText: '{$block->escapeJs(__('Go Today'))}', + prevText: '{$block->escapeJs(__('Previous'))}', + nextText: '{$block->escapeJs(__('Next'))}', + weekHeader: '{$block->escapeJs(__('WK'))}', + timeText: '{$block->escapeJs(__('Time'))}', + hourText: '{$block->escapeJs(__('Hour'))}', + minuteText: '{$block->escapeJs(__('Minute'))}', + dateFormat: "D, d M yy", // $.datepicker.RFC_2822 + showOn: 'button', + showAnim: '', changeMonth: true, changeYear: true, buttonImageOnly: null, @@ -50,8 +54,10 @@ require([ } }); - enUS = <?= /* @noEscape */ $enUS ?>; // en_US locale reference + enUS = {$enUS}; // en_US locale reference //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/frontend/templates/js/cookie_status.phtml b/app/code/Magento/Theme/view/frontend/templates/js/cookie_status.phtml index 2da71c90b5657..7d43ffcbb8063 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/cookie_status.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/cookie_status.phtml @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div id="cookie-status" style="display: none"> +<div id="cookie-status"> <?= $block->escapeHtml(__('The store will not work correctly in the case when cookies are disabled.')); ?> </div> +<?php +$script = 'document.querySelector("#cookie-status").style.display = "none";'; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', ['type' => 'text/javascript'], $script, false); ?> <script type="text/x-magento-init"> { @@ -16,5 +22,3 @@ } } </script> - - diff --git a/app/code/Magento/Theme/view/frontend/templates/js/css_rel_preload.phtml b/app/code/Magento/Theme/view/frontend/templates/js/css_rel_preload.phtml index d90d528ffc6f8..2df684ccffee1 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/css_rel_preload.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/css_rel_preload.phtml @@ -2,9 +2,25 @@ /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. + * @deprecated as polyfill isn't used anymore */ -?> -<script> + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$scriptString = <<<script + /*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */ - !function(t){"use strict";t.loadCSS||(t.loadCSS=function(){});var e=loadCSS.relpreload={};if(e.support=function(){var e;try{e=t.document.createElement("link").relList.supports("preload")}catch(t){e=!1}return function(){return e}}(),e.bindMediaToggle=function(t){var e=t.media||"all";function a(){t.media=e}t.addEventListener?t.addEventListener("load",a):t.attachEvent&&t.attachEvent("onload",a),setTimeout(function(){t.rel="stylesheet",t.media="only x"}),setTimeout(a,3e3)},e.poly=function(){if(!e.support())for(var a=t.document.getElementsByTagName("link"),n=0;n<a.length;n++){var o=a[n];"preload"!==o.rel||"style"!==o.getAttribute("as")||o.getAttribute("data-loadcss")||(o.setAttribute("data-loadcss",!0),e.bindMediaToggle(o))}},!e.support()){e.poly();var a=t.setInterval(e.poly,500);t.addEventListener?t.addEventListener("load",function(){e.poly(),t.clearInterval(a)}):t.attachEvent&&t.attachEvent("onload",function(){e.poly(),t.clearInterval(a)})}"undefined"!=typeof exports?exports.loadCSS=loadCSS:t.loadCSS=loadCSS}("undefined"!=typeof global?global:this); -</script> + !function(t){"use strict";t.loadCSS||(t.loadCSS=function(){});var e=loadCSS.relpreload={}; + if(e.support=function(){var e;try{e=t.document.createElement("link").relList.supports("preload")} + catch(t){e=!1}return function(){return e}}(),e.bindMediaToggle=function(t){var e=t.media||"all"; + function a(){t.media=e}t.addEventListener?t.addEventListener("load",a):t.attachEvent&&t.attachEvent("onload",a), + setTimeout(function(){t.rel="stylesheet",t.media="only x"}),setTimeout(a,3e3)},e.poly=function(){if(!e.support()) + for(var a=t.document.getElementsByTagName("link"),n=0;n<a.length;n++){var o=a[n];"preload"!==o.rel|| + "style"!==o.getAttribute("as")||o.getAttribute("data-loadcss")|| + (o.setAttribute("data-loadcss",!0),e.bindMediaToggle(o))}},!e.support()){e.poly();var a=t.setInterval(e.poly,500); + t.addEventListener?t.addEventListener("load", + function(){e.poly(),t.clearInterval(a)}):t.attachEvent&&t.attachEvent("onload", + function(){e.poly(),t.clearInterval(a)})}"undefined"!=typeof exports?exports.loadCSS=loadCSS:t.loadCSS=loadCSS} + ("undefined"!=typeof global?global:this); + +script; diff --git a/app/code/Magento/Theme/view/frontend/templates/messages.phtml b/app/code/Magento/Theme/view/frontend/templates/messages.phtml index dd9b81ecb38b9..f863da70e8987 100644 --- a/app/code/Magento/Theme/view/frontend/templates/messages.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/messages.phtml @@ -6,22 +6,25 @@ ?> <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 }"> - <div data-bind="html: message.text"></div> + <div data-bind="html: $parent.prepareMessageForHtml(message.text)"></div> </div> </div> <!-- /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 }"> - <div data-bind="html: message.text"></div> + <div data-bind="html: $parent.prepareMessageForHtml(message.text)"></div> </div> </div> <!-- /ko --> diff --git a/app/code/Magento/Theme/view/frontend/templates/page/js/require_js.phtml b/app/code/Magento/Theme/view/frontend/templates/page/js/require_js.phtml index ad998c56b963f..a83d510ee0926 100644 --- a/app/code/Magento/Theme/view/frontend/templates/page/js/require_js.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/page/js/require_js.phtml @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<script> - var BASE_URL = '<?= $block->escapeUrl($block->getBaseUrl()) ?>'; + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$scriptString = ' + var BASE_URL = \'' . /* @noEscape */ $block->escapeJs($block->getBaseUrl()) .'\'; var require = { - "baseUrl": "<?= $block->escapeUrl($block->getViewFileUrl('/')) ?>" - }; -</script> + \'baseUrl\': \'' . /* @noEscape */ $block->escapeJs($block->getViewFileUrl('/')) . '\' + };'; + +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Theme/view/frontend/web/js/view/messages.js b/app/code/Magento/Theme/view/frontend/web/js/view/messages.js index cefbf11d73933..8d2ffed2d3bac 100644 --- a/app/code/Magento/Theme/view/frontend/web/js/view/messages.js +++ b/app/code/Magento/Theme/view/frontend/web/js/view/messages.js @@ -11,14 +11,16 @@ define([ 'uiComponent', 'Magento_Customer/js/customer-data', 'underscore', + 'escaper', 'jquery/jquery-storageapi' -], function ($, Component, customerData, _) { +], function ($, Component, customerData, _, escaper) { 'use strict'; return Component.extend({ defaults: { cookieMessages: [], - messages: [] + messages: [], + allowedTags: ['div', 'span', 'b', 'strong', 'i', 'em', 'u', 'a'] }, /** @@ -38,6 +40,16 @@ define([ } $.cookieStorage.set('mage-messages', ''); + }, + + /** + * Prepare the given message to be rendered as HTML + * + * @param {String} message + * @return {String} + */ + prepareMessageForHtml: function (message) { + return escaper.escapeHtml(message, this.allowedTags); } }); }); diff --git a/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php b/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php index e59aa2934e4ed..e525bf62cc3a3 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php +++ b/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php @@ -12,7 +12,7 @@ /** * Class Config adds information about required configurations to display media gallery of tinymce3 editor * - * @deprecated use \Magento\Cms\Model\Wysiwyg\DefaultConfigProvider instead + * @deprecated 100.3.0 use \Magento\Cms\Model\Wysiwyg\DefaultConfigProvider instead */ class Config implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface { diff --git a/app/code/Magento/Tinymce3/Model/Config/Source/Wysiwyg/Editor.php b/app/code/Magento/Tinymce3/Model/Config/Source/Wysiwyg/Editor.php index 00f1a82698381..96a3d42d15f36 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Source/Wysiwyg/Editor.php +++ b/app/code/Magento/Tinymce3/Model/Config/Source/Wysiwyg/Editor.php @@ -9,7 +9,7 @@ /** * Class Editor provides configuration value for TinyMCE3 editor - * @deprecated use as configuration value tinymce4 path: mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter + * @deprecated 100.3.0 use as configuration value tinymce4 path: mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter */ class Editor { diff --git a/app/code/Magento/Tinymce3/Model/Config/Variable/Config.php b/app/code/Magento/Tinymce3/Model/Config/Variable/Config.php index 2d016a5101abe..97aab0f38c4ee 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Variable/Config.php +++ b/app/code/Magento/Tinymce3/Model/Config/Variable/Config.php @@ -9,7 +9,7 @@ /** * Class Config adds variable plugin information required for tinymce3 editor - * @deprecated use \Magento\Variable\Model\Variable\ConfigProvider instead + * @deprecated 100.3.0 use \Magento\Variable\Model\Variable\ConfigProvider instead */ class Config implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface { diff --git a/app/code/Magento/Tinymce3/Model/Config/Widget/Config.php b/app/code/Magento/Tinymce3/Model/Config/Widget/Config.php index de548df4bc9f3..fcb8235495d47 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Widget/Config.php +++ b/app/code/Magento/Tinymce3/Model/Config/Widget/Config.php @@ -9,7 +9,7 @@ /** * Class Config adds widget plugin information required for tinymce3 editor - * @deprecated use \Magento\Widget\Model\Widget\Config instead + * @deprecated 100.3.0 use \Magento\Widget\Model\Widget\Config instead */ class Config implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface { diff --git a/app/code/Magento/Tinymce3/Model/Config/Widget/PlaceholderImagesPool.php b/app/code/Magento/Tinymce3/Model/Config/Widget/PlaceholderImagesPool.php index 1ab3de708dd26..7004beea70f1c 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Widget/PlaceholderImagesPool.php +++ b/app/code/Magento/Tinymce3/Model/Config/Widget/PlaceholderImagesPool.php @@ -8,7 +8,7 @@ /** * Class PlaceholderImages provide ability to override placeholder images for Widgets - * @deprecated + * @deprecated 100.3.0 */ class PlaceholderImagesPool { diff --git a/app/code/Magento/Tinymce3/Model/Config/Wysiwyg/Config.php b/app/code/Magento/Tinymce3/Model/Config/Wysiwyg/Config.php index f3dc4c8591cbd..c70ad28a114df 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Wysiwyg/Config.php +++ b/app/code/Magento/Tinymce3/Model/Config/Wysiwyg/Config.php @@ -9,7 +9,7 @@ /** * Class Config adds information about required css files for tinymce3 editor - * @deprecated use \Magento\Cms\Model\Wysiwyg\DefaultConfigProvider instead + * @deprecated 100.3.0 use \Magento\Cms\Model\Wysiwyg\DefaultConfigProvider instead */ class Config implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface { diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/NewsletterWYSIWYGSection.xml b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/NewsletterWYSIWYGSection.xml index 14002028d9da4..13917561629bc 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/NewsletterWYSIWYGSection.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/NewsletterWYSIWYGSection.xml @@ -8,6 +8,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="NewsletterWYSIWYGSection"> - <element name="TinyMCE3" type="text" selector="#cms_page_form_content_tbl"/> + <element name="TinyMCE3" type="text" selector="#cms_page_form_content_tbl" deprecated="This version of TinyMCE is no longer supported"/> </section> </sections> diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/ProductWYSIWYGSection.xml b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/ProductWYSIWYGSection.xml index 9ce4e067169ec..bc666d3590a8f 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/ProductWYSIWYGSection.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/ProductWYSIWYGSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="ProductWYSIWYGSection"> + <section name="ProductWYSIWYGSection" deprecated="This version of TinyMCE is no longer supported"> <element name="Tinymce3MSG" type="button" selector=".admin__field-error"/> </section> </sections> diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/TinyMCESection.xml b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/TinyMCESection.xml index cb46bed781e5a..38b2d907ecf44 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/TinyMCESection.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/TinyMCESection.xml @@ -8,7 +8,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="TinyMCESection"> - <element name="TinyMCE3" type="text" selector="#cms_page_form_content_tbl"/> - <element name="InsertImageBtnTinyMCE3" type="button" selector="#cms_page_form_content_image"/> + <element name="TinyMCE3" type="text" selector="#cms_page_form_content_tbl" deprecated="Deprecated this version of TinyMCE is no longer supported"/> + <element name="InsertImageBtnTinyMCE3" type="button" selector="#cms_page_form_content_image" deprecated="Deprecated this version of TinyMCE is no longer supported"/> </section> </sections> diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml b/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml index 9c4e7bf3a646a..ddb6c4071a0e7 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml @@ -8,7 +8,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminSwitchWYSIWYGOptionsTest"> + <test name="AdminSwitchWYSIWYGOptionsTest" deprecated="TinyMCE3 is no longer supported"> <annotations> <features value="Cms"/> <stories value="MAGETWO-51829-Extensible list of WYSIWYG editors available in Magento"/> @@ -17,6 +17,9 @@ <description value="Admin should able to switch between versions of TinyMCE"/> <severity value="CRITICAL"/> <testCaseId value="MC-6114"/> + <skip> + <issueId value="DEPRECATED">TinyMCE3 is no longer supported</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> @@ -31,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"/> @@ -59,12 +61,11 @@ <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" /> - <waitForElementVisible selector="{{TinyMCESection.TinyMCE3}}" stepKey="waitForTinyMCE3"/> - <seeElement selector="{{TinyMCESection.TinyMCE3}}" stepKey="seeTinyMCE3" /> + <comment userInput="removing deprecated element" stepKey="waitForTinyMCE3"/> + <comment userInput="removing deprecated element" stepKey="seeTinyMCE3" /> <executeJS function="tinyMCE.activeEditor.setContent('Hello TinyMCE3!');" stepKey="executeJSFillContent2"/> <click selector="{{CmsWYSIWYGSection.ShowHideBtn}}" stepKey="clickShowHideBtn2" /> <scrollTo selector="{{CmsNewPagePageSeoSection.header}}" stepKey="scrollToSearchEngineTab2" /> diff --git a/app/code/Magento/Tinymce3/etc/adminhtml/di.xml b/app/code/Magento/Tinymce3/etc/adminhtml/di.xml index 53ab66c7ef21f..bcccf44594103 100644 --- a/app/code/Magento/Tinymce3/etc/adminhtml/di.xml +++ b/app/code/Magento/Tinymce3/etc/adminhtml/di.xml @@ -39,14 +39,4 @@ </argument> </arguments> </type> - <type name="Magento\Cms\Model\Config\Source\Wysiwyg\Editor"> - <arguments> - <argument name="adapterOptions" xsi:type="array"> - <item name="tinymce3" xsi:type="array"> - <item name="value" xsi:type="const">Magento\Tinymce3\Model\Config\Source\Wysiwyg\Editor::WYSIWYG_EDITOR_CONFIG_VALUE</item> - <item name="label" xsi:type="string" translatable="true">TinyMCE 3 (deprecated)</item> - </item> - </argument> - </arguments> - </type> </config> diff --git a/app/code/Magento/Tinymce3/etc/csp_whitelist.xml b/app/code/Magento/Tinymce3/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..e2b2364dc80e8 --- /dev/null +++ b/app/code/Magento/Tinymce3/etc/csp_whitelist.xml @@ -0,0 +1,28 @@ +<?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="style-src"> + <values> + <value id="firebug" type="host">getfirebug.com</value> + </values> + </policy> + <policy id="script-src"> + <values> + <value id="www_youtube" type="host">www.youtube.com</value> + <value id="google_video" type="host">video.google.com</value> + </values> + </policy> + <policy id="img-src"> + <values> + <value id="youtube_cdn" type="host">s.ytimg.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/Tinymce3/etc/di.xml b/app/code/Magento/Tinymce3/etc/di.xml index e03d865ce4e01..0b1175b0cd94c 100644 --- a/app/code/Magento/Tinymce3/etc/di.xml +++ b/app/code/Magento/Tinymce3/etc/di.xml @@ -6,11 +6,4 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Ui\Block\Wysiwyg\ActiveEditor"> - <arguments> - <argument name="availableAdapterPaths" xsi:type="array"> - <item name="Magento_Tinymce3/tinymce3Adapter" xsi:type="string"/> - </argument> - </arguments> - </type> </config> diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js index a3bd16cab718e..2119426a5c157 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js @@ -82,9 +82,9 @@ } } - function attemptMerge(e1, e2, differentStylesMasterElement, mergeParagraphs) { - if (canMerge(e1, e2, !!differentStylesMasterElement, mergeParagraphs)) { - return merge(e1, e2, differentStylesMasterElement); + function attemptMerge(e1, e2, differentStylesMainElement, mergeParagraphs) { + if (canMerge(e1, e2, !!differentStylesMainElement, mergeParagraphs)) { + return merge(e1, e2, differentStylesMainElement); } else if (e1 && e1.tagName === 'LI' && isList(e2)) { // Fix invalidly nested lists. e1.appendChild(e2); @@ -112,7 +112,7 @@ return firstChild && lastChild && firstChild === lastChild && isList(firstChild); } - function merge(e1, e2, masterElement) { + function merge(e1, e2, mainElement) { var lastOriginal = skipWhitespaceNodesBackwards(e1.lastChild), firstNew = skipWhitespaceNodesForwards(e2.firstChild); if (e1.tagName === 'P') { e1.appendChild(e1.ownerDocument.createElement('br')); @@ -120,8 +120,8 @@ while (e2.firstChild) { e1.appendChild(e2.firstChild); } - if (masterElement) { - e1.style.listStyleType = masterElement.style.listStyleType; + if (mainElement) { + e1.style.listStyleType = mainElement.style.listStyleType; } e2.parentNode.removeChild(e2); attemptMerge(lastOriginal, firstNew, false); @@ -164,7 +164,7 @@ } return false; } - + // If we are at the end of a paragraph in a list item, pressing enter should create a new list item instead of a new paragraph. function isEndOfParagraph() { var node = ed.selection.getNode(); @@ -241,7 +241,7 @@ Event.cancel(e); } } - + // Creates a new list item after the current selection's list item parent function createNewLi(ed, e) { if (state == LIST_PARAGRAPH) { diff --git a/app/code/Magento/Translation/Block/Js.php b/app/code/Magento/Translation/Block/Js.php index db26feb8067ff..fa3d6905f5868 100644 --- a/app/code/Magento/Translation/Block/Js.php +++ b/app/code/Magento/Translation/Block/Js.php @@ -14,6 +14,10 @@ * * @api * @since 100.0.2 + * @deprecated logic was refactored in order to not use localstorage at all. + * + * You can see details in app/code/Magento/Translation/view/base/web/js/mage-translation-dictionary.js + * These block and view file were left in order to keep backward compatibility */ class Js extends Template { @@ -78,6 +82,7 @@ public function getTranslationFilePath() * Gets current version of the translation file. * * @return string + * @since 100.3.0 */ public function getTranslationFileVersion() { diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationOnProductPageTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationOnProductPageTest.xml new file mode 100644 index 0000000000000..f3103c4bea51c --- /dev/null +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationOnProductPageTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontButtonsInlineTranslationOnProductPageTest"> + <annotations> + <features value="Translation"/> + <stories value="Inline Translation"/> + <title value="Buttons inline translation on product page"/> + <description value="A merchant should be able to translate buttons by an inline translation tool"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-27118"/> + <useCaseId value="MC-24186"/> + <group value="translation"/> + <group value="catalog"/> + <group value="developer_mode_only"/> + </annotations> + <before> + <!-- Enable Translate Inline For Storefront --> + <magentoCLI command="config:set {{EnableTranslateInlineForStorefront.path}} {{EnableTranslateInlineForStorefront.value}}" stepKey="enableTranslateInlineForStorefront"/> + <!-- Create Simple Product --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!-- Disable Translate Inline For Storefront --> + <magentoCLI command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" stepKey="disableTranslateInlineForStorefront"/> + <!-- Delete Simple Product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Add product to cart on storefront --> + <amOnPage url="{{StorefrontProductPage.url($createProduct.custom_attributes[url_key]$)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <waitForElementVisible selector="{{StorefrontProductActionSection.addToCartEnabledWithTranslation}}" stepKey="waitForAddToCartButtonEnabled"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + + <!-- Open Mini Cart --> + <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> + + <!-- Check button "Proceed to Checkout". There must be red borders and "book" icons on labels that can be translated. --> + <actionGroup ref="AssertElementInTranslateInlineModeActionGroup" stepKey="assertRedBordersAndBookIcon"> + <argument name="elementSelector" value="{{StorefrontMinicartSection.goToCheckout}}"/> + </actionGroup> + + <!-- Open Inline Translation popup --> + <actionGroup ref="StorefrontOpenInlineTranslationPopupActionGroup" stepKey="openInlineTranslationPopup"> + <argument name="elementSelector" value="{{StorefrontMinicartSection.goToCheckout}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml index 1ba3236185148..3d617360a9d28 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml @@ -20,6 +20,9 @@ <group value="translation"/> <group value="catalog"/> <group value="developer_mode_only"/> + <skip> + <issueId value="DEPRECATED">Use StorefrontButtonsInlineTranslationOnProductPageTest instead</issueId> + </skip> </annotations> <before> <!-- Enable Translate Inline For Storefront --> diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index 9255923213839..e30ab98982b78 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -115,7 +115,9 @@ <magentoCLI command="config:set {{EnableTranslateInlineForStorefront.path}} {{EnableTranslateInlineForStorefront.value}}" stepKey="enableTranslateInlineForStorefront"/> <!-- 2. Refresh magento cache --> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterTranslateEnabled"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTranslateEnabled"> + <argument name="tags" value=""/> + </actionGroup> <!-- 3. Go to storefront and click on cart button on the top --> <reloadPage stepKey="reloadPage"/> @@ -476,7 +478,9 @@ <!-- 7. Set *Enabled for Storefront* option to *No* and save configuration --> <magentoCLI command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" stepKey="disableTranslateInlineForStorefront"/> <!-- 8. Clear magento cache --> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterTranslateDisabled"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTranslateDisabled"> + <argument name="tags" value=""/> + </actionGroup> <magentoCLI command="setup:static-content:deploy -f" stepKey="deployStaticContent"/> diff --git a/app/code/Magento/Translation/view/adminhtml/templates/translate_inline.phtml b/app/code/Magento/Translation/view/adminhtml/templates/translate_inline.phtml index 6b6327a5679ea..67dd55d3d6372 100644 --- a/app/code/Magento/Translation/view/adminhtml/templates/translate_inline.phtml +++ b/app/code/Magento/Translation/view/adminhtml/templates/translate_inline.phtml @@ -4,10 +4,15 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Framework\View\Element\Template $block */ +/** + * @var \Magento\Framework\View\Element\Template $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<link rel="stylesheet" type="text/css" href="<?= $block->escapeUrl($block->getViewFileUrl('prototype/windows/themes/default.css')) ?>"/> -<link rel="stylesheet" type="text/css" href="<?= $block->escapeUrl($block->getViewFileUrl('mage/translate-inline.css')) ?>"/> +<link rel="stylesheet" type="text/css" + href="<?= $block->escapeUrl($block->getViewFileUrl('prototype/windows/themes/default.css')) ?>"/> +<link rel="stylesheet" type="text/css" + href="<?= $block->escapeUrl($block->getViewFileUrl('mage/translate-inline.css')) ?>"/> <script id="translate-inline-icon" type="text/x-magento-template"> <img src="<%- data.img %>" height="16" width="16" class="translate-edit-icon"> @@ -51,8 +56,12 @@ <% } %> </script> -<div data-role="translate-dialog" data-mage-init='{"translateInline":{"ajaxUrl":"<?= $block->escapeJs($block->escapeUrl($block->getAjaxUrl())) ?>"},"loader":{}}'></div> -<script> +<div data-role="translate-dialog" + data-mage-init='{"translateInline":{"ajaxUrl":"<?= $block->escapeJs($block->escapeUrl($block->getAjaxUrl())) ?>"}, + "loader":{}}'> +</div> +<?php $scriptString = <<<script + require([ "jquery", "mage/edit-trigger", @@ -60,12 +69,15 @@ require([ ], function($){ $('body').editTrigger( { - img: '<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('Magento_Theme::fam_book_open.png'))) ?>', + img: '{$block->escapeJs($block->getViewFileUrl('Magento_Theme::fam_book_open.png'))}', alwaysShown: true, singleElement: false } ); - + $('body').addClass('trnslate-inline-area'); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Translation/view/base/requirejs-config.js b/app/code/Magento/Translation/view/base/requirejs-config.js new file mode 100644 index 0000000000000..682c3fca81117 --- /dev/null +++ b/app/code/Magento/Translation/view/base/requirejs-config.js @@ -0,0 +1,15 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + map: { + '*': { + mageTranslationDictionary: 'Magento_Translation/js/mage-translation-dictionary' + } + }, + deps: [ + 'mageTranslationDictionary' + ] +}; diff --git a/app/code/Magento/Translation/view/base/templates/translate.phtml b/app/code/Magento/Translation/view/base/templates/translate.phtml index 4c257eb76843f..98997398c0938 100644 --- a/app/code/Magento/Translation/view/base/templates/translate.phtml +++ b/app/code/Magento/Translation/view/base/templates/translate.phtml @@ -4,57 +4,11 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Translation\Block\Js $block */ -?> -<!-- - For frontend area dictionary file is inserted into html head in Magento/Translation/view/base/templates/dictionary.phtml - Same translation mechanism should be introduced for admin area in 2.4 version. ---> -<?php if ($block->dictionaryEnabled()) : ?> - <script> - require.config({ - deps: [ - 'jquery', - 'mage/translate', - 'jquery/jquery-storageapi' - ], - callback: function ($) { - 'use strict'; - - var dependencies = [], - versionObj; - - $.initNamespaceStorage('mage-translation-storage'); - $.initNamespaceStorage('mage-translation-file-version'); - versionObj = $.localStorage.get('mage-translation-file-version'); - - <?php $version = $block->getTranslationFileVersion(); ?> - - if (versionObj.version !== '<?= $block->escapeJs($version) ?>') { - dependencies.push( - 'text!<?= /* @noEscape */ Magento\Translation\Model\Js\Config::DICTIONARY_FILE_NAME ?>' - ); - - } - - require.config({ - deps: dependencies, - callback: function (string) { - if (typeof string === 'string') { - $.mage.translate.add(JSON.parse(string)); - $.localStorage.set('mage-translation-storage', string); - $.localStorage.set( - 'mage-translation-file-version', - { - version: '<?= $block->escapeJs($version) ?>' - } - ); - } else { - $.mage.translate.add($.localStorage.get('mage-translation-storage')); - } - } - }); - } - }); - </script> -<?php endif; ?> +/** + * @var \Magento\Translation\Block\Js $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + * @deprecated logic was refactored in order to not use localstorage at all. + * + * You can see details in app/code/Magento/Translation/view/base/web/js/mage-translation-dictionary.js + * These block and view file were left in order to keep backward compatibility + */ diff --git a/app/code/Magento/Translation/view/frontend/requirejs-config.js b/app/code/Magento/Translation/view/frontend/requirejs-config.js index b5351b9d471cf..9a99d49eddbcf 100644 --- a/app/code/Magento/Translation/view/frontend/requirejs-config.js +++ b/app/code/Magento/Translation/view/frontend/requirejs-config.js @@ -8,12 +8,7 @@ var config = { '*': { editTrigger: 'mage/edit-trigger', addClass: 'Magento_Translation/js/add-class', - 'Magento_Translation/add-class': 'Magento_Translation/js/add-class', - mageTranslationDictionary: 'Magento_Translation/js/mage-translation-dictionary' + 'Magento_Translation/add-class': 'Magento_Translation/js/add-class' } - }, - deps: [ - 'mage/translate-inline', - 'mageTranslationDictionary' - ] + } }; diff --git a/app/code/Magento/Ui/Block/Wysiwyg/ActiveEditor.php b/app/code/Magento/Ui/Block/Wysiwyg/ActiveEditor.php index b6077b7b1625d..462e4a4695ef0 100644 --- a/app/code/Magento/Ui/Block/Wysiwyg/ActiveEditor.php +++ b/app/code/Magento/Ui/Block/Wysiwyg/ActiveEditor.php @@ -13,6 +13,7 @@ * ActiveEditor block * * @api + * @since 101.1.0 */ class ActiveEditor extends \Magento\Framework\View\Element\Template { @@ -50,6 +51,7 @@ public function __construct( * Get active wysiwyg adapter path * * @return string + * @since 101.1.0 */ public function getWysiwygAdapterPath() { diff --git a/app/code/Magento/Ui/Component/Control/Button.php b/app/code/Magento/Ui/Component/Control/Button.php index 952f1f62fa2d7..fbbf0e1f0fa61 100644 --- a/app/code/Magento/Ui/Component/Control/Button.php +++ b/app/code/Magento/Ui/Component/Control/Button.php @@ -5,14 +5,45 @@ */ namespace Magento\Ui\Component\Control; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Element\UiComponent\Control\ControlInterface; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +use Magento\Framework\View\Element\Template\Context; /** - * Class Button + * Widget for standard button. */ class Button extends Template implements ControlInterface { + /** + * @var Random + */ + private $random; + + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param Random|null $random + * @param SecureHtmlRenderer|null $htmlRenderer + */ + public function __construct( + Context $context, + array $data = [], + ?Random $random = null, + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct($context, $data); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Define block template * @@ -91,6 +122,18 @@ public function getOnClick() } } + /** + * @inheritDoc + */ + protected function _beforeToHtml() + { + parent::_beforeToHtml(); + + $this->setData('ui_button_widget_hook_id', 'buttonId' .$this->random->getRandomString(10)); + + return $this; + } + /** * Prepare attributes * @@ -107,8 +150,6 @@ protected function prepareAttributes($title, $classes, $disabled) 'title' => $title, 'type' => $this->getType(), 'class' => implode(' ', $classes), - 'onclick' => $this->getOnClick(), - 'style' => $this->getStyle(), 'value' => $this->getValue(), 'disabled' => $disabled, ]; @@ -117,6 +158,9 @@ protected function prepareAttributes($title, $classes, $disabled) $attributes['data-' . $key] = is_scalar($attr) ? $attr : json_encode($attr); } } + if ($this->hasData('ui_button_widget_hook_id')) { + $attributes['ui-button-widget-hook-id'] = $this->getData('ui_button_widget_hook_id'); + } return $attributes; } @@ -139,4 +183,31 @@ protected function attributesToHtml($attributes) return $html; } + + /** + * Return HTML to be rendered after the button. + * + * @return string|null + */ + public function getAfterHtml(): ?string + { + $afterHtml = $this->getData('after_html'); + $buttonId = $this->getData('ui_button_widget_hook_id'); + if ($handler = $this->getOnClick()) { + $afterHtml .= $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + $handler, + "*[ui-button-widget-hook-id='$buttonId']" + ); + } + if ($this->getStyle()) { + $selector = "*[ui-button-widget-hook-id='$buttonId']"; + if ($this->getId()) { + $selector = "#{$this->getId()}"; + } + $afterHtml .= $this->secureRenderer->renderStyleAsTag($this->getStyle(), $selector); + } + + return $afterHtml; + } } diff --git a/app/code/Magento/Ui/Component/Control/SplitButton.php b/app/code/Magento/Ui/Component/Control/SplitButton.php index 5c9d09565fc66..64ca6cf3dfee6 100644 --- a/app/code/Magento/Ui/Component/Control/SplitButton.php +++ b/app/code/Magento/Ui/Component/Control/SplitButton.php @@ -6,8 +6,13 @@ namespace Magento\Ui\Component\Control; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** - * Class SplitButton + * Widget for standard button with a selection. * * @method string getTitle * @method string getLabel @@ -22,6 +27,32 @@ */ class SplitButton extends Button { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + + /** + * @inheritDoc + */ + public function __construct( + Context $context, + array $data = [], + ?Random $random = null, + ?SecureHtmlRenderer $htmlRenderer = null + ) { + $random = $random ?? ObjectManager::getInstance()->get(Random::class); + $htmlRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($context, $data, $random, $htmlRenderer); + $this->random = $random; + $this->secureRenderer = $htmlRenderer; + } + /** * @inheritdoc */ @@ -54,6 +85,16 @@ public function getAttributesHtml() return $this->attributesToHtml(['title' => $title, 'class' => join(' ', $classes)]); } + /** + * Get main button's "id" attribute value. + * + * @return string + */ + private function getButtonId(): string + { + return $this->getId() .'-button'; + } + /** * Retrieve button attributes html * @@ -77,11 +118,10 @@ public function getButtonAttributesHtml() } $attributes = [ - 'id' => $this->getId() . '-button', + 'id' => $this->getButtonId(), 'title' => $title, 'class' => join(' ', $classes), 'disabled' => $disabled, - 'style' => $this->getStyle(), ]; if ($idHard = $this->getIdHard()) { @@ -159,6 +199,21 @@ public function getOptionAttributesHtml($key, $option) return $html; } + /** + * Retrieve "id" attribute value for an option. + * + * @param array $option + * @return string + */ + private function identifyOption(array $option): string + { + return isset($option['id']) + ? $this->getId() .'-' .$option['id'] + : (isset($option['id_attribute']) ? + $option['id_attribute'] + : $this->getId() .'-optId' .$this->random->getRandomString(10)); + } + /** * Prepare option attributes * @@ -172,11 +227,9 @@ public function getOptionAttributesHtml($key, $option) protected function prepareOptionAttributes($option, $title, $classes, $disabled) { $attributes = [ - 'id' => isset($option['id']) ? $this->getId() . '-' . $option['id'] : '', + 'id' => $this->identifyOption($option), 'title' => $title, 'class' => join(' ', $classes), - 'onclick' => isset($option['onclick']) ? $option['onclick'] : '', - 'style' => isset($option['style']) ? $option['style'] : '', 'disabled' => $disabled, ]; @@ -215,4 +268,43 @@ protected function getDataAttributes($data, &$attributes) $attributes['data-' . $key] = is_scalar($attr) ? $attr : json_encode($attr); } } + + /** + * @inheritDoc + */ + protected function _beforeToHtml() + { + parent::_beforeToHtml(); + + /** @var array|null $options */ + $options = $this->getOptions() ?? []; + foreach ($options as &$option) { + $option['id_attribute'] = $this->identifyOption($option); + } + $this->setOptions($options); + + return $this; + } + + /** + * @inheritDoc + */ + public function getAfterHtml(): ?string + { + $afterHtml = parent::getAfterHtml(); + + /** @var array|null $options */ + $options = $this->getOptions() ?? []; + foreach ($options as $option) { + $id = $this->identifyOption($option); + if (!empty($option['onclick'])) { + $afterHtml .= $this->secureRenderer->renderEventListenerAsTag('onclick', $option['onclick'], "#$id"); + } + if (!empty($option['style'])) { + $afterHtml .= $this->secureRenderer->renderStyleAsTag($option['style'], "#$id"); + } + } + + return $afterHtml; + } } diff --git a/app/code/Magento/Ui/Component/Form/Element/ColorPicker.php b/app/code/Magento/Ui/Component/Form/Element/ColorPicker.php index b1925b4641d0b..23188363cc1d1 100644 --- a/app/code/Magento/Ui/Component/Form/Element/ColorPicker.php +++ b/app/code/Magento/Ui/Component/Form/Element/ColorPicker.php @@ -17,6 +17,7 @@ * Prepares Color Picker UI component with mode and format * * @api + * @since 101.1.0 */ class ColorPicker extends AbstractElement { @@ -54,6 +55,7 @@ public function __construct( * Get component name * * @return string + * @since 101.1.0 */ public function getComponentName(): string { @@ -64,6 +66,7 @@ public function getComponentName(): string * Prepare component configuration * * @return void + * @since 101.1.0 */ public function prepare() : void { diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php index 27370cbfbd68c..9961fc41fc70d 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php @@ -8,10 +8,8 @@ namespace Magento\Ui\Component\Form\Element\DataType\Media; -use Magento\Framework\DataObject; - /** - * Basic configuration for OdenDialogUrl + * Basic configuration for OpenDialogUrl */ class OpenDialogUrl { @@ -23,11 +21,11 @@ class OpenDialogUrl private $openDialogUrl; /** - * @param DataObject $url + * @param string $url */ - public function __construct(DataObject $url = null) + public function __construct(string $url = null) { - $this->openDialogUrl = $url; + $this->openDialogUrl = $url ?? self::DEFAULT_OPEN_DIALOG_URL; } /** @@ -37,9 +35,6 @@ public function __construct(DataObject $url = null) */ public function get(): string { - if ($this->openDialogUrl) { - return $this->openDialogUrl->getUrl(); - } - return self::DEFAULT_OPEN_DIALOG_URL; + return $this->openDialogUrl; } } diff --git a/app/code/Magento/Ui/Component/Listing/Columns/Date.php b/app/code/Magento/Ui/Component/Listing/Columns/Date.php index f0b3ee4334d4f..d141499718a76 100644 --- a/app/code/Magento/Ui/Component/Listing/Columns/Date.php +++ b/app/code/Magento/Ui/Component/Listing/Columns/Date.php @@ -77,6 +77,7 @@ public function __construct( /** * @inheritdoc + * @since 101.1.1 */ public function prepare() { diff --git a/app/code/Magento/Ui/Component/Wrapper/Block.php b/app/code/Magento/Ui/Component/Wrapper/Block.php index a4e5bbf213062..0380a447e0cb9 100644 --- a/app/code/Magento/Ui/Component/Wrapper/Block.php +++ b/app/code/Magento/Ui/Component/Wrapper/Block.php @@ -11,7 +11,7 @@ use Magento\Framework\View\Element\UiComponent\BlockWrapperInterface; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ class Block extends AbstractComponent implements BlockWrapperInterface { diff --git a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php index b45880c1ce726..803495439d65e 100644 --- a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php +++ b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php @@ -54,7 +54,7 @@ class Save extends AbstractAction implements HttpPostActionInterface /** * @var DecoderInterface - * @deprecated + * @deprecated 101.1.0 */ protected $jsonDecoder; diff --git a/app/code/Magento/Ui/Controller/Index/Render.php b/app/code/Magento/Ui/Controller/Index/Render.php index 42818686840aa..3ec58784ef53b 100644 --- a/app/code/Magento/Ui/Controller/Index/Render.php +++ b/app/code/Magento/Ui/Controller/Index/Render.php @@ -97,11 +97,8 @@ public function __construct( public function execute() { if ($this->_request->getParam('namespace') === null) { - $this->_redirect('admin/noroute'); - - return; + return $this->_redirect('noroute'); } - try { $component = $this->uiComponentFactory->create($this->getRequest()->getParam('namespace')); if ($this->validateAclResource($component->getContext()->getDataProvider()->getConfigData())) { @@ -110,6 +107,7 @@ public function execute() $contentType = $this->contentTypeResolver->resolve($component->getContext()); $this->getResponse()->setHeader('Content-Type', $contentType, true); + return $this->getResponse(); } else { /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); diff --git a/app/code/Magento/Ui/DataProvider/AbstractDataProvider.php b/app/code/Magento/Ui/DataProvider/AbstractDataProvider.php index abbc79859a038..6e4e488619e86 100644 --- a/app/code/Magento/Ui/DataProvider/AbstractDataProvider.php +++ b/app/code/Magento/Ui/DataProvider/AbstractDataProvider.php @@ -297,7 +297,7 @@ public function setConfigData($config) * Retrieve all ids from collection * * @return int[] - * @since 100.2.0 + * @since 101.0.0 */ public function getAllIds() { diff --git a/app/code/Magento/Ui/DataProvider/Modifier/WysiwygModifierInterface.php b/app/code/Magento/Ui/DataProvider/Modifier/WysiwygModifierInterface.php index 6336d8f8fb828..5c70a06dad318 100644 --- a/app/code/Magento/Ui/DataProvider/Modifier/WysiwygModifierInterface.php +++ b/app/code/Magento/Ui/DataProvider/Modifier/WysiwygModifierInterface.php @@ -7,20 +7,25 @@ /** * @api + * @since 101.1.0 */ interface WysiwygModifierInterface { /** - * Provide editor name - * For example tmce3 or tmce4 + * Provide editor name for example tmce4 * * @return array + * @since 101.1.0 */ public function getEditorName(); /** + * Modifies the meta + * * @param array $meta + * * @return array + * @since 101.1.0 */ public function modifyMeta(array $meta); } diff --git a/app/code/Magento/Ui/DataProvider/SearchResultFactory.php b/app/code/Magento/Ui/DataProvider/SearchResultFactory.php index f2ed0677d4cd9..83d06c7cf5fc1 100644 --- a/app/code/Magento/Ui/DataProvider/SearchResultFactory.php +++ b/app/code/Magento/Ui/DataProvider/SearchResultFactory.php @@ -17,6 +17,7 @@ * Allows to use Repositories (instead of Collections) in UI Components Data providers * * @api + * @since 101.1.0 */ class SearchResultFactory { @@ -64,6 +65,7 @@ public function __construct( * @param SearchCriteriaInterface SearchCriteriaInterface $searchCriteria * @param string $idFieldName * @return SearchResultInterface + * @since 101.1.0 */ public function create( array $items, diff --git a/app/code/Magento/Ui/Model/Bookmark.php b/app/code/Magento/Ui/Model/Bookmark.php index b404e8d3b475f..2cb5666063067 100644 --- a/app/code/Magento/Ui/Model/Bookmark.php +++ b/app/code/Magento/Ui/Model/Bookmark.php @@ -23,7 +23,7 @@ class Bookmark extends AbstractExtensibleModel implements BookmarkInterface { /** * @var DecoderInterface - * @deprecated + * @deprecated 101.1.0 */ protected $jsonDecoder; diff --git a/app/code/Magento/Ui/Model/Manager.php b/app/code/Magento/Ui/Model/Manager.php index 1bacdc80a5c5e..357a41285e275 100644 --- a/app/code/Magento/Ui/Model/Manager.php +++ b/app/code/Magento/Ui/Model/Manager.php @@ -24,7 +24,7 @@ /** * @inheritdoc * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Manager implements ManagerInterface diff --git a/app/code/Magento/Ui/Model/ResourceModel/BookmarkRepository.php b/app/code/Magento/Ui/Model/ResourceModel/BookmarkRepository.php index 3e738baa404c8..f773daaaf50c4 100644 --- a/app/code/Magento/Ui/Model/ResourceModel/BookmarkRepository.php +++ b/app/code/Magento/Ui/Model/ResourceModel/BookmarkRepository.php @@ -162,7 +162,7 @@ public function deleteById($bookmarkId) * @param FilterGroup $filterGroup * @param Collection $collection * @return void - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @throws \Magento\Framework\Exception\InputException */ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collection $collection) @@ -176,7 +176,7 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collecti /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAddColumnToAdminGridActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAddColumnToAdminGridActionGroup.xml new file mode 100644 index 0000000000000..25cc4b5c17d92 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAddColumnToAdminGridActionGroup.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="AdminAddColumnToAdminGridActionGroup"> + <annotations> + <description value="Adds column to admin grid"/> + </annotations> + <arguments> + <argument name="columnName" defaultValue="Email" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminGridColumnsControls.columns}}" stepKey="waitForAdminGridColumnControlsColumn"/> + <click selector="{{AdminGridColumnsControls.columns}}" stepKey="clickAdminGridColumnControlsColumn"/> + <waitForElementVisible selector="{{AdminDataGridHeaderSection.columnCheckbox(columnName)}}" stepKey="verifyAdminGridColumnControlsForSelectedColumnVisible"/> + <click selector="{{AdminDataGridHeaderSection.columnCheckbox(columnName)}}" stepKey="clickForAdminGridControlForSelectedColumn"/> + <waitForElementVisible selector="{{AdminGridHeaders.headerByName(columnName)}}" stepKey="waitForAdminGridColumnHeaderForSelectedColumn"/> + <click selector="{{AdminGridColumnsControls.columns}}" stepKey="closeAdminGridColumnControls"/> + <waitForElementNotVisible selector="{{AdminGridColumnsControls.columnName(columnName)}}" stepKey="verifyAdminGridColumnControlsForSelectedColumnNotVisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridBulkActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridBulkActionGroup.xml new file mode 100644 index 0000000000000..9db9ea7becfc8 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridBulkActionGroup.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="AdminGridBulkActionGroup"> + <annotations> + <description> + Massive action for all rows on Admin Grid page. + </description> + </annotations> + <arguments> + <argument name="actionLabel" type="string"/> + </arguments> + + <click selector="{{AdminGridSelectRows.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> + <click selector="{{AdminGridSelectRows.multicheckOption('Select All')}}" stepKey="selectAllRows"/> + <click selector="{{AdminGridSelectRows.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminGridSelectRows.bulkActionOption(actionLabel)}}" stepKey="clickActionLabel"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridColumnShowActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridColumnShowActionGroup.xml new file mode 100644 index 0000000000000..6440cc01bcafe --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridColumnShowActionGroup.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="AdminGridColumnShowActionGroup"> + <annotations> + <description> + Shows new column on Admin Grid page. + </description> + </annotations> + <arguments> + <argument name="columnLabel" type="string"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.columnsToggle}}" stepKey="openColumnsTab"/> + <checkOption selector="{{AdminDataGridHeaderSection.columnCheckbox(columnLabel)}}" stepKey="showNewColumn"/> + <click selector="{{AdminDataGridHeaderSection.columnsToggle}}" stepKey="closeColumnsTab"/> + <seeElement selector="{{AdminDataGridTableSection.columnHeader(columnLabel)}}" stepKey="seeNewColumnInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridSelectAllActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridSelectAllActionGroup.xml new file mode 100644 index 0000000000000..bbfb7e46d89ec --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridSelectAllActionGroup.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="AdminGridSelectAllActionGroup"> + <annotations> + <description>Click on select all option on the grid</description> + </annotations> + + <waitForElementVisible selector="{{AdminGridSelectRows.multicheckDropdown}}" stepKey="waitForElement"/> + <click selector="{{AdminGridSelectRows.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> + <click selector="{{AdminGridSelectRows.multicheckOption('Select All')}}" stepKey="clickSelectAllCustomers"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/StorefrontAssertErrorMessageActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/StorefrontAssertErrorMessageActionGroup.xml new file mode 100644 index 0000000000000..fa9b7c377e32b --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/StorefrontAssertErrorMessageActionGroup.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="StorefrontAssertErrorMessageActionGroup"> + <arguments> + <argument name="message" type="string"/> + <argument name="messageType" type="string" defaultValue="success"/> + </arguments> + + <see userInput="{{message}}" selector="{{StorefrontMessagesSection.messageByType(messageType)}}" stepKey="verifyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml index 133836761174d..51cebdb01a74d 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml @@ -21,5 +21,8 @@ <element name="currentPage" type="input" selector="div.admin__data-grid-pager > input[data-ui-id='current-page-input']"/> <element name="totalPages" type="text" selector="div.admin__data-grid-pager > label"/> <element name="perPageDropDownValue" type="input" selector=".selectmenu-value input" timeout="30"/> + <element name="selectedPage" type="input" selector="#sales_order_create_search_grid_page-current" timeout="30"/> + <element name="nextPageActive" type="button" selector="div.admin__data-grid-pager > button.action-next:not(.disabled)" timeout="30"/> + <element name="prevPageActive" type="button" selector="div.admin__data-grid-pager > button.action-previous:not(.disabled)" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml index fcee31c0bd80c..c5b000259e265 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml @@ -9,7 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminDataGridTableSection"> - <element name="firstRow" type="button" selector="tr.data-row:nth-of-type(1)" timeout="60"/> + <element name="firstRow" type="button" selector="table.data-grid tbody > tr:nth-of-type(1)" timeout="60"/> <element name="columnHeader" type="button" selector="//div[@data-role='grid-wrapper']//table[contains(@class, 'data-grid')]/thead/tr/th[contains(@class, 'data-grid-th')]/span[text() = '{{label}}']" parameterized="true" timeout="30"/> <element name="column" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{col}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> <element name="rowCheckbox" type="checkbox" selector="table.data-grid tbody > tr:nth-of-type({{row}}) td.data-grid-checkbox-cell input" parameterized="true"/> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml index c58479a7b73e5..f46e25a832134 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml @@ -12,5 +12,6 @@ <element name="success" type="text" selector="div.message-success.success.message"/> <element name="error" type="text" selector="div.message-error.error.message"/> <element name="noticeMessage" type="text" selector="div.message.notice div"/> + <element name="messageByType" type="text" selector=".messages .message-{{messageType}}" parameterized="true" /> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml index d38e065914617..3c93ed38b4eed 100644 --- a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml @@ -51,8 +51,7 @@ </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="AdminOpenProductIndexPageActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="$$createProduct$$"/> </actionGroup> 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/Test/Unit/Component/Control/ButtonTest.php b/app/code/Magento/Ui/Test/Unit/Component/Control/ButtonTest.php index b62075c3d8cb1..77c0bfd62865e 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Control/ButtonTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Control/ButtonTest.php @@ -60,7 +60,7 @@ public function testGetType() public function testGetAttributesHtml() { $expected = 'type="button" class="action- scalable classValue disabled" ' - . 'onclick="location.href = 'url2';" disabled="disabled" data-attributeKey="attributeValue" '; + . 'disabled="disabled" data-attributeKey="attributeValue" '; $this->button->setDisabled(true); $this->button->setData('url', 'url2'); $this->button->setData('class', 'classValue'); diff --git a/app/code/Magento/Ui/view/base/requirejs-config.js b/app/code/Magento/Ui/view/base/requirejs-config.js index 5e76600673254..4ca2c39781343 100644 --- a/app/code/Magento/Ui/view/base/requirejs-config.js +++ b/app/code/Magento/Ui/view/base/requirejs-config.js @@ -4,6 +4,7 @@ */ var config = { + deps: [], shim: { 'chartjs/Chart.min': ['moment'], 'tiny_mce_4/tinymce.min': { @@ -30,3 +31,29 @@ var config = { } } }; + +/** + * Adds polyfills only for browser contexts which prevents bundlers from including them. + */ +if (typeof window !== 'undefined' && window.document) { + /** + * Polyfill Map and WeakMap for older browsers that do not support them. + */ + if (typeof Map === 'undefined' || typeof WeakMap === 'undefined') { + config.deps.push('es6-collections'); + } + + /** + * Polyfill MutationObserver only for the browsers that do not support it. + */ + if (typeof MutationObserver === 'undefined') { + config.deps.push('MutationObserver'); + } + + /** + * Polyfill FormData object for old browsers that don't have full support for it. + */ + if (typeof FormData === 'undefined' || typeof FormData.prototype.get === 'undefined') { + config.deps.push('FormData'); + } +} diff --git a/app/code/Magento/Ui/view/base/templates/control/button/split.phtml b/app/code/Magento/Ui/view/base/templates/control/button/split.phtml index 08230184d5a4d..ce7112c400509 100644 --- a/app/code/Magento/Ui/view/base/templates/control/button/split.phtml +++ b/app/code/Magento/Ui/view/base/templates/control/button/split.phtml @@ -11,19 +11,19 @@ <button <?= $block->getButtonAttributesHtml() ?>> <span><?= $block->escapeHtml($block->getLabel()) ?></span> </button> - <?php if ($block->hasSplit()) : ?> + <?php if ($block->hasSplit()): ?> <button <?= $block->getToggleAttributesHtml() ?>> <span><?= $block->escapeHtml(__('Select')) ?></span> </button> - <?php if (!$block->getDisabled()) : ?> + <?php if (!$block->getDisabled()): ?> <ul class="dropdown-menu" <?= /* @noEscape */ $block->getUiId("dropdown-menu") ?>> - <?php foreach ($block->getOptions() as $key => $option) : ?> + <?php foreach ($block->getOptions() as $key => $option): ?> <li> <span <?= $block->getOptionAttributesHtml($key, $option) ?>> <?= $block->escapeHtml($option['label']) ?> </span> - <?php if (isset($option['hint'])) : ?> + <?php if (isset($option['hint'])): ?> <div class="tooltip" <?= /* @noEscape */ $block->getUiId('item', $key, 'tooltip') ?>> <a href="<?= $block->escapeUrl($option['hint']['href']) ?>" class="help"> <?= $block->escapeHtml($option['hint']['label']) ?> @@ -36,6 +36,7 @@ <?php endif; ?> <?php endif; ?> </div> +<?= /* @noEscape */$block->getAfterHtml() ?> <script type="text/x-magento-init"> { ".actions-split": { diff --git a/app/code/Magento/Ui/view/base/templates/logger.phtml b/app/code/Magento/Ui/view/base/templates/logger.phtml index 7466781a606f1..b93ac7542464b 100644 --- a/app/code/Magento/Ui/view/base/templates/logger.phtml +++ b/app/code/Magento/Ui/view/base/templates/logger.phtml @@ -5,11 +5,13 @@ */ /** @var $block \Magento\Ui\Block\Logger */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->isLoggingEnabled()) : ?> - <script> +<?php if ($block->isLoggingEnabled()): ?> + <?php $scriptString = <<<script + window.onerror = function(msg, url, line) { - var key = "<?= $block->escapeJs($block->getSessionStorageKey()) ?>"; + var key = "{$block->escapeJs($block->getSessionStorageKey())}"; var errors = {}; if (sessionStorage.getItem(key)) { errors = JSON.parse(sessionStorage.getItem(key)); @@ -20,5 +22,7 @@ errors[window.location.href].push("error: \'" + msg + "\' " + "file: " + url + " " + "line: " + line); sessionStorage.setItem(key, JSON.stringify(errors)); }; - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml b/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml index a7c279c431665..ff8236478bbbd 100644 --- a/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml +++ b/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml @@ -5,13 +5,17 @@ */ /** @var Magento\Ui\Block\Wysiwyg\ActiveEditor $block */ -?> -<script> +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$wysiwygAdapterPath = /* @noEscape */ $block->getWysiwygAdapterPath(); +$scriptString = <<<script require.config({ map: { '*': { - wysiwygAdapter: '<?= /* @noEscape */ $block->getWysiwygAdapterPath() ?>' + wysiwygAdapter: '{$wysiwygAdapterPath}' } } }); -</script> +script; + +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Ui/view/base/web/js/block-loader.js b/app/code/Magento/Ui/view/base/web/js/block-loader.js index 531591b41b0d8..e509f7dc23fea 100644 --- a/app/code/Magento/Ui/view/base/web/js/block-loader.js +++ b/app/code/Magento/Ui/view/base/web/js/block-loader.js @@ -15,14 +15,18 @@ define([ blockContentLoadingClass = '_block-content-loading', blockLoader, blockLoaderClass, - loaderImageHref; + blockLoaderElement = $.Deferred(), + loaderImageHref = $.Deferred(); templateLoader.loadTemplate(blockLoaderTemplatePath).done(function (blockLoaderTemplate) { - blockLoader = template($.trim(blockLoaderTemplate), { - loaderImageHref: loaderImageHref + loaderImageHref.done(function (loaderHref) { + blockLoader = template($.trim(blockLoaderTemplate), { + loaderImageHref: loaderHref + }); + blockLoader = $(blockLoader); + blockLoaderClass = '.' + blockLoader.attr('class'); + blockLoaderElement.resolve(); }); - blockLoader = $(blockLoader); - blockLoaderClass = '.' + blockLoader.attr('class'); }); /** @@ -70,7 +74,7 @@ define([ } return function (loaderHref) { - loaderImageHref = loaderHref; + loaderImageHref.resolve(loaderHref); ko.bindingHandlers.blockLoader = { /** * Process loader for block @@ -81,9 +85,9 @@ define([ element = $(element); if (ko.unwrap(displayBlockLoader())) { - addBlockLoader(element); + blockLoaderElement.done(addBlockLoader(element)); } else { - removeBlockLoader(element); + blockLoaderElement.done(removeBlockLoader(element)); } } }; diff --git a/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js b/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js index ac1de4631e908..5240fe55f6a74 100644 --- a/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js +++ b/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js @@ -499,7 +499,7 @@ define([ component = registry.get(val.path); if (component) { - component.cleanData().destroy(); + component.destroy(); } }); 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 b488a4b2f8c16..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 @@ -291,6 +291,13 @@ define([ return false; }, + /** + * Return empty options html + */ + getEmptyOptionsUnsanitizedHtml: function () { + return this.emptyOptionsHtml; + }, + /** * Check options length and set to cache * if some options is added @@ -661,7 +668,7 @@ define([ * @returns {Object} Chainable */ toggleListVisible: function () { - this.listVisible(!this.listVisible()); + this.listVisible(!this.disabled() && !this.listVisible()); return this; }, @@ -748,11 +755,6 @@ define([ return this.value() ? !!this.value().length : false; }, - /** - * @deprecated - */ - onMousemove: function () {}, - /** * Handles hover on list items. * @@ -1167,7 +1169,7 @@ define([ return; } - if (searchKey !== this.lastSearchKey) { + if (currentPage === 1) { this.options([]); } this.processRequest(searchKey, currentPage); 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/columns/image.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image.js index e8e1cf3246c76..611a14ce778de 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/image.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image.js @@ -11,6 +11,7 @@ define([ defaults: { bodyTmpl: 'ui/grid/columns/image', modules: { + masonry: '${ $.parentName }', previewComponent: '${ $.parentName }.preview' }, previewRowId: null, @@ -35,6 +36,15 @@ define([ return this; }, + /** + * Updates styles when image loaded. + * + * @param {Object} record + */ + updateStyles: function (record) { + !record.lastInRow || this.masonry().updateStyles(); + }, + /** * Returns url to given record. * diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/multiselect.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/multiselect.js index ba0f4d25c25a4..828bbccee0478 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/multiselect.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/multiselect.js @@ -52,6 +52,7 @@ define([ listens: { '${ $.provider }:params.filters': 'onFilter', + '${ $.provider }:params.search': 'onSearch', selected: 'onSelectedChange', rows: 'onRowsChange' }, @@ -235,7 +236,7 @@ define([ * @returns {Multiselect} Chainable. */ togglePage: function () { - return this.isPageSelected() ? this.deselectPage() : this.selectPage(); + return this.isPageSelected() && !this.excluded().length ? this.deselectPage() : this.selectPage(); }, /** @@ -496,6 +497,13 @@ define([ if (!this.preserveSelectionsOnFilter) { this.deselectAll(); } + }, + + /** + * Is invoked when search is applied or removed + */ + onSearch: function () { + this.onFilter(); } }); }); 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/masonry.js b/app/code/Magento/Ui/view/base/web/js/grid/masonry.js index e4c72ee950c26..ac17c7fb565e1 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/masonry.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/masonry.js @@ -112,13 +112,20 @@ define([ */ setEventListener: function () { window.addEventListener('resize', function () { - raf(function () { - this.containerWidth = window.innerWidth; - this.setLayoutStyles(); - }.bind(this), this.refreshFPS); + this.updateStyles(); }.bind(this)); }, + /** + * Updates styles for component. + */ + updateStyles: function () { + raf(function () { + this.containerWidth = window.innerWidth; + this.setLayoutStyles(); + }.bind(this), this.refreshFPS); + }, + /** * Set layout styles inside the container */ 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 new file mode 100644 index 0000000000000..3c5e72d4d66ed --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js @@ -0,0 +1,101 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + '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 }' + } + }, + + /** + * Init component + * + * @return {exports} + */ + initialize: function () { + this._super(); + this.apply(); + + return this; + }, + + /** + * Apply filter + */ + apply: function () { + var urlFilter = this.getFilterParam(this.searchString), + applied, + filters; + + if (_.isUndefined(this.filterComponent())) { + setTimeout(function () { + this.apply(); + }.bind(this), 100); + + 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) { + applied = this.filterComponent().get('applied'); + filters = $.extend({}, applied, urlFilter); + this.filterComponent().set('applied', filters); + } + }, + + /** + * Get filter param from url + * + * @returns {Object} + */ + getFilterParam: function (url) { + var searchString = decodeURI(url), + itemArray; + + return _.chain(searchString.slice(1).split('&')) + .map(function (item) { + + if (item && item.search(this.filterKey) !== -1) { + itemArray = item.split('='); + + if (itemArray[1].search('\\[') === 0) { + itemArray[1] = itemArray[1].replace(/[\[\]]/g, '').split(','); + } + + itemArray[0] = itemArray[0].replace(this.filterKey, '') + .replace(/[\[\]]/g, ''); + + return itemArray; + } + }.bind(this)) + .compact() + .object() + .value(); + } + }); +}); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/core/events.js b/app/code/Magento/Ui/view/base/web/js/lib/core/events.js index fdb11cd89f361..15965fba1ad2d 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/core/events.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/core/events.js @@ -6,8 +6,7 @@ /* global WeakMap, Map*/ define([ 'ko', - 'underscore', - 'es6-collections' + 'underscore' ], function (ko, _) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js index 2fab8c219c02a..284d395d8120b 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js @@ -7,11 +7,8 @@ define([ 'ko', 'underscore', 'jquery', - 'mage/translate', - 'mage/calendar', - 'moment', - 'mageUtils' -], function (ko, _, $, $t, calendar, moment, utils) { + 'mage/translate' +], function (ko, _, $, $t) { 'use strict'; var defaults = { @@ -46,10 +43,12 @@ define([ observable = config; } - $(el).calendar(options); + require(['mage/calendar'], function () { + $(el).calendar(options); - ko.utils.registerEventHandler(el, 'change', function () { - observable(this.value); + ko.utils.registerEventHandler(el, 'change', function () { + observable(this.value); + }); }); }, @@ -62,6 +61,7 @@ define([ */ update: function (element, valueAccessor) { var config = valueAccessor(), + $element = $(element), observable, options = {}, newVal; @@ -75,26 +75,21 @@ define([ observable = config; } - if (_.isEmpty(observable())) { - if ($(element).datepicker('getDate')) { - $(element).datepicker('setDate', null); - $(element).blur(); + require(['moment', 'mage/utils/misc', 'mage/calendar'], function (moment, utils) { + if (_.isEmpty(observable())) { + newVal = null; + } else { + newVal = moment( + observable(), + utils.convertToMomentFormat( + options.dateFormat + (options.showsTime ? ' ' + options.timeFormat : '') + ) + ).toDate(); } - } else { - newVal = moment( - observable(), - utils.convertToMomentFormat( - options.dateFormat + (options.showsTime ? ' ' + options.timeFormat : '') - ) - ).toDate(); - if ($(element).datepicker('getDate') == null || - newVal.valueOf() !== $(element).datepicker('getDate').valueOf() - ) { - $(element).datepicker('setDate', newVal); - $(element).blur(); - } - } + $element.datepicker('setDate', newVal); + $element.blur(); + }); } }; }); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js index 1dda3254f4613..52031dc0c3792 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js @@ -7,13 +7,18 @@ define([ 'ko', 'jquery', 'underscore', - '../template/renderer', - 'jquery-ui-modules/slider' + '../template/renderer' ], function (ko, $, _, renderer) { 'use strict'; var isTouchDevice = !_.isUndefined(document.ontouchstart), - sliderFn = 'slider'; + sliderFn = 'slider', + sliderModule = 'jquery-ui-modules/slider'; + + if (isTouchDevice) { + sliderFn = 'touchSlider'; + sliderModule = 'mage/touch-slider'; + } ko.bindingHandlers.range = { @@ -41,7 +46,9 @@ define([ } }); - $(element)[sliderFn](config); + require([sliderModule], function () { + $(element)[sliderFn](config); + }); }, /** @@ -55,149 +62,11 @@ define([ config.value = ko.unwrap(config.value); - $(element)[sliderFn]('option', config); + require([sliderModule], function () { + $(element)[sliderFn]('option', config); + }); } }; renderer.addAttribute('range'); - - if (!isTouchDevice) { - return; - } - - $.widget('mage.touchSlider', $.ui.slider, { - - /** - * Creates instance of widget. - * - * @override - */ - _create: function () { - _.bindAll( - this, - '_mouseDown', - '_mouseMove', - '_onTouchEnd' - ); - - return this._superApply(arguments); - }, - - /** - * Initializes mouse events on element. - * @override - */ - _mouseInit: function () { - var result = this._superApply(arguments); - - this.element - .off('mousedown.' + this.widgetName) - .on('touchstart.' + this.widgetName, this._mouseDown); - - return result; - }, - - /** - * Elements' 'mousedown' event handler polyfill. - * @override - */ - _mouseDown: function (event) { - var prevDelegate = this._mouseMoveDelegate, - result; - - event = this._touchToMouse(event); - result = this._super(event); - - if (prevDelegate === this._mouseMoveDelegate) { - return result; - } - - $(document) - .off('mousemove.' + this.widgetName) - .off('mouseup.' + this.widgetName); - - $(document) - .on('touchmove.' + this.widgetName, this._mouseMove) - .on('touchend.' + this.widgetName, this._onTouchEnd) - .on('tochleave.' + this.widgetName, this._onTouchEnd); - - return result; - }, - - /** - * Documents' 'mousemove' event handler polyfill. - * - * @override - * @param {Event} event - Touch event object. - */ - _mouseMove: function (event) { - event = this._touchToMouse(event); - - return this._super(event); - }, - - /** - * Documents' 'touchend' event handler. - */ - _onTouchEnd: function (event) { - $(document).trigger('mouseup'); - - return this._mouseUp(event); - }, - - /** - * Removes previously assigned touch handlers. - * - * @override - */ - _mouseUp: function () { - this._removeTouchHandlers(); - - return this._superApply(arguments); - }, - - /** - * Removes previously assigned touch handlers. - * - * @override - */ - _mouseDestroy: function () { - this._removeTouchHandlers(); - - return this._superApply(arguments); - }, - - /** - * Removes touch events from document object. - */ - _removeTouchHandlers: function () { - $(document) - .off('touchmove.' + this.widgetName) - .off('touchend.' + this.widgetName) - .off('touchleave.' + this.widgetName); - }, - - /** - * Adds properties to the touch event to mimic mouse event. - * - * @param {Event} event - Touch event object. - * @returns {Event} - */ - _touchToMouse: function (event) { - var orig = event.originalEvent, - touch = orig.touches[0]; - - return _.extend(event, { - which: 1, - pageX: touch.pageX, - pageY: touch.pageY, - clientX: touch.clientX, - clientY: touch.clientY, - screenX: touch.screenX, - screenY: touch.screenY - }); - } - }); - - sliderFn = 'touchSlider'; }); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js index 6b3c437b90508..0b80a75bf0c18 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js @@ -8,8 +8,7 @@ define([ 'ko', 'underscore', 'mage/utils/wrapper', - 'uiEvents', - 'es6-collections' + 'uiEvents' ], function (ko, _, wrapper, Events) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js b/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js index 826e8ec8c33b4..18e05b8daac68 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js @@ -9,8 +9,7 @@ /* global WeakMap */ define([ 'jquery', - 'underscore', - 'es6-collections' + 'underscore' ], function ($, _) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js index f8e752fb77af2..cb9f5b13de494 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js @@ -5,7 +5,6 @@ define([ 'jquery', 'underscore', - 'MutationObserver', 'domReady!' ], function ($, _) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/raf.js b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/raf.js index 3ec0996543c7d..bc8e3095b5cd2 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/raf.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/raf.js @@ -4,9 +4,7 @@ */ /* global WeakMap */ -define([ - 'es6-collections' -], function () { +define([], function () { 'use strict'; var processMap = new WeakMap(), diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/cells/thumbnail.html b/app/code/Magento/Ui/view/base/web/templates/grid/cells/thumbnail.html index 705becce75a0a..25f26813d6eaa 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/cells/thumbnail.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/cells/thumbnail.html @@ -4,4 +4,8 @@ * See COPYING.txt for license details. */ --> -<img class="admin__control-thumbnail" attr="src: $col.getSrc($row()), alt: $col.getAlt($row())"/> +<span class="thumbnail-container"> + <span class="thumbnail-wrapper"> + <img class="admin__control-thumbnail" attr="src: $col.getSrc($row()), alt: $col.getAlt($row())"/> + </span> +</span> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html b/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html index fa0074ad72283..e9834ac449cce 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html @@ -5,5 +5,5 @@ */ --> <div class="masonry-image-block" ko-style="$col.getStyles($row())" css="{'active': $col.getIsActive($row())}" attr="'data-id': $col.getId($row())"> - <img attr="src: $col.getUrl($row())" css="$col.getClasses($row())" click="function(){ expandPreview($row()) }" data-role="thumbnail"/> + <img data-bind="event: { load: updateStyles($row()) }" attr="src: $col.getUrl($row())" css="$col.getClasses($row())" click="function(){ expandPreview($row()) }" data-role="thumbnail"/> </div> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html index b9425c020c0e9..5036b7121c626 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html @@ -141,13 +141,12 @@ </div> <div ifnot="options().length" class="admin__action-multiselect-empty-area"> - <ul data-bind="html: emptyOptionsHtml"/> + <ul data-bind="html: getEmptyOptionsUnsanitizedHtml()"/> </div> <!-- /ko --> <ul class="admin__action-multiselect-menu-inner _root" data-bind=" event: { - mousemove: function(data, event){onMousemove($data, $index(), event)}, scroll: function(data, event){onScrollDown(data, event)} } "> 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/Ups/Block/Backend/System/CarrierConfig.php b/app/code/Magento/Ups/Block/Backend/System/CarrierConfig.php index 15e782efd0757..77e8c2a4b10e2 100644 --- a/app/code/Magento/Ups/Block/Backend/System/CarrierConfig.php +++ b/app/code/Magento/Ups/Block/Backend/System/CarrierConfig.php @@ -7,8 +7,10 @@ use Magento\Backend\Block\Template; use Magento\Backend\Block\Template\Context as TemplateContext; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\Website; use Magento\Ups\Helper\Config as ConfigHelper; +use Magento\Framework\Json\Helper\Data as JsonHelper; /** * Backend shipping UPS content block @@ -35,15 +37,18 @@ class CarrierConfig extends Template * @param \Magento\Ups\Helper\Config $carrierConfig * @param \Magento\Store\Model\Website $websiteModel * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( TemplateContext $context, ConfigHelper $carrierConfig, Website $websiteModel, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->carrierConfig = $carrierConfig; $this->_websiteModel = $websiteModel; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index 57ad12a972dab..b6e539bdadcb9 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -1594,7 +1594,7 @@ private function generateShipmentDescription(array $items): string * * @param Element $shipmentConfirmResponse * @return DataObject - * @deprecated New asynchronous methods introduced. + * @deprecated 100.3.3 New asynchronous methods introduced. * @see requestToShipment */ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) @@ -1789,7 +1789,7 @@ private function requestShipments(array $quoteIds): array * * @param DataObject $request * @return DataObject - * @deprecated New asynchronous methods introduced. + * @deprecated 100.3.3 New asynchronous methods introduced. * @see requestToShipment */ protected function _doShipmentRequest(DataObject $request) diff --git a/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml b/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml index c4298cd8dc046..f068b0cf0079f 100644 --- a/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml +++ b/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $upsModel \Magento\Ups\Helper\Config */ /** @var $block \Magento\Ups\Block\Backend\System\CarrierConfig */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $upsCarrierConfig = $block->getCarrierConfig(); $orShipArr = $upsCarrierConfig->getCode('originShipment'); $defShipArr = $upsCarrierConfig->getCode('method'); @@ -15,6 +15,8 @@ $defShipArr = $upsCarrierConfig->getCode('method'); $sectionCode = $block->getRequest()->getParam('section'); $websiteCode = $block->getRequest()->getParam('website'); $storeCode = $block->getRequest()->getParam('store'); +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); if (!$storeCode && $websiteCode) { /** @var $web \Magento\Store\Model\Website */ @@ -35,7 +37,8 @@ if (!$storeCode && $websiteCode) { $storedUpsType = $block->escapeHtml($block->getConfig('carriers/ups/type')); } ?> -<script> + +<?php $scriptString = <<<script require(["prototype"], function(){ //<![CDATA[ @@ -85,14 +88,17 @@ require(["prototype"], function(){ 'carriers_ups_mode_xml','carriers_ups_include_taxes']; this.onlyUpsElements = ['carriers_ups_gateway_url']; - this.storedOriginShipment = '<?= /* @noEscape */ $storedOriginShipment ?>'; - this.storedFreeShipment = '<?= /* @noEscape */ $storedFreeShipment ?>'; - this.storedUpsType = '<?= /* @noEscape */ $storedUpsType ?>'; - <?php /** @var $jsonHelper \Magento\Framework\Json\Helper\Data */ $jsonHelper = $this->helper(\Magento\Framework\Json\Helper\Data::class); ?> - this.storedAllowedMethods = <?= /* @noEscape */ $jsonHelper->jsonEncode($storedAllowedMethods) ?>; - this.originShipmentObj = <?= /* @noEscape */ $jsonHelper->jsonEncode($orShipArr) ?>; - this.originShipmentObj['default'] = <?= /* @noEscape */ $jsonHelper->jsonEncode($defShipArr) ?>; +script; +$scriptString .= 'this.storedOriginShipment = \'' . /* @noEscape */ $storedOriginShipment . '\'; + this.storedFreeShipment = \'' . /* @noEscape */ $storedFreeShipment . '\'; + this.storedUpsType = \'' . /* @noEscape */ $storedUpsType . '\';'; +?> +<?php $scriptString .= 'this.storedAllowedMethods = ' . /* @noEscape */ $jsonHelper->jsonEncode($storedAllowedMethods) . + '; + this.originShipmentObj = ' . /* @noEscape */ $jsonHelper->jsonEncode($orShipArr) . '; + this.originShipmentObj[\'default\'] = ' . /* @noEscape */ $jsonHelper->jsonEncode($defShipArr) . ';'; +$scriptString .= <<<script this.setFormValues(); Event.observe($(this.carriersUpsTypeId), 'change', this.setFormValues.bind(this)); Event.observe($(this.carriersUpsActiveId), 'change', this.setFormValues.bind(this)); @@ -110,8 +116,13 @@ require(["prototype"], function(){ while (freeMethod.length > 0) { freeMethod.remove(0); } - freeMethod.insert(new Element('option', {value:''}).update('<?= $block->escapeHtml(__('None')) ?>')); +script; + +$scriptString .= 'freeMethod.insert(new Element(\'option\', {value:\'\'}).update(\'' . $block->escapeHtml(__('None')) . + '\'));'; + +$scriptString .= <<<script var code, option; for (code in originShipment) { option = new Element('option', {value:code}).update(originShipment[code]); @@ -145,7 +156,7 @@ require(["prototype"], function(){ setFormValues: function() { var a; - if ($F(this.carriersUpsTypeId) == 'UPS') { + if (\$F(this.carriersUpsTypeId) == 'UPS') { for (a = 0; a < this.checkingUpsXmlId.length; a++) { $(this.checkingUpsXmlId[a]).removeClassName('required-entry'); } @@ -173,13 +184,13 @@ require(["prototype"], function(){ }, changeOriginShipment: function(Event, key) { - this.originShipmentTitle = key ? key : $F('carriers_ups_origin_shipment'); + this.originShipmentTitle = key ? key : \$F('carriers_ups_origin_shipment'); this.updateAllowedMethods(this.originShipmentTitle); }, changeFieldsDisabledState: function (fields, key) { - $(fields[key]).disabled = $F(this.carriersUpsActiveId) !== '1' + $(fields[key]).disabled = \$F(this.carriersUpsActiveId) !== '1' || $(fields[key] + '_inherit') !== null - && $F(fields[key] + '_inherit') === '1'; + && \$F(fields[key] + '_inherit') === '1'; if ($(fields[key]).next() !== undefined) { $(fields[key]).removeClassName('mage-error').next().remove(); @@ -191,4 +202,6 @@ require(["prototype"], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/UrlRewrite/Block/GridContainer.php b/app/code/Magento/UrlRewrite/Block/GridContainer.php index 1a3a6e89fa854..066b76ed610e0 100644 --- a/app/code/Magento/UrlRewrite/Block/GridContainer.php +++ b/app/code/Magento/UrlRewrite/Block/GridContainer.php @@ -10,7 +10,7 @@ * Url rewrite grid container class * * @api - * @deprecated Moved to UI component implementation + * @deprecated 102.0.0 Moved to UI component implementation * @since 100.0.2 */ class GridContainer extends \Magento\Backend\Block\Widget\Grid\Container diff --git a/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php b/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php index 906ba1f625477..12620edf460d2 100644 --- a/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php +++ b/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php @@ -11,7 +11,7 @@ * Exception for already created url. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class UrlAlreadyExistsException extends \Magento\Framework\Exception\AlreadyExistsException { @@ -39,7 +39,7 @@ public function __construct(Phrase $phrase = null, \Exception $cause = null, $co * Get URLs * * @return array - * @since 100.2.0 + * @since 101.0.0 */ public function getUrls() { diff --git a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php index f187408d45a9d..9a62ed211d2b1 100644 --- a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php +++ b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php @@ -353,7 +353,7 @@ protected function insertMultiple($data): void * * @param UrlRewrite[] $urls * @return array - * @deprecated Not used anymore. + * @deprecated 101.0.3 Not used anymore. */ protected function createFilterDataBasedOnUrls($urls): array { diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminSearchUrlRewriteByRequestPathActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminSearchUrlRewriteByRequestPathActionGroup.xml new file mode 100644 index 0000000000000..dfdc840e0dc9f --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminSearchUrlRewriteByRequestPathActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSearchUrlRewriteByRequestPathActionGroup" extends="AdminSearchAndSelectUrlRewriteInGridActionGroup"> + <annotations> + <description>EXTENDS: SearchAndSelectUrlRewrite. Removes 'clickOnRowSelectButton' and 'clickOnEditButton'.</description> + </annotations> + <arguments> + <argument name="requestPath" type="string"/> + </arguments> + + <remove keyForRemoval="clickOnRowSelectButton"/> + <remove keyForRemoval="clickOnEditButton"/> + <remove keyForRemoval="waitForEditPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathInUrlRewriteGrigActionGroup.xml new file mode 100644 index 0000000000000..9de6045d70c03 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathInUrlRewriteGrigActionGroup.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="AssertAdminRequestPathInUrlRewriteGrigActionGroup"> + <annotations> + <description>Assert the requested path is shown in the URL Rewrite grid.</description> + </annotations> + <arguments> + <argument name="requestPath" type="string"/> + </arguments> + + <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', requestPath)}}" + stepKey="seeValueInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup.xml new file mode 100644 index 0000000000000..8aac6ae54582a --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup.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="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup"> + <annotations> + <description>Assert the requested path is not shown in the URL Rewrite grid.</description> + </annotations> + <arguments> + <argument name="requestPath" type="string"/> + </arguments> + + <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', requestPath)}}" + stepKey="valueIsNotShownInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminStoreValueIsSetForUrlRewriteActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminStoreValueIsSetForUrlRewriteActionGroup.xml new file mode 100644 index 0000000000000..dea0b8d19b428 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminStoreValueIsSetForUrlRewriteActionGroup.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="AssertAdminStoreValueIsSetForUrlRewriteActionGroup"> + <annotations> + <description>Verifies that the proper Store Value is used for URL Rewrite.</description> + </annotations> + <arguments> + <argument name="storeValue" type="string"/> + </arguments> + + <see selector="{{AdminUrlRewriteIndexSection.gridCellByColumnRowNumber('1', 'Store View')}}" + userInput="{{storeValue}}" stepKey="seeStoreValueForCategoryId"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml new file mode 100644 index 0000000000000..a409860811837 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.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="AssertAdminTargetPathInUrlRewriteGrigActionGroup"> + <annotations> + <description>Assert the target path is shown in the URL Rewrite grid.</description> + </annotations> + <arguments> + <argument name="targetPath" type="string"/> + </arguments> + + <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', targetPath)}}" + stepKey="seeValueInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup.xml new file mode 100644 index 0000000000000..739675ba264ea --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup.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="AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup"> + <annotations> + <description>Assert the target path is not shown in the URL Rewrite grid.</description> + </annotations> + <arguments> + <argument name="targetPath" type="string"/> + </arguments> + + <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', targetPath)}}" + stepKey="valueIsNotShownInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertStorefrontUrlRewriteSuccessOutsideRedirectActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertStorefrontUrlRewriteSuccessOutsideRedirectActionGroup.xml new file mode 100644 index 0000000000000..757c15775dd66 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertStorefrontUrlRewriteSuccessOutsideRedirectActionGroup.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="AssertStorefrontUrlRewriteSuccessOutsideRedirectActionGroup"> + <annotations> + <description>Assert redirect to proper URL on the Storefront.</description> + </annotations> + <arguments> + <argument name="target_path" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{target_path}}" stepKey="seePropertUrlRewrite"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml index 4e46ed8e4fc79..3b140aed5f572 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml @@ -47,84 +47,103 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersIfSet"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> + <!--Change category name and URL key for EN Store View--> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchToStoreViewEn"> <argument name="Store" value="customStoreENNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-english" stepKey="changeNameField"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForENStoreView"> + <argument name="categoryName" value="categoryEN"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyENStoreView"> <argument name="value" value="category-english"/> </actionGroup> + + <!--Change category name and URL key for NL Store View--> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchToStoreViewNl"> <argument name="Store" value="customStoreNLNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValue1"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-dutch" stepKey="changeNameFieldNLStoreView"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader2"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForNLStoreView"> + <argument name="categoryName" value="categoryNL"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyNLStoreView"> <argument name="value" value="category-dutch"/> </actionGroup> - <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="navigateToSystemImport"/> - <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> - <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> - <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> - <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> - <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> - <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> - <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> - <argument name="productName" value="productformagetwo68980"/> - </actionGroup> - <click selector="{{AdminProductGridSection.productRowBySku('productformagetwo68980')}}" stepKey="clickOnProductRow"/> + + <!-- Import products with add/update behavior --> + <actionGroup ref="AdminImportProductsWithCheckValidationResultActionGroup" stepKey="importProduct"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="import_updated.csv"/> + <argument name="importNoticeMessage" value="Created: 1, Updated: 0, Deleted: 0"/> + <argument name="validationNoticeMessage" value="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="productformagetwo68980"/> + </actionGroup> <grabFromCurrentUrl regex="~/id/(\d+)/~" stepKey="grabProductIdFromUrl"/> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="goToUrlRewritesIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-english.html" stepKey="inputCategoryUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english.html')}}" stepKey="seeUrlInRequestPathColumn"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn"/> + <!--Open Marketing - SEO & Search - URL Rewrites--> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-dutch.html" stepKey="inputCategoryUrlForNLStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch.html')}}" stepKey="seeUrlInRequestPathColumn1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn1"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters2"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrlRewriteForENStoreView"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters3"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView1"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters3"/> - <waitForPageLoad stepKey="waitForPageToLoad3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn3"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrlRewriteForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters4"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-english/productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView2"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters4"/> - <waitForPageLoad stepKey="waitForPageToLoad4"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english/productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn4"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn4"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteForENStoreView"> + <argument name="requestPath" value="category-english/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="category-english/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters5"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-dutch/productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView3"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters5"/> - <waitForPageLoad stepKey="waitForPageToLoad5"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch/productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn5"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn5"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteForNLStoreView"> + <argument name="requestPath" value="category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml index 1d604ef7648dc..6f7bb6ccb2b84 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml @@ -22,7 +22,9 @@ <comment userInput="Enable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentEnableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableConfig"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="ApiCategory" stepKey="createCategory"> <field key="name">category-admin</field> </createData> @@ -39,7 +41,9 @@ <comment userInput="Disable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentDisableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisableConfig"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -58,15 +62,17 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache2"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchToStoreViewEn"> <argument name="Store" value="customStoreENNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-english" stepKey="changeNameField"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForENStoreView"> + <argument name="categoryName" value="categoryenglish"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyENStoreView"> <argument name="value" value="category-english"/> </actionGroup> @@ -74,82 +80,114 @@ <argument name="Store" value="customStoreNLNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValue1"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-dutch" stepKey="changeNameFieldNLStoreView"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader2"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForNLStoreView"> + <argument name="categoryName" value="categorydutch"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyNLStoreView"> <argument name="value" value="category-dutch"/> </actionGroup> - <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="navigateToSystemImport"/> - <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> - <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> - <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> - <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> - <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> - <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> - <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> - <argument name="productName" value="productformagetwo68980"/> - </actionGroup> - <click selector="{{AdminProductGridSection.productRowBySku('productformagetwo68980')}}" stepKey="clickOnProductRow"/> + + <!-- Import products with add/update behavior --> + <actionGroup ref="AdminImportProductsWithCheckValidationResultActionGroup" stepKey="importProduct"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="import_updated.csv"/> + <argument name="importNoticeMessage" value="Created: 1, Updated: 0, Deleted: 0"/> + <argument name="validationNoticeMessage" value="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="productformagetwo68980"/> + </actionGroup> <grabFromCurrentUrl regex="~/id/(\d+)/~" stepKey="grabProductIdFromUrl"/> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="goToUrlRewritesIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-english.html" stepKey="inputCategoryUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english.html')}}" stepKey="seeUrlInRequestPathColumn"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn"/> + <!-- Open Marketing - SEO & Search - URL Rewrites --> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-dutch.html" stepKey="inputCategoryUrlForNLStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch.html')}}" stepKey="seeUrlInRequestPathColumn1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn1"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters2"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn2"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english/productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn4"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn4"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrl"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeRequestPathForProduct"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeTargetPathForProduct"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeRequestPathForENStoreView"> + <argument name="requestPath" value="category-english/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeTargetPathForENStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters3"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView1"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters3"/> - <waitForPageLoad stepKey="waitForPageToLoad3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn3"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch/productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn5"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn5"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrlForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeRequestPathForProductForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeTargetPathForProductForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeRequestPathForNLStoreView"> + <argument name="requestPath" value="category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeTargetPathForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> <!-- Switch StoreView --> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnStoreFrontHomePage"/> <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"> <argument name="storeView" value="customStoreENNotUnique"/> </actionGroup> - <amOnPage url="/productformagetwo68980-english.html" stepKey="navigateToProductPage"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-english" stepKey="seeProductName"/> - <amOnPage url="/category-english/productformagetwo68980-english.html" stepKey="navigateToProductPage2"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-english" stepKey="seeProductName2"/> + <!-- Assert Redirects work and Product is present on StoreFront--> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPage"> + <argument name="productName" value="productformagetwo68980-english"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageSecondAttempt"> + <argument name="productName" value="productformagetwo68980-english"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/category-english/productformagetwo68980-english.html"/> + </actionGroup> <!-- Switch StoreView --> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page2"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="backToHomePage"/> <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView2"> <argument name="storeView" value="customStoreNLNotUnique"/> </actionGroup> - <amOnPage url="/productformagetwo68980-dutch.html" stepKey="navigateToProductPage3"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-dutch" stepKey="seeProductName3"/> - <amOnPage url="/category-dutch/productformagetwo68980-dutch.html" stepKey="navigateToProductPage4"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-dutch" stepKey="seeProductName4"/> + <!-- Assert Redirects work and Product is present on StoreFront--> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageThirdAttempt"> + <argument name="productName" value="productformagetwo68980-dutch"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageFourthAttempt"> + <argument name="productName" value="productformagetwo68980-dutch"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategory2Test.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategory2Test.xml index 20e6392091998..fee13adcb433c 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategory2Test.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategory2Test.xml @@ -35,7 +35,9 @@ <!--Create additional Store View in Main Website Store --> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"/> - <magentoCLI command="indexer:reindex" stepKey="reindexAll"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml index 036d35d9c3258..c14d0b175d2c0 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml @@ -63,26 +63,26 @@ </actionGroup> <!-- Create simple product with categories created in create data --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductsGrid"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="$$createProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowOfCreatedSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> - <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$rootCategory.name$$" stepKey="fillSearchForInitialCategory"/> - <waitForPageLoad stepKey="waitForCategory1"/> - <click selector="{{AdminProductFormSection.selectCategory($$rootCategory.name$$)}}" stepKey="unselectInitialCategory"/> - <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$category.name$$" stepKey="fillSearchCategory"/> - <waitForPageLoad stepKey="waitForCategory2"/> - <click selector="{{AdminProductFormSection.selectCategory($$category.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> - <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSaved"/> + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProduct"/> + <actionGroup ref="SetCategoryByNameActionGroup" stepKey="unselectInitialCategory"> + <argument name="categoryName" value="$$rootCategory.name$$"/> + </actionGroup> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="pressDoneButton"/> + <actionGroup ref="SetCategoryByNameActionGroup" stepKey="setNewCategory"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneButton"/> + <actionGroup ref="SaveProductFormNoSuccessCheckActionGroup" stepKey="saveProduct"/> + <!-- Verify customer see success message --> - <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="seeAssertSimpleProductSaveSuccessMessage"> + <argument name="message" value="You saved the product."/> + </actionGroup> <!-- Grab category Id --> <actionGroup ref="OpenCategoryFromCategoryTreeActionGroup" stepKey="grabCategoryId"> @@ -95,8 +95,13 @@ <argument name="redirectType" value="No"/> <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> </actionGroup> - <see selector="{{AdminUrlRewriteIndexSection.gridCellByColumnRowNumber('1', 'Store View')}}" userInput="{{customStoreGroup.name}}" stepKey="seeStoreValueForCategoryId"/> - <see selector="{{AdminUrlRewriteIndexSection.gridCellByColumnRowNumber('1', 'Store View')}}" userInput="{{customStoreEN.name}}" stepKey="seeStoreViewValueForCategoryId"/> + + <actionGroup ref="AssertAdminStoreValueIsSetForUrlRewriteActionGroup" stepKey="seeStoreValueForCategoryId"> + <argument name="storeValue" value="{{customStoreGroup.name}}"/> + </actionGroup> + <actionGroup ref="AssertAdminStoreValueIsSetForUrlRewriteActionGroup" stepKey="seeStoreViewValueForCategoryId"> + <argument name="storeValue" value="{{customStoreEN.name}}"/> + </actionGroup> <!-- Grab product Id --> <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="grabProductId"> @@ -109,7 +114,12 @@ <argument name="redirectType" value="No"/> <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> </actionGroup> - <see selector="{{AdminUrlRewriteIndexSection.gridCellByColumnRowNumber('1', 'Store View')}}" userInput="{{customStore.name}}" stepKey="seeStoreValueForProductId"/> - <see selector="{{AdminUrlRewriteIndexSection.gridCellByColumnRowNumber('1', 'Store View')}}" userInput="{{storeViewData.name}}" stepKey="seeStoreViewValueForProductId"/> + + <actionGroup ref="AssertAdminStoreValueIsSetForUrlRewriteActionGroup" stepKey="seeStoreValueForProductId"> + <argument name="storeValue" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="AssertAdminStoreValueIsSetForUrlRewriteActionGroup" stepKey="seeStoreViewValueForProductId"> + <argument name="storeValue" value="{{storeViewData.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml index 9d6b267055f70..10b377eebd313 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml @@ -22,7 +22,9 @@ <comment userInput="Enable config to generate category/product URL Rewrites" stepKey="commentEnableConfig" /> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -38,40 +40,41 @@ <comment userInput="Enable config to generate category/product URL Rewrites" stepKey="commentEnableConfig" /> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- 1. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewrite"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInGrid"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInGrid"> + <argument name="requestPath" value="$createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> <!-- 2. Set the configuration for Generate "category/product" URL Rewrites to No--> - <amOnPage url="{{CatalogConfigPage.url}}" stepKey="amOnCatalogConfigPage"/> - <conditionalClick selector="{{CatalogSection.seo}}" dependentSelector="{{CatalogSection.CheckIfSeoTabExpand}}" visible="true" stepKey="expandSeoTab" /> - <waitForElementVisible selector="{{CatalogSection.GenerateUrlRewrites}}" stepKey="GenerateUrlRewritesSelect"/> - <selectOption userInput="0" selector="{{CatalogSection.GenerateUrlRewrites}}" stepKey="selectUrlGenerationNo" /> - <waitForElementVisible selector="{{GenerateUrlRewritesConfirm.title}}" stepKey="waitForConfirmModal"/> - <click selector="{{GenerateUrlRewritesConfirm.ok}}" stepKey="confirmSwitchingGenerationOff"/> - <click selector="{{CatalogSection.save}}" stepKey="saveConfig" /> - <waitForPageLoad stepKey="waitForSavingSystemConfiguration"/> + <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!-- 3. Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- 4. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage2"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName2"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue1"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="dontSeeValue2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteAfterDisablingTheConfig"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInGridAfterDisablingTheConfig"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="categoryUrlIsNotShownAfterDisablingTheConfig"> + <argument name="requestPath" value="$createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml index f03d9ae1bad67..d102286bd9e6d 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml @@ -31,7 +31,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCLI command="indexer:reindex" stepKey="runReindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runReindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml index 9b739b157cddc..16916426167b8 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml @@ -51,8 +51,11 @@ </actionGroup> <!--AssertUrlRewriteSuccessOutsideRedirect--> - <amOnPage url="{{StorefrontHomePage.url}}{{customPermanentUrlRewrite.request_path}}" stepKey="amOnStorefrontPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <seeInCurrentUrl url="{{customPermanentUrlRewrite.target_path}}" stepKey="seeAssertUrlRewrite"/> + <actionGroup ref="NavigateToStorefrontForCreatedPageActionGroup" stepKey="navigateToTheStoreFront"> + <argument name="page" value="{{customPermanentUrlRewrite.request_path}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontUrlRewriteSuccessOutsideRedirectActionGroup" stepKey="seeAssertUrlRewrite"> + <argument name="target_path" value="{{customPermanentUrlRewrite.target_path}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml index a70065dc1d307..2dd7df9cbb548 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml @@ -43,7 +43,7 @@ <deleteData createDataKey="simpleSubCategory1" stepKey="deleteSimpleSubCategory1"/> <comment userInput="Disable config to generate category/product URL Rewrites " stepKey="commentDisableConfig" /> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="disableGenerateUrlRewrite"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <comment userInput="1. Log in to Admin " stepKey="commentAdminLogin" /> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTest.xml index cce0cd11e0199..e98d1b3f526c5 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTest.xml @@ -39,47 +39,60 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="simpleSubCategory1" stepKey="deletesimpleSubCategory1"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Steps --> <!-- 1. Log in to Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- 2. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue4"/> - + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewrite"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueOne"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueTwo"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueThree"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueFour"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> <!-- 3. Edit Category 1 for DEFAULT Store View: --> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchStoreView"> <argument name="Store" value="_defaultStore.name"/> <argument name="CatName" value="$$simpleSubCategory1.name$$"/> </actionGroup> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection2"/> - <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyDefaultValueCheckbox}}" stepKey="uncheckRedirect2"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="$simpleSubCategory1.custom_attributes[url_key]$-new" stepKey="changeURLKey"/> - <checkOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="checkUrlKeyRedirect"/> - <!-- 4. Save Category 1 --> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaved"/> - + <!-- 4. Change URL key for category and save changes --> + <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeFirstCategoryUrlKey"> + <argument name="value" value="$simpleSubCategory1.custom_attributes[url_key]$new"/> + </actionGroup> <!-- 5. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage2"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName2"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue4"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue5"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue6"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue7"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteSecondTime"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValueOne"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValueTwo"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValuethree"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValueFour"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValueFive"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$new/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValue1Six"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValueSeven"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewTest.xml index 1876b001eb5bc..de44200994873 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewTest.xml @@ -28,9 +28,13 @@ </before> <remove keyForRemoval="switchStoreView"/> <!-- 3. Edit Category 1 for All store view: --> - <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="goToCategoryPage" after="seeValue4"> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="goToCategoryPage" after="seeValueFour"> <argument name="Category" value="$$simpleSubCategory1$$"/> </actionGroup> - <remove keyForRemoval="uncheckRedirect2"/> + <remove keyForRemoval="changeFirstCategoryUrlKey"/> + <!-- 4. Change URL key for category and save changes --> + <actionGroup ref="ChangeSeoUrlKeyActionGroup" stepKey="changeCategoryUrlKey" after="goToCategoryPage"> + <argument name="value" value="$simpleSubCategory1.custom_attributes[url_key]$new"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml index 14f7c9fb7cbe3..bc2005b32bae2 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml @@ -28,9 +28,13 @@ </before> <remove keyForRemoval="switchStoreView"/> <!-- 3. Edit Category 1 for All store view: --> - <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="goToCategoryPage" after="seeValue4"> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="goToCategoryPage" after="doNotSeeValueFour"> <argument name="Category" value="$$simpleSubCategory1$$"/> </actionGroup> <remove keyForRemoval="uncheckRedirect2"/> + <!-- 4. Change URL key for category and save changes --> + <actionGroup ref="ChangeSeoUrlKeyActionGroup" stepKey="changeCategoryUrlKey" after="goToCategoryPage"> + <argument name="value" value="$simpleSubCategory1.custom_attributes[url_key]$new"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml index 639cd2c57f7d1..78bd397c69289 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml @@ -25,7 +25,9 @@ <comment userInput="Enable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentEnableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableConfig"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="SimpleSubCategory" stepKey="simpleSubCategory1"/> <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory2"> @@ -42,72 +44,113 @@ <comment userInput="Disable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentDisableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="simpleSubCategory1" stepKey="deletesimpleSubCategory1"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache2"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisableConfig"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Steps --> <!-- 1. Log in to Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- 2. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue1"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue3"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue4"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewrite"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueOne"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueTwo"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueThree"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueFour"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> <!-- 3. Edit Category 1 for DEFAULT Store View: --> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchStoreView"> <argument name="Store" value="_defaultStore.name"/> <argument name="CatName" value="$$simpleSubCategory1.name$$"/> </actionGroup> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection2"/> - <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyDefaultValueCheckbox}}" stepKey="uncheckRedirect2"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="$simpleSubCategory1.custom_attributes[url_key]$-new" stepKey="changeURLKey"/> - <checkOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="checkUrlKeyRedirect"/> - <!-- 4. Save Category 1 --> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaved"/> - <!-- 5. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage2"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName1"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue1"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue2"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue3"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue4"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue5"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue6"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue7"/> - - <amOnPage url="/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName"/> + <!-- 4. Change URL key for category and save changes --> + <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeCategoryUrlKey"> + <argument name="value" value="$simpleSubCategory1.custom_attributes[url_key]$new"/> + </actionGroup> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage2"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName2"/> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage3"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName3"/> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage4"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName4"/> + <!-- 5. Open Marketing - SEO & Search - URL Rewrites --> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteOneMoreTime"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueInGrid"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueTwoInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueThreeInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueFourInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueFiveInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$-new/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueSixInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueSevenInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$-new/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage5"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName5"/> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage6"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName6"/> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage7"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName7"/> + <!-- 6. Assert Redirects work and Product is present on StoreFront--> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageSecondAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageThirdAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageFourthAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageFifthAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$new/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageSixthAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageSeventhAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml index 1d460b9b668a0..bfe8a28064496 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml @@ -22,7 +22,9 @@ <comment userInput="Enable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentEnableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableConfig"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="SimpleSubCategory" stepKey="simpleSubCategory1"/> <!-- Create Simple product 1 and assign it to Category 1 --> <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> @@ -32,26 +34,39 @@ <comment userInput="Disable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentDisableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisableConfig"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="simpleSubCategory1" stepKey="deletesimpleSubCategory1"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache2"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> + <!-- 1. Log in to Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- 2. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeProducturl"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="dontSeeCategoryProducturlKey"/> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewrite"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInGrid"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="categoryProductUrlIsNotShown"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + + <!-- 3. Assert the Redirect works and Product is present on StoreFront--> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/view/adminhtml/templates/selector.phtml b/app/code/Magento/UrlRewrite/view/adminhtml/templates/selector.phtml index 84abf64af9757..837c528d6cfda 100644 --- a/app/code/Magento/UrlRewrite/view/adminhtml/templates/selector.phtml +++ b/app/code/Magento/UrlRewrite/view/adminhtml/templates/selector.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var \Magento\UrlRewrite\Block\Selector $block */ +/** + * @var \Magento\UrlRewrite\Block\Selector $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="form-inline"> <fieldset class="admin__fieldset fieldset" data-container-for="entity-type-selector"> @@ -13,11 +16,18 @@ <span><?= $block->escapeHtml($block->getSelectorLabel()) ?></span> </label> <div class="admin__field-control control"> - <select data-role="entity-type-selector" class="admin__control-select select" onchange="window.location = this.value;" id="entity-type-selector"> - <?php foreach ($block->getModes() as $mode => $label) : ?> - <option <?= /* @noEscape */ $block->isMode($mode) ? 'selected="selected" ' : '' ?>value="<?= $block->escapeUrl($block->getModeUrl($mode)) ?>"><?= $block->escapeHtml($label) ?></option> + <select data-role="entity-type-selector" class="admin__control-select select" id="entity-type-selector"> + <?php foreach ($block->getModes() as $mode => $label): ?> + <option <?= /* @noEscape */ $block->isMode($mode) ? 'selected="selected" ' : '' ?> + value="<?= $block->escapeUrl($block->getModeUrl($mode)) ?>"><?= $block->escapeHtml($label) ?> + </option> <?php endforeach; ?> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'window.location = this.value;', + 'select#entity-type-selector' + ) ?> </div> </div> </fieldset> diff --git a/app/code/Magento/User/Block/User/Edit.php b/app/code/Magento/User/Block/User/Edit.php index 6e036cf20fa25..233fe1e0cfee5 100644 --- a/app/code/Magento/User/Block/User/Edit.php +++ b/app/code/Magento/User/Block/User/Edit.php @@ -87,7 +87,7 @@ protected function _construct() * - click "Delete User" at top left part of the page; * * @return \Magento\Framework\Phrase - * @since 100.2.0 + * @since 101.0.0 */ public function getDeleteMessage() { @@ -100,7 +100,7 @@ public function getDeleteMessage() * Magento\User\Controller\Adminhtml\User\Delete * * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getDeleteUrl() { @@ -113,7 +113,7 @@ public function getDeleteUrl() * to create a new user account OR to edit the previously created user account * * @return int - * @since 100.2.0 + * @since 101.0.0 */ public function getObjectId() { diff --git a/app/code/Magento/User/Model/Notificator.php b/app/code/Magento/User/Model/Notificator.php index 3a5522db4c533..3e36cd1387e39 100644 --- a/app/code/Magento/User/Model/Notificator.php +++ b/app/code/Magento/User/Model/Notificator.php @@ -107,6 +107,7 @@ public function sendForgotPassword(UserInterface $user): void $this->sendNotification( 'admin/emails/forgot_email_template', [ + 'username' => $user->getFirstName().' '.$user->getLastName(), 'user' => $user, 'store' => $this->storeManager->getStore( Store::DEFAULT_STORE_ID diff --git a/app/code/Magento/User/Model/ResourceModel/User/Collection.php b/app/code/Magento/User/Model/ResourceModel/User/Collection.php index 7683adae84365..422afb47f0a0e 100644 --- a/app/code/Magento/User/Model/ResourceModel/User/Collection.php +++ b/app/code/Magento/User/Model/ResourceModel/User/Collection.php @@ -27,6 +27,7 @@ protected function _construct() * Collection Init Select * * @return $this + * @since 101.1.0 */ protected function _initSelect() { diff --git a/app/code/Magento/User/Model/User.php b/app/code/Magento/User/Model/User.php index 00d2aa140a991..61af14d943615 100644 --- a/app/code/Magento/User/Model/User.php +++ b/app/code/Magento/User/Model/User.php @@ -120,12 +120,12 @@ class User extends AbstractModel implements StorageInterface, UserInterface protected $_encryptor; /** - * @deprecated + * @deprecated 101.1.0 */ protected $_transportBuilder; /** - * @deprecated + * @deprecated 101.1.0 */ protected $_storeManager; @@ -145,7 +145,7 @@ class User extends AbstractModel implements StorageInterface, UserInterface private $notificator; /** - * @deprecated + * @deprecated 101.1.0 */ private $deploymentConfig; @@ -451,7 +451,7 @@ public function roleUserExists() * * @return $this * @throws NotificationExceptionInterface - * @deprecated + * @deprecated 101.1.0 * @see NotificatorInterface::sendForgotPassword() */ public function sendPasswordResetConfirmationEmail() @@ -529,7 +529,7 @@ protected function createChangesDescriptionString() * @throws NotificationExceptionInterface * @return $this * @since 100.1.0 - * @deprecated + * @deprecated 101.1.0 * @see NotificatorInterface::sendUpdated() * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml index 8abb4e9224b0a..139d6b2a0f49c 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml @@ -29,9 +29,10 @@ <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterCurrentPassword"/> <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminEditUserSection.userRoleTab}}" stepKey="clickUserRole"/> + <waitForPageLoad stepKey="waitForAdminUserRoleTabLoad"/> <fillField selector="{{AdminEditUserSection.roleNameFilterTextField}}" userInput="{{role.name}}" stepKey="filterRole"/> <click selector="{{AdminEditUserSection.searchButton}}" stepKey="clickSearch"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear1"/> + <waitForPageLoad stepKey="waitForLoadingMaskToDisappear1"/> <click selector="{{AdminEditUserSection.searchResultFirstRow}}" stepKey="selectRole"/> <click selector="{{AdminEditUserSection.saveButton}}" stepKey="clickSaveUser"/> <waitForPageLoad stepKey="waitForPageLoad2"/> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml index 2957e6953dad8..46ad2e228c6c1 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml @@ -10,15 +10,32 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminDeleteRoleActionGroup"> <annotations> - <description>Deletes a User Role that contains the text 'Role'. PLEASE NOTE: The Action Group values are Hardcoded.</description> + <description>Deletes a User Role.</description> </annotations> + <arguments> + <argument name="role" defaultValue=""/> + </arguments> - <click stepKey="clickOnRole" selector="{{AdminDeleteRoleSection.theRole}}"/> + <click stepKey="clickResetFilterButtonBefore" selector="{{AdminRoleGridSection.resetButton}}"/> + <waitForPageLoad stepKey="waitForRolesGridFilterResetBefore" time="10"/> + <fillField stepKey="TypeRoleFilter" selector="{{AdminRoleGridSection.roleNameFilterTextField}}" userInput="{{role.name}}"/> + <waitForElementVisible stepKey="waitForFilterSearchButtonBefore" selector="{{AdminRoleGridSection.searchButton}}" time="10"/> + <click stepKey="clickFilterSearchButton" selector="{{AdminRoleGridSection.searchButton}}"/> + <waitForPageLoad stepKey="waitForUserRoleFilter" time="10"/> + <waitForElementVisible stepKey="waitForRoleInRoleGrid" selector="{{AdminDeleteRoleSection.role(role.name)}}" time="10"/> + <click stepKey="clickOnRole" selector="{{AdminDeleteRoleSection.role(role.name)}}"/> + <waitForPageLoad stepKey="waitForRolePageToLoad" time="10"/> <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteRoleSection.current_pass}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <waitForElementVisible stepKey="waitForDeleteRoleButton" selector="{{AdminDeleteRoleSection.delete}}" time="10"/> <click stepKey="clickToDeleteRole" selector="{{AdminDeleteRoleSection.delete}}"/> - <waitForAjaxLoad stepKey="waitForDeleteConfirmationPopup" time="5"/> + <waitForPageLoad stepKey="waitForDeleteConfirmationPopup" time="5"/> + <waitForElementVisible stepKey="waitForConfirmButton" selector="{{AdminDeleteRoleSection.confirm}}" time="10"/> <click stepKey="clickToConfirm" selector="{{AdminDeleteRoleSection.confirm}}"/> <waitForPageLoad stepKey="waitForPageLoad" time="10"/> <see stepKey="seeSuccessMessage" userInput="You deleted the role."/> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <waitForElementVisible stepKey="waitForResetFilterButtonAfter" selector="{{AdminRoleGridSection.resetButton}}" time="10"/> + <click stepKey="clickResetFilterButtonAfter" selector="{{AdminRoleGridSection.resetButton}}"/> + <waitForPageLoad stepKey="waitForRolesGridFilterResetAfter" time="10"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml similarity index 83% rename from app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml rename to app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml index 4049e60e83455..d41ed63678783 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml @@ -5,10 +5,8 @@ * See COPYING.txt for license details. */ --> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <!--Login New User--> - <actionGroup name="LoginNewUser"> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="LoginNewUserActionGroup" deprecated="Use AdminLoginActionGroup instead"> <annotations> <description>Goes to the Backend Admin Login page. Fill Username and Password. Click on Sign In.</description> </annotations> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml index a3a82f6ce38e0..6a0d0c9210396 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml @@ -11,6 +11,7 @@ <section name="AdminDeleteRoleSection"> <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> <element name="salesRole" selector="//td[contains(text(), 'Sales')]" type="button"/> + <element name="role" parameterized="true" selector="//td[contains(@class,'col-role_name') and contains(text(), '{{roleName}}')]" type="button"/> <element name="current_pass" type="button" selector="#current_password"/> <element name="delete" selector="//button/span[contains(text(), 'Delete Role')]" type="button"/> <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml index 668ae550f1b3d..ba8d6ef433e13 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml @@ -32,7 +32,7 @@ <argument name="user" value="activeAdmin"/> <argument name="role" value="roleDefaultAdministrator"/> </actionGroup> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutMasterAdmin"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutMainAdmin"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToNewAdmin"> <argument name="username" value="{{activeAdmin.username}}"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml index 23a30246bd999..c26821d5be4b2 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml @@ -20,7 +20,7 @@ <group value="mtf_migrated"/> </annotations> - <actionGroup ref="AdminLoginActionGroup" stepKey="adminMasterLogin"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminMainLogin"/> <actionGroup ref="AdminCreateUserWithRoleAndIsActiveActionGroup" stepKey="createAdminUser"> <argument name="user" value="inactiveAdmin"/> <argument name="role" value="roleDefaultAdministrator"/> @@ -29,7 +29,7 @@ <actionGroup ref="AssertAdminUserIsInGridActionGroup" stepKey="assertAdminIsInGrid"> <argument name="user" value="inactiveAdmin"/> </actionGroup> - <actionGroup ref="AdminLogoutActionGroup" stepKey="adminMasterLogout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminMainLogout"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminNewLogin"> <argument name="username" value="{{inactiveAdmin.username}}"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml index 850fa04549e84..6750f21311d3a 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml @@ -22,13 +22,17 @@ <before> <magentoCLI command="config:set admin/captcha/enable 0" stepKey="disableAdminCaptcha"/> <magentoCLI command="config:set admin/security/lockout_failures 2" stepKey="setDefaultMaximumLoginFailures"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches1"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches1"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> </before> <after> <magentoCLI command="config:set admin/captcha/enable 1" stepKey="enableAdminCaptcha"/> <magentoCLI command="config:set admin/security/lockout_failures 6" stepKey="setDefaultMaximumLoginFailures"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserTest.xml index 501e9520c6367..0943b33e8a711 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserTest.xml @@ -18,8 +18,9 @@ <description value="Change full access role for admin user to custom one with restricted permission (Sales)"/> <group value="user"/> <group value="mtf_migrated"/> - <!-- skip due to MQE-1964 --> - <group value="skip"/> + <skip> + <issueId value="MQE-1964"/> + </skip> </annotations> <before> diff --git a/app/code/Magento/User/composer.json b/app/code/Magento/User/composer.json index 8ac1677bdfe81..6ba4be749cc7c 100644 --- a/app/code/Magento/User/composer.json +++ b/app/code/Magento/User/composer.json @@ -12,7 +12,8 @@ "magento/module-email": "*", "magento/module-integration": "*", "magento/module-security": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/User/etc/webapi_rest/di.xml b/app/code/Magento/User/etc/webapi_rest/di.xml index 7c6cccb454df7..930e505648d9c 100644 --- a/app/code/Magento/User/etc/webapi_rest/di.xml +++ b/app/code/Magento/User/etc/webapi_rest/di.xml @@ -10,7 +10,7 @@ <arguments> <argument name="userContexts" xsi:type="array"> <item name="adminSessionUserContext" xsi:type="array"> - <item name="type" xsi:type="object">Magento\User\Model\Authorization\AdminSessionUserContext</item> + <item name="type" xsi:type="object">Magento\User\Model\Authorization\AdminSessionUserContext\Proxy</item> <item name="sortOrder" xsi:type="string">30</item> </item> </argument> diff --git a/app/code/Magento/User/i18n/en_US.csv b/app/code/Magento/User/i18n/en_US.csv index 064b6428387fe..cd550015401d0 100644 --- a/app/code/Magento/User/i18n/en_US.csv +++ b/app/code/Magento/User/i18n/en_US.csv @@ -106,8 +106,8 @@ username,username Custom,Custom All,All Resources,Resources -"Warning!\r\nThis action will remove this user from already assigned role\r\nAre you sure?","Warning!\r\nThis action will remove this user from already assigned role\r\nAre you sure?" -"Warning!\r\nThis action will remove those users from already assigned roles\r\nAre you sure?","Warning!\r\nThis action will remove those users from already assigned roles\r\nAre you sure?" +"Warning!<br>This action will remove this user from already assigned role.<br>Are you sure?","Warning!<br>This action will remove this user from already assigned role.<br>Are you sure?" +"Warning!<br>This action will remove those users from already assigned roles.<br>Are you sure?","Warning!<br>This action will remove those users from already assigned roles.<br>Are you sure?" "Password Reset Confirmation for %name","Password Reset Confirmation for %name" "%name,","%name," "There was recently a request to change the password for your account.","There was recently a request to change the password for your account." diff --git a/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html b/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html index dacfa640464a3..42240bff3b8db 100644 --- a/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html +++ b/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html @@ -4,16 +4,17 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Password Reset Confirmation for %name" name=$user.name}} @--> +<!--@subject {{trans "Password Reset Confirmation for %name" name=$username}} @--> <!--@vars { "var store.frontend_name":"Store Name", "var user.id":"Account Holder Id", "var user.rp_token":"Reset Password Token", "var user.name":"Account Holder Name", -"store url=\"admin\/auth\/resetpassword\/\" _query_id=$user.id _query_token=$user.rp_token":"Reset Password URL" +"store url=\"admin\/auth\/resetpassword\/\" _query_id=$user.id _query_token=$user.rp_token":"Reset Password URL", +"var username":"Account Holder Name" } @--> -{{trans "%name," name=$user.name}} +{{trans "%name," name=$username}} {{trans "There was recently a request to change the password for your account."}} diff --git a/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml b/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml index 97308204be854..84567a81660f2 100644 --- a/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml +++ b/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var $block \Magento\User\Block\Role\Tab\Edit */ +/** + * @var $block \Magento\User\Block\Role\Tab\Edit + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?= $block->getChildHtml() ?> @@ -18,8 +21,7 @@ <label class="label" for="all"><span><?= $block->escapeHtml(__('Resource Access')) ?></span></label> <div class="control"> - <select id="all" name="all" - onchange="jQuery('[data-role=tree-resources-container]').toggle()" class="select"> + <select id="all" name="all" class="select"> <option value="0" <?= ($block->isEverythingAllowed() ? '' : 'selected="selected"') ?>> <?= $block->escapeHtml(__('Custom')) ?> </option> @@ -27,11 +29,16 @@ <?= $block->escapeHtml(__('All')) ?> </option> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + "jQuery('[data-role=tree-resources-container]').toggle()", + 'select#all' + ) ?> </div> </div> <div class="field - <?php if ($block->isEverythingAllowed()) :?> + <?php if ($block->isEverythingAllowed()):?> no-display <?php endif ?>" data-role="tree-resources-container"> diff --git a/app/code/Magento/User/view/adminhtml/templates/role/info.phtml b/app/code/Magento/User/view/adminhtml/templates/role/info.phtml index 6cf1bb373541d..f6375b17086f9 100644 --- a/app/code/Magento/User/view/adminhtml/templates/role/info.phtml +++ b/app/code/Magento/User/view/adminhtml/templates/role/info.phtml @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form action="<?= $block->escapeUrl($block->getUrl('*/*/saverole')) ?>" method="post" id="role-edit-form"> <?= $block->getBlockHtml('formkey') ?> </form> -<script> +<?php $scriptString = <<<script + require([ "jquery", "mage/mage" @@ -18,4 +21,7 @@ require([ }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/User/view/adminhtml/templates/role/users_grid_js.phtml b/app/code/Magento/User/view/adminhtml/templates/role/users_grid_js.phtml index a3b5dc68050ac..b0107a53593d3 100644 --- a/app/code/Magento/User/view/adminhtml/templates/role/users_grid_js.phtml +++ b/app/code/Magento/User/view/adminhtml/templates/role/users_grid_js.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script + require([ 'jquery', 'Magento_Ui/js/modal/confirm', @@ -12,9 +15,14 @@ require([ 'mage/adminhtml/grid', 'prototype' ], function(jQuery, confirm, _){ -<?php $myBlock = $block->getLayout()->getBlock('roleUsersGrid'); ?> -<?php if (is_object($myBlock) && $myBlock->getJsObjectName()) : ?> - var checkBoxes = $H(<?= /* @noEscape */ $myBlock->getUsers(true) ?>); + +script; + +$myBlock = $block->getLayout()->getBlock('roleUsersGrid'); +if (is_object($myBlock) && $myBlock->getJsObjectName()): + $scriptString .= <<<script + + var checkBoxes = \$H({$myBlock->getUsers(true)}); var warning = false; if (checkBoxes.size() > 0) { warning = true; @@ -43,7 +51,8 @@ require([ if (checked) { confirm({ - content: "<?= $myBlock->escapeHtml(__('Warning!\r\nThis action will remove this user from already assigned role\r\nAre you sure?')) ?>", + content: "{$myBlock->escapeJs(__('Warning!<br>This action will remove this user from already ' . + 'assigned role.<br>Are you sure?'))}", actions: { confirm: function () { checkbox[0].checked = false; @@ -92,7 +101,9 @@ require([ if (!allCheckbox.checked && _.size(checkBoxes._object) > 0) { allCheckbox.checked = true; confirm({ - content: "<?= $myBlock->escapeHtml(__('Warning!\r\nThis action will remove those users from already assigned roles\r\nAre you sure?')) ?>", + content: "{$myBlock->escapeJs( + __('Warning!<br>This action will remove those users from already assigned roles.<br>Are you sure?') + )}", actions: { confirm: function () { allCheckbox.checked = false; @@ -105,25 +116,25 @@ require([ } } function markCheckboxes(value) { - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.rows.each(function(row) + {$myBlock->escapeJs($myBlock->getJsObjectName())}.rows.each(function(row) { $(row).getElementsByClassName('checkbox')[0].checked = value; - roleUsersRowInit(<?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>, row); + roleUsersRowInit({$myBlock->escapeJs($myBlock->getJsObjectName())}, row); }); } function onLoad() { - if (typeof <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?> !== 'undefined') { - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>. + if (typeof {$myBlock->escapeJs($myBlock->getJsObjectName())} !== 'undefined') { + {$myBlock->escapeJs($myBlock->getJsObjectName())}. rowClickCallback = roleUsersRowClick; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>. + {$myBlock->escapeJs($myBlock->getJsObjectName())}. initRowCallback = roleUsersRowInit; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>. + {$myBlock->escapeJs($myBlock->getJsObjectName())}. checkboxCheckCallback = registerUserRole; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>. + {$myBlock->escapeJs($myBlock->getJsObjectName())}. checkCheckboxes = massSelectUsers; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>. + {$myBlock->escapeJs($myBlock->getJsObjectName())}. rows.each(function (row) { - roleUsersRowInit(<?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>, row) + roleUsersRowInit({$myBlock->escapeJs($myBlock->getJsObjectName())}, row) }); $('in_role_user_old').value = $('in_role_user').value; } else { @@ -131,7 +142,13 @@ require([ } } onLoad(); -<?php endif; ?> + +script; +endif; +$scriptString .= <<<script }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/User/view/adminhtml/templates/user/roles_grid_js.phtml b/app/code/Magento/User/view/adminhtml/templates/user/roles_grid_js.phtml index 92a97e825ea67..7455c26334c02 100644 --- a/app/code/Magento/User/view/adminhtml/templates/user/roles_grid_js.phtml +++ b/app/code/Magento/User/view/adminhtml/templates/user/roles_grid_js.phtml @@ -3,18 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script + require([ "mage/adminhtml/grid", "prototype" ], function(){ -<?php $myBlock = $block->getLayout()->getBlock('user.roles.grid'); ?> -<?php if (is_object($myBlock) && $myBlock->getJsObjectName()) : ?> - var radioBoxes = $H({}); +script; + +$myBlock = $block->getLayout()->getBlock('user.roles.grid'); +if (is_object($myBlock) && $myBlock->getJsObjectName()): + $scriptString .= <<<script + + var radioBoxes = \$H({}); var warning = false; - var userRoles = $H(<?= /* @noEscape */ $myBlock->getSelectedRoles(true) ?>); + var userRoles = \$H({$myBlock->getSelectedRoles(true)}); if (userRoles.size() > 0) warning = true; $('user_user_roles').value = userRoles.toQueryString(); @@ -37,7 +44,9 @@ require([ if(checkbox[0] && !checkbox[0].checked){ var checked = isInput ? checkbox[0].checked : !checkbox[0].checked; if (checked && warning && radioBoxes.size() > 0) { - if ( !confirm("<?= $myBlock->escapeHtml(__('Warning!\r\nThis action will remove this user from already assigned role\r\nAre you sure?')) ?>") ) { + if ( !confirm("{$myBlock->escapeJs( + __('Warning!<br>This action will remove this user from already assigned role.<br>Are you sure?') + )}") ) { checkbox[0].checked = false; for(i in radioBoxes) { if( radioBoxes[i].status == 1) { @@ -48,7 +57,7 @@ require([ } warning = false; } - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.setCheckboxChecked(checkbox[0], checked); + {$myBlock->escapeJs($myBlock->getJsObjectName())}.setCheckboxChecked(checkbox[0], checked); } } } @@ -60,19 +69,24 @@ require([ } } - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.rowClickCallback = roleRowClick; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.initRowCallback = rolesRowInit; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.checkboxCheckCallback = registerUserRole; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.rows.each(function(row){ - rolesRowInit(<?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>, row) + {$myBlock->escapeJs($myBlock->getJsObjectName())}.rowClickCallback = roleRowClick; + {$myBlock->escapeJs($myBlock->getJsObjectName())}.initRowCallback = rolesRowInit; + {$myBlock->escapeJs($myBlock->getJsObjectName())}.checkboxCheckCallback = registerUserRole; + {$myBlock->escapeJs($myBlock->getJsObjectName())}.rows.each(function(row){ + rolesRowInit({$myBlock->escapeJs($myBlock->getJsObjectName())}, row) }); -<?php endif; ?> + +script; +endif; +$scriptString .= <<<script }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php $editBlock = $block->getLayout()->getBlock('adminhtml.user.edit'); ?> -<?php if (is_object($editBlock)) : ?> +<?php if (is_object($editBlock)): ?> <script type="text/x-magento-init"> { "[data-role=delete-user]" : { diff --git a/app/code/Magento/Usps/Model/Carrier.php b/app/code/Magento/Usps/Model/Carrier.php index 1c8ff0ce9efa9..85e0cf2f6999a 100644 --- a/app/code/Magento/Usps/Model/Carrier.php +++ b/app/code/Magento/Usps/Model/Carrier.php @@ -1470,7 +1470,7 @@ protected function _filterServiceName($name) * * @param \Magento\Framework\DataObject $request * @return string - * @deprecated This method should not be used anymore. + * @deprecated 100.2.1 This method should not be used anymore. * @see \Magento\Usps\Model\Carrier::_doShipmentRequest method doc block. */ protected function _formUsExpressShipmentRequest(\Magento\Framework\DataObject $request) @@ -1647,7 +1647,7 @@ protected function _convertPoundOunces($weightInPounds) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @deprecated Should not be used anymore. + * @deprecated 100.2.1 Should not be used anymore. * @see \Magento\Usps\Model\Carrier::_doShipmentRequest doc block. */ protected function _formIntlShipmentRequest(\Magento\Framework\DataObject $request) @@ -1902,7 +1902,7 @@ protected function _formIntlShipmentRequest(\Magento\Framework\DataObject $reque * @param \Magento\Framework\DataObject $request * @return \Magento\Framework\DataObject * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @deprecated This method must not be used anymore. Starting from 23.02.2018 USPS elimates API usage for + * @deprecated 100.2.1 This method must not be used anymore. Starting from 23.02.2018 USPS elimates API usage for * free shipping labels generating. */ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) diff --git a/app/code/Magento/Variable/view/adminhtml/templates/system/variable/js.phtml b/app/code/Magento/Variable/view/adminhtml/templates/system/variable/js.phtml index a569b8e71a055..28f66d4c913e2 100644 --- a/app/code/Magento/Variable/view/adminhtml/templates/system/variable/js.phtml +++ b/app/code/Magento/Variable/view/adminhtml/templates/system/variable/js.phtml @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script + require([ 'prototype' ], function () { @@ -27,4 +31,7 @@ window.toggleValueElement = function(element) { } }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Vault/Api/Data/PaymentTokenFactoryInterface.php b/app/code/Magento/Vault/Api/Data/PaymentTokenFactoryInterface.php index bb6343691f726..94d14dc14228c 100644 --- a/app/code/Magento/Vault/Api/Data/PaymentTokenFactoryInterface.php +++ b/app/code/Magento/Vault/Api/Data/PaymentTokenFactoryInterface.php @@ -9,7 +9,7 @@ /** * Interface PaymentTokenFactoryInterface * @api - * @since 100.3.0 + * @since 101.0.0 */ interface PaymentTokenFactoryInterface { @@ -24,7 +24,7 @@ interface PaymentTokenFactoryInterface * Create payment token entity * @param $type string|null * @return PaymentTokenInterface - * @since 100.3.0 + * @since 101.0.0 */ public function create($type = null); } diff --git a/app/code/Magento/Vault/Api/Data/PaymentTokenInterfaceFactory.php b/app/code/Magento/Vault/Api/Data/PaymentTokenInterfaceFactory.php index 1a854ec814844..501e516841b6e 100644 --- a/app/code/Magento/Vault/Api/Data/PaymentTokenInterfaceFactory.php +++ b/app/code/Magento/Vault/Api/Data/PaymentTokenInterfaceFactory.php @@ -8,7 +8,7 @@ /** * Interface PaymentTokenInterfaceFactory - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @see PaymentTokenFactoryInterface * @codingStandardsIgnoreStart */ diff --git a/app/code/Magento/Vault/Model/AbstractPaymentTokenFactory.php b/app/code/Magento/Vault/Model/AbstractPaymentTokenFactory.php index d568a91c0421b..ab1e2ccd783d5 100644 --- a/app/code/Magento/Vault/Model/AbstractPaymentTokenFactory.php +++ b/app/code/Magento/Vault/Model/AbstractPaymentTokenFactory.php @@ -12,7 +12,7 @@ /** * Class AbstractPaymentTokenFactory - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @see PaymentTokenFactoryInterface */ abstract class AbstractPaymentTokenFactory implements PaymentTokenInterfaceFactory diff --git a/app/code/Magento/Vault/Model/AccountPaymentTokenFactory.php b/app/code/Magento/Vault/Model/AccountPaymentTokenFactory.php index 8fb060b41a24f..e9178ccaf50a8 100644 --- a/app/code/Magento/Vault/Model/AccountPaymentTokenFactory.php +++ b/app/code/Magento/Vault/Model/AccountPaymentTokenFactory.php @@ -7,7 +7,7 @@ /** * Class AccountPaymentTokenFactory - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @see PaymentTokenFactoryInterface */ class AccountPaymentTokenFactory extends AbstractPaymentTokenFactory diff --git a/app/code/Magento/Vault/Model/CreditCardTokenFactory.php b/app/code/Magento/Vault/Model/CreditCardTokenFactory.php index 735dc7c706f62..b0015e3f78316 100644 --- a/app/code/Magento/Vault/Model/CreditCardTokenFactory.php +++ b/app/code/Magento/Vault/Model/CreditCardTokenFactory.php @@ -7,7 +7,7 @@ /** * Class CreditCardTokenFactory - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @see PaymentTokenFactoryInterface */ class CreditCardTokenFactory extends AbstractPaymentTokenFactory diff --git a/app/code/Magento/Vault/Model/PaymentTokenFactory.php b/app/code/Magento/Vault/Model/PaymentTokenFactory.php index 6249fa4944a2c..cee838d622749 100644 --- a/app/code/Magento/Vault/Model/PaymentTokenFactory.php +++ b/app/code/Magento/Vault/Model/PaymentTokenFactory.php @@ -13,7 +13,7 @@ /** * PaymentTokenFactory class * @api - * @since 100.3.0 + * @since 101.0.0 */ class PaymentTokenFactory implements PaymentTokenFactoryInterface { @@ -42,7 +42,7 @@ public function __construct(ObjectManagerInterface $objectManager, array $tokenT * Create payment token entity * @param $type string * @return PaymentTokenInterface - * @since 100.3.0 + * @since 101.0.0 */ public function create($type = null) { diff --git a/app/code/Magento/Vault/Model/PaymentTokenRepository.php b/app/code/Magento/Vault/Model/PaymentTokenRepository.php index 2ccd6181b9b81..46d7b6d2e80fe 100644 --- a/app/code/Magento/Vault/Model/PaymentTokenRepository.php +++ b/app/code/Magento/Vault/Model/PaymentTokenRepository.php @@ -158,7 +158,7 @@ public function save(Data\PaymentTokenInterface $paymentToken) * @param FilterGroup $filterGroup * @param Collection $collection * @return void - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @throws \Magento\Framework\Exception\InputException */ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collection $collection) @@ -172,7 +172,7 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collecti /** * Retrieve collection processor * - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml b/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml index f496e500a4d9b..a43d6578925b2 100644 --- a/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml +++ b/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml @@ -28,11 +28,15 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Vault/view/adminhtml/templates/form/vault.phtml b/app/code/Magento/Vault/view/adminhtml/templates/form/vault.phtml index fb0666cde976f..8311ff374c3d1 100644 --- a/app/code/Magento/Vault/view/adminhtml/templates/form/vault.phtml +++ b/app/code/Magento/Vault/view/adminhtml/templates/form/vault.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var Magento\Vault\Block\Form $block */ +/** + * @var Magento\Vault\Block\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $code = $block->escapeHtml($block->getMethodCode()); ?> <fieldset data-mage-init='{ @@ -12,10 +15,11 @@ $code = $block->escapeHtml($block->getMethodCode()); "code": "<?= /* @noEscape */ $code ?>", "fieldset": "payment_form_<?= /* @noEscape */ $code ?>" } - }' class="admin__fieldset payment-method" - id="payment_form_<?= /* @noEscape */ $code ?>" - style="display:none" - > + }' class="admin__fieldset payment-method" id="payment_form_<?= /* @noEscape */ $code ?>"> <input type="hidden" name="payment[public_hash]" id="<?= /* @noEscape */ $code ?>_public_hash" value="" /> <?= $block->getChildHtml() ?> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> diff --git a/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php b/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php index 5e50cdee794ce..a01c5054f9b5f 100644 --- a/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php +++ b/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php @@ -179,7 +179,7 @@ private function prepareOperationInput(string $serviceClass, array $methodMetada * @param string $serviceMethod * @param array $arguments * @return array - * @deprecated + * @deprecated 100.3.2 * @see Handler::prepareOperationInput() */ protected function _prepareRequestData($serviceClass, $serviceMethod, $arguments) diff --git a/app/code/Magento/Webapi/Model/ConfigInterface.php b/app/code/Magento/Webapi/Model/ConfigInterface.php index 338c18795595f..a0467fb840ccb 100644 --- a/app/code/Magento/Webapi/Model/ConfigInterface.php +++ b/app/code/Magento/Webapi/Model/ConfigInterface.php @@ -12,6 +12,7 @@ * This class gives access to consolidated web API configuration from <Module_Name>/etc/webapi.xml files. * * @api + * @since 100.2.4 */ interface ConfigInterface { @@ -19,6 +20,7 @@ interface ConfigInterface * Return services loaded from cache if enabled or from files merged previously * * @return array + * @since 100.2.4 */ public function getServices(); } diff --git a/app/code/Magento/WebapiAsync/Model/BulkServiceConfig.php b/app/code/Magento/WebapiAsync/Model/BulkServiceConfig.php index a0499087c35b9..febe7cba0b7fc 100644 --- a/app/code/Magento/WebapiAsync/Model/BulkServiceConfig.php +++ b/app/code/Magento/WebapiAsync/Model/BulkServiceConfig.php @@ -15,6 +15,7 @@ /** * @api + * @since 100.2.0 */ class BulkServiceConfig implements \Magento\Webapi\Model\ConfigInterface { @@ -58,6 +59,7 @@ public function __construct( * Return services loaded from cache if enabled or from files merged previously * * @return array + * @since 100.2.0 */ public function getServices() { diff --git a/app/code/Magento/WebapiAsync/Model/OperationRepository.php b/app/code/Magento/WebapiAsync/Model/OperationRepository.php index 7af8ff877ebbc..87db3dfb59e2c 100644 --- a/app/code/Magento/WebapiAsync/Model/OperationRepository.php +++ b/app/code/Magento/WebapiAsync/Model/OperationRepository.php @@ -72,6 +72,7 @@ public function __construct( */ public function create($topicName, $entityParams, $groupId, $operationId): OperationInterface { + $this->messageValidator->validate($topicName, $entityParams); $requestData = $this->inputParamsResolver->getInputData(); if ($operationId === null || !isset($requestData[$operationId])) { @@ -88,13 +89,13 @@ public function create($topicName, $entityParams, $groupId, $operationId): Opera ]; $data = [ 'data' => [ - OperationInterface::BULK_ID => $groupId, - OperationInterface::TOPIC_NAME => $topicName, + OperationInterface::ID => $operationId, + OperationInterface::BULK_ID => $groupId, + OperationInterface::TOPIC_NAME => $topicName, OperationInterface::SERIALIZED_DATA => $this->jsonSerializer->serialize($serializedData), - OperationInterface::STATUS => OperationInterface::STATUS_TYPE_OPEN, + OperationInterface::STATUS => OperationInterface::STATUS_TYPE_OPEN, ], ]; - /** @var OperationInterface $operation */ $operation = $this->operationFactory->create($data); return $operation; diff --git a/app/code/Magento/WebapiAsync/Model/ServiceConfig.php b/app/code/Magento/WebapiAsync/Model/ServiceConfig.php index 4c085935090bd..8387b2dc53118 100644 --- a/app/code/Magento/WebapiAsync/Model/ServiceConfig.php +++ b/app/code/Magento/WebapiAsync/Model/ServiceConfig.php @@ -17,6 +17,7 @@ * This class gives access to consolidated web API configuration from <Module_Name>/etc/webapi_async.xml files. * * @api + * @since 100.2.0 */ class ServiceConfig { @@ -63,6 +64,7 @@ public function __construct( * Return services loaded from cache if enabled or from files merged previously * * @return array + * @since 100.2.0 */ public function getServices() { diff --git a/app/code/Magento/Weee/Block/Item/Price/Renderer.php b/app/code/Magento/Weee/Block/Item/Price/Renderer.php index 721df2c83f460..e29dd9d58f0b4 100644 --- a/app/code/Magento/Weee/Block/Item/Price/Renderer.php +++ b/app/code/Magento/Weee/Block/Item/Price/Renderer.php @@ -40,6 +40,7 @@ public function __construct( array $data = [] ) { $this->weeeHelper = $weeeHelper; + $data['weeeHelper'] = $this->weeeHelper; parent::__construct($context, $taxHelper, $priceCurrency, $data); $this->_isScopePrivate = true; } diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml index 77a8e6e6fd20c..0f4a7f9a55d26 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml @@ -51,8 +51,12 @@ <!--Set catalog price scope to Global--> <comment userInput="Set catalog price scope to Global" stepKey="commentSetPriceScope"/> <magentoCLI command="config:set catalog/price/scope 0" stepKey="setPriceScopeGlobal"/> - <magentoCLI command="indexer:reindex catalog_product_price" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalog_product_price"/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!--Set catalog price scope to Global--> @@ -97,8 +101,12 @@ <!--Set catalog price scope to Website--> <comment userInput="Set catalog price scope to Website" stepKey="commentSetPriceScope"/> <magentoCLI command="config:set catalog/price/scope 1" stepKey="setPriceScopeWebsite"/> - <magentoCLI command="indexer:reindex catalog_product_price" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalog_product_price"/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--See available websites only 'All Websites'--> <comment userInput="See available websites 'All Websites', 'Main Website' and Second website" stepKey="commentCheckWebsitesInProductPage"/> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductPageSecondTime"> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml index 60c39dd5058b5..0d7c21b6efffc 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml @@ -38,8 +38,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductInitial"/> </before> <after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductListing"/> - <waitForPageLoad stepKey="waitForProductListingPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductListing"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="createProductFPTAttribute" stepKey="deleteProductFPTAttribute"/> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml index 74d6c2a97b089..e78036458301b 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml @@ -68,7 +68,7 @@ <createData entity="WeeeConfigDisable" stepKey="disableFPT"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <magentoCron groups="index" stepKey="reindexBrokenIndices"/> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml index 92f526c79e926..74ba7c1f2bff3 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml @@ -63,7 +63,7 @@ <createData entity="DefaultTaxConfig" stepKey="defaultTaxConfiguration"/> <createData entity="WeeeConfigDisable" stepKey="disableFPT"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <magentoCron groups="index" stepKey="reindexBrokenIndices"/> diff --git a/app/code/Magento/Weee/view/adminhtml/templates/renderer/tax.phtml b/app/code/Magento/Weee/view/adminhtml/templates/renderer/tax.phtml index 1b77231640868..1eff06bb4b985 100644 --- a/app/code/Magento/Weee/view/adminhtml/templates/renderer/tax.phtml +++ b/app/code/Magento/Weee/view/adminhtml/templates/renderer/tax.phtml @@ -4,28 +4,37 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php -/** @var $block \Magento\Weee\Block\Renderer\Weee\Tax */ +/** + * @var $block \Magento\Weee\Block\Renderer\Weee\Tax + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +/** @var \Magento\Directory\Helper\Data $directoryHelper */ +$directoryHelper = $block->getData('directoryHelper'); + $data = ['fptAttribute' => [ - 'region' => $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode( - $this->helper(\Magento\Directory\Helper\Data::class)->getRegionJson() - ), + 'region' => $jsonHelper->jsonDecode($directoryHelper->getRegionJson()), 'itemsData' => $block->getValues(), 'bundlePriceType' => '#price_type', ]]; ?> <div id="attribute-<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>-container" class="field" data-attribute-code="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>" - data-mage-init="<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($data) ?>"> + data-mage-init="<?= /* @noEscape */ $jsonHelper->jsonEncode($data) ?>"> <label class="label"><span><?= $block->escapeHtml($block->getElement()->getLabel()) ?></span></label> <div class="control"> <table class="data-table"> <thead> <tr> - <th class="col-website" <?php if (!$block->isMultiWebsites()) : ?>style="display: none;"<?php endif; ?>><?= $block->escapeHtml(__('Website')) ?></th> + <th class="col-website"><?= $block->escapeHtml(__('Website')) ?></th> + <?php if (!$block->isMultiWebsites()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'th.col-website') ?> + <?php endif; ?> <th class="col-country required"><?= $block->escapeHtml(__('Country/State')) ?></th> <th class="col-tax required"><?= $block->escapeHtml(__('Tax')) ?></th> <th class="col-action"><?= $block->escapeHtml(__('Action')) ?></th> @@ -43,7 +52,8 @@ $data = ['fptAttribute' => [ Hidden field below with attribute code id is necessary for jQuery validation plugin. Validation message will be displayed after this field. --> - <input type="hidden" name="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>" id="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>" disabled="disabled"> + <input type="hidden" name="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>" + id="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>" disabled="disabled"> </div> <script data-role="row-template" type="text/x-magento-template"> @@ -51,22 +61,32 @@ $data = ['fptAttribute' => [ $elementName = $block->escapeHtmlAttr($block->getElement()->getName()); $elementClass = $block->escapeHtmlAttr($block->getElement()->getClass()); ?> - <tr id="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>_weee_tax_row_<%- data.index %>" data-role="fpt-item-row"> - <td class="col-website" <?php if (!$block->isMultiWebsites()) : ?>style="display: none"<?php endif; ?>> + <tr id="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>_weee_tax_row_<%- data.index %>" + data-role="fpt-item-row"> + <td class="col-website"> <select id="<?= /* @noEscape */ $elementName ?>_weee_tax_row_<%- data.index %>_website" name="<?= /* @noEscape */ $elementName ?>[<%- data.index %>][website_id]" class="<?= /* @noEscape */ $elementClass ?> website required-entry" data-role="select-website"> - <?php foreach ($block->getWebsites() as $_websiteId => $_info) : ?> - <option value="<?= /* @noEscape */ $_websiteId ?>"><?= $block->escapeHtml($_info['name']) ?><?php if (!empty($_info['currency'])) : ?>[<?= /* @noEscape */ $_info['currency'] ?>]<?php endif; ?></option> + <?php foreach ($block->getWebsites() as $_websiteId => $_info): ?> + <option value="<?= /* @noEscape */ $_websiteId ?>"><?= $block->escapeHtml($_info['name']) ?> + <?php if (!empty($_info['currency'])): ?> + [<?= /* @noEscape */ $_info['currency'] ?>] + <?php endif; ?> + </option> <?php endforeach ?> </select> </td> + <?php if (!$block->isMultiWebsites()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'td.col-website') ?> + <?php endif; ?> <td class="col-country"> <select id="<?= /* @noEscape */ $elementName ?>_weee_tax_row_<%- data.index %>_country" name="<?= /* @noEscape */ $elementName ?>[<%- data.index %>][country]" class="<?= /* @noEscape */ $elementClass ?> country required-entry" data-role="select-country"> - <?php foreach ($block->getCountries() as $_country) : ?> - <option value="<?= $block->escapeHtmlAttr($_country['value']) ?>"><?= $block->escapeHtml($_country['label']) ?></option> + <?php foreach ($block->getCountries() as $_country): ?> + <option value="<?= $block->escapeHtmlAttr($_country['value']) ?>"> + <?= $block->escapeHtml($_country['label']) ?> + </option> <?php endforeach ?> </select> <select id="<?= /* @noEscape */ $elementName ?>_weee_tax_row_<%- data.index %>_state" @@ -81,7 +101,8 @@ $data = ['fptAttribute' => [ type="text" value="<%- data.value %>"/> </td> <td class="col-action"> - <input name="<?= /* @noEscape */ $elementName ?>[<%- data.index %>][delete]" class="delete" type="hidden" value="" data-role="delete-fpt-item"/> + <input name="<?= /* @noEscape */ $elementName ?>[<%- data.index %>][delete]" class="delete" + type="hidden" value="" data-role="delete-fpt-item"/> <?= $block->getChildHtml('delete_button') ?> </td> </tr> diff --git a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_excl_tax.phtml b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_excl_tax.phtml index 15abae5c889fe..b9b5a00d0a157 100644 --- a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_excl_tax.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_excl_tax.phtml @@ -4,31 +4,39 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** @var \Magento\Weee\Helper\Data $weeeHelper */ +$weeeHelper = $block->getData('weeeHelper'); $_item = $block->getItem(); ?> -<?php if ($block->displayPriceWithWeeeDetails()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> -<?php else : ?> +<?php if ($block->displayPriceWithWeeeDetails()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> +<?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getRowDisplayPriceExclTax()) ?> </span> -<?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item)) : ?> - <span class="cart-tax-info" id="esubtotal-item-tax-details<?= (int) $_item->getId() ?>" style="display: none;"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item) as $tax) : ?> - <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"><?= /* @noEscape */ $block->formatPrice($tax['row_amount'], true, true) ?></span> +<?php if ($weeeHelper->getApplied($_item)): ?> + <span class="cart-tax-info no-display" id="esubtotal-item-tax-details<?= (int) $_item->getId() ?>"> + <?php if ($block->displayPriceWithWeeeDetails()): ?> + <?php foreach ($weeeHelper->getApplied($_item) as $tax): ?> + <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> + <?= /* @noEscape */ $block->formatPrice($tax['row_amount'], true, true) ?> + </span> <?php endforeach; ?> <?php endif; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> + <?php if ($block->displayFinalPrice()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalRowDisplayPriceExclTax()) ?> </span> diff --git a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_incl_tax.phtml b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_incl_tax.phtml index b848698b8b829..38f2c528b15c9 100644 --- a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_incl_tax.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_incl_tax.phtml @@ -4,34 +4,39 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $_item = $block->getItem(); /** @var $_weeeHelper \Magento\Weee\Helper\Data */ -$_weeeHelper = $this->helper(\Magento\Weee\Helper\Data::class); +$_weeeHelper = $block->getData('weeeHelper'); ?> <?php $_incl = $_item->getRowTotalInclTax(); ?> -<?php if ($block->displayPriceWithWeeeDetails()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> -<?php else : ?> +<?php if ($block->displayPriceWithWeeeDetails()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> +<?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getRowDisplayPriceInclTax()) ?> </span> -<?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item)) : ?> - <span class="cart-tax-info" id="subtotal-item-tax-details<?= (int) $_item->getId() ?>" style="display: none;"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item) as $tax) : ?> - <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"><?= /* @noEscape */ $block->formatPrice($tax['row_amount_incl_tax'], true, true) ?></span> +<?php if ($_weeeHelper->getApplied($_item)): ?> + <span class="cart-tax-info no-display" id="subtotal-item-tax-details<?= (int) $_item->getId() ?>"> + <?php if ($block->displayPriceWithWeeeDetails()): ?> + <?php foreach ($_weeeHelper->getApplied($_item) as $tax): ?> + <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> + <?= /* @noEscape */ $block->formatPrice($tax['row_amount_incl_tax'], true, true) ?> + </span> <?php endforeach; ?> <?php endif; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> + <?php if ($block->displayFinalPrice()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total Incl. Tax')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalRowDisplayPriceInclTax()) ?> </span> diff --git a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_excl_tax.phtml b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_excl_tax.phtml index a485de90c871d..8bb331c109119 100644 --- a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_excl_tax.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_excl_tax.phtml @@ -4,31 +4,39 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** @var \Magento\Weee\Helper\Data $weeeHelper */ +$weeeHelper = $block->getData('weeeHelper'); $_item = $block->getItem(); ?> -<?php if ($block->displayPriceWithWeeeDetails()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $_item->getId() ?>"}}'> -<?php else : ?> +<?php if ($block->displayPriceWithWeeeDetails()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $_item->getId() ?>"}}'> +<?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getUnitDisplayPriceExclTax()) ?> </span> -<?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item)) : ?> - <span class="cart-tax-info" id="eunit-item-tax-details<?= (int) $_item->getId() ?>" style="display:none;"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item) as $tax) : ?> - <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"><?= /* @noEscape */ $block->formatPrice($tax['amount'], true, true) ?></span> +<?php if ($weeeHelper->getApplied($_item)): ?> + <span class="cart-tax-info no-display" id="eunit-item-tax-details<?= (int) $_item->getId() ?>"> + <?php if ($block->displayPriceWithWeeeDetails()): ?> + <?php foreach ($weeeHelper->getApplied($_item) as $tax): ?> + <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> + <?= /* @noEscape */ $block->formatPrice($tax['amount'], true, true) ?> + </span> <?php endforeach; ?> <?php endif; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $_item->getId() ?>"}}'> + <?php if ($block->displayFinalPrice()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $_item->getId() ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalUnitDisplayPriceExclTax()) ?> </span> diff --git a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_incl_tax.phtml b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_incl_tax.phtml index 0dada610e181e..e667796825327 100644 --- a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_incl_tax.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_incl_tax.phtml @@ -4,35 +4,40 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $_item = $block->getItem(); /** @var $_weeeHelper \Magento\Weee\Helper\Data */ -$_weeeHelper = $this->helper(\Magento\Weee\Helper\Data::class); +$_weeeHelper = $block->getData('weeeHelper'); ?> <?php $_incl = $_item->getPriceInclTax(); ?> -<?php if ($block->displayPriceWithWeeeDetails()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $_item->getId() ?>"}}'> -<?php else : ?> +<?php if ($block->displayPriceWithWeeeDetails()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $_item->getId() ?>"}}'> +<?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getUnitDisplayPriceInclTax()) ?> </span> -<?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item)) : ?> - <span class="cart-tax-info" id="unit-item-tax-details<?= (int) $_item->getId() ?>" style="display: none;"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item) as $tax) : ?> - <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"><?= /* @noEscape */ $block->formatPrice($tax['amount_incl_tax'], true, true) ?></span> +<?php if ($_weeeHelper->getApplied($_item)): ?> + <span class="cart-tax-info no-display" id="unit-item-tax-details<?= (int) $_item->getId() ?>"> + <?php if ($block->displayPriceWithWeeeDetails()): ?> + <?php foreach ($_weeeHelper->getApplied($_item) as $tax): ?> + <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> + <?= /* @noEscape */ $block->formatPrice($tax['amount_incl_tax'], true, true) ?> + </span> <?php endforeach; ?> <?php endif; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $_item->getId() ?>"}}'> + <?php if ($block->displayFinalPrice()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $_item->getId() ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total Incl. Tax')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalUnitDisplayPriceInclTax()) ?> </span> diff --git a/app/code/Magento/Weee/view/frontend/templates/item/price/row.phtml b/app/code/Magento/Weee/view/frontend/templates/item/price/row.phtml index 37aa852871408..5bd2a2f81acbc 100644 --- a/app/code/Magento/Weee/view/frontend/templates/item/price/row.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/item/price/row.phtml @@ -4,35 +4,40 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** @var \Magento\Weee\Helper\Data $weeeHelper */ +$weeeHelper = $block->getData('weeeHelper'); $item = $block->getItem(); ?> -<?php if (($block->displayPriceInclTax() || $block->displayBothPrices()) && !$item->getNoSubtotal()) : ?> +<?php if (($block->displayPriceInclTax() || $block->displayBothPrices()) && !$item->getNoSubtotal()): ?> <span class="price-including-tax" data-label="<?= $block->escapeHtmlAttr(__('Incl. Tax')) ?>"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> + <?php if ($block->displayPriceWithWeeeDetails()): ?> <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $item->getId() ?>"}}'> - <?php else : ?> + <?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getRowDisplayPriceInclTax()) ?> </span> - <?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item)) : ?> - <div class="cart-tax-info" id="subtotal-item-tax-details<?= (int) $item->getId() ?>" style="display: none;"> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item) as $tax) : ?> + <?php if ($weeeHelper->getApplied($item)): ?> + <div class="cart-tax-info no-display" id="subtotal-item-tax-details<?= (int) $item->getId() ?>"> + <?php foreach ($weeeHelper->getApplied($item) as $tax): ?> <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> <?= /* @noEscape */ $block->formatPrice($tax['row_amount_incl_tax'], true, true) ?> </span> <?php endforeach; ?> </div> - <?php if ($block->displayFinalPrice()) : ?> + <?php if ($block->displayFinalPrice()): ?> <span class="cart-tax-total" - data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $item->getId() ?>"}}'> + data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $item->getId() + ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total Incl. Tax')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalRowDisplayPriceInclTax()) ?> </span> @@ -42,30 +47,30 @@ $item = $block->getItem(); </span> <?php endif; ?> -<?php if ($block->displayPriceExclTax() || $block->displayBothPrices()) : ?> +<?php if ($block->displayPriceExclTax() || $block->displayBothPrices()): ?> <span class="price-excluding-tax" data-label="<?= $block->escapeHtmlAttr(__('Excl. Tax')) ?>"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> + <?php if ($block->displayPriceWithWeeeDetails()): ?> <span class="cart-tax-total" - data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $item->getId() ?>"}}'> - <?php else : ?> + data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $item->getId()?>"}}'> + <?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getRowDisplayPriceExclTax()) ?> </span> - <?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item)) : ?> - <span class="cart-tax-info" id="esubtotal-item-tax-details<?= (int) $item->getId() ?>" - style="display: none;"> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item) as $tax) : ?> + <?php if ($weeeHelper->getApplied($item)): ?> + <span class="cart-tax-info no-display" id="esubtotal-item-tax-details<?= (int) $item->getId() ?>"> + <?php foreach ($weeeHelper->getApplied($item) as $tax): ?> <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> <?= /* @noEscape */ $block->formatPrice($tax['row_amount'], true, true) ?> </span> <?php endforeach; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> + <?php if ($block->displayFinalPrice()): ?> <span class="cart-tax-total" - data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $item->getId() ?>"}}'> + data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int)$item->getId() + ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalRowDisplayPriceExclTax()) ?> </span> diff --git a/app/code/Magento/Weee/view/frontend/templates/item/price/unit.phtml b/app/code/Magento/Weee/view/frontend/templates/item/price/unit.phtml index 4e62409ad00f4..39d0bc59653d4 100644 --- a/app/code/Magento/Weee/view/frontend/templates/item/price/unit.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/item/price/unit.phtml @@ -4,33 +4,37 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** @var \Magento\Weee\Helper\Data $weeeHelper */ +$weeeHelper = $block->getData('weeeHelper'); $item = $block->getItem(); ?> -<?php if ($block->displayPriceInclTax() || $block->displayBothPrices()) : ?> +<?php if ($block->displayPriceInclTax() || $block->displayBothPrices()): ?> <span class="price-including-tax" data-label="<?= $block->escapeHtmlAttr(__('Incl. Tax')) ?>"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> + <?php if ($block->displayPriceWithWeeeDetails()): ?> <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $item->getId() ?>"}}'> - <?php else : ?> + <?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getUnitDisplayPriceInclTax()) ?> </span> - <?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item)) : ?> - <span class="cart-tax-info" id="unit-item-tax-details<?= (int) $item->getId() ?>" style="display: none;"> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item) as $tax) : ?> + <?php if ($weeeHelper->getApplied($item)): ?> + <span class="cart-tax-info no-display" id="unit-item-tax-details<?= (int) $item->getId() ?>"> + <?php foreach ($weeeHelper->getApplied($item) as $tax): ?> <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> <?= /* @noEscape */ $block->formatPrice($tax['amount_incl_tax'], true, true) ?> </span> <?php endforeach; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> + <?php if ($block->displayFinalPrice()): ?> <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $item->getId() ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total Incl. Tax')) ?>"> @@ -42,30 +46,28 @@ $item = $block->getItem(); </span> <?php endif; ?> -<?php if ($block->displayPriceExclTax() || $block->displayBothPrices()) : ?> +<?php if ($block->displayPriceExclTax() || $block->displayBothPrices()): ?> <span class="price-excluding-tax" data-label="<?= $block->escapeHtmlAttr(__('Excl. Tax')) ?>"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> + <?php if ($block->displayPriceWithWeeeDetails()): ?> <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $item->getId() ?>"}}'> - <?php else : ?> + <?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getUnitDisplayPriceExclTax()) ?> </span> - <?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item)) : ?> - <span class="cart-tax-info" id="eunit-item-tax-details<?= (int) $item->getId() ?>" - style="display: none;"> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item) as $tax) : ?> + <?php if ($weeeHelper->getApplied($item)): ?> + <span class="cart-tax-info no-display" id="eunit-item-tax-details<?= (int) $item->getId() ?>"> + <?php foreach ($weeeHelper->getApplied($item) as $tax): ?> <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> <?= /* @noEscape */ $block->formatPrice($tax['amount'], true, true) ?> </span> <?php endforeach; ?> </span> - - <?php if ($block->displayFinalPrice()) : ?> + <?php if ($block->displayFinalPrice()): ?> <span class="cart-tax-total" - data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $item->getId() ?>"}}'> + data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?=(int)$item->getId()?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalUnitDisplayPriceExclTax()) ?> </span> diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php index 45b3056eac68d..f10a821c510e1 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php @@ -11,6 +11,9 @@ */ namespace Magento\Widget\Block\Adminhtml\Widget; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Chooser widget block. */ @@ -26,20 +29,28 @@ class Chooser extends \Magento\Backend\Block\Template */ protected $_jsonEncoder; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Framework\Data\Form\Element\Factory $elementFactory * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Framework\Data\Form\Element\Factory $elementFactory, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_elementFactory = $elementFactory; + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); parent::__construct($context, $data); } @@ -189,33 +200,35 @@ protected function _toHtml() '</label> <div id="' . $chooserId . - 'advice-container" class="hidden"></div> - <script> - require(["prototype", "mage/adminhtml/wysiwyg/widget"], function(){ + 'advice-container" class="hidden"></div>' . + $this->secureRenderer->renderTag( + 'script', + [], + 'require(["prototype", "mage/adminhtml/wysiwyg/widget"], function(){ //<![CDATA[ (function() { var instantiateChooser = function() { window.' . - $chooserId . - ' = new WysiwygWidget.chooser( + $chooserId . + ' = new WysiwygWidget.chooser( "' . - $chooserId . - '", + $chooserId . + '", "' . - $this->getSourceUrl() . - '", + $this->getSourceUrl() . + '", ' . - $configJson . - ' + $configJson . + ' ); if ($("' . - $chooserId . - 'value")) { + $chooserId . + 'value")) { $("' . - $chooserId . - 'value").advaiceContainer = "' . - $chooserId . - 'advice-container"; + $chooserId . + 'value").advaiceContainer = "' . + $chooserId . + 'advice-container"; } } @@ -223,7 +236,8 @@ protected function _toHtml() })(); //]]> }); - </script> - '; + ', + false + ); } } diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php index 03d9d10311382..fe96cbecb425a 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php @@ -60,7 +60,7 @@ public function execute() /** * @return \Magento\Widget\Helper\Conditions - * @deprecated 100.1.4 + * @deprecated 101.0.0 */ private function getConditionsHelper() { diff --git a/app/code/Magento/Widget/Model/ResourceModel/Widget.php b/app/code/Magento/Widget/Model/ResourceModel/Widget.php index 8d78bb5a56800..4ed77b48f297d 100644 --- a/app/code/Magento/Widget/Model/ResourceModel/Widget.php +++ b/app/code/Magento/Widget/Model/ResourceModel/Widget.php @@ -9,7 +9,7 @@ /** * Resource model for widget. * - * @deprecated 100.2.0 Data from this table was moved to xml(widget.xml). + * @deprecated 101.0.0 Data from this table was moved to xml(widget.xml). */ class Widget extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Widget/Model/ResourceModel/Widget/Instance/Options/ThemeId.php b/app/code/Magento/Widget/Model/ResourceModel/Widget/Instance/Options/ThemeId.php index 8e5d8d63840fb..0532c57f0306d 100644 --- a/app/code/Magento/Widget/Model/ResourceModel/Widget/Instance/Options/ThemeId.php +++ b/app/code/Magento/Widget/Model/ResourceModel/Widget/Instance/Options/ThemeId.php @@ -9,7 +9,7 @@ /** * Widget Instance Theme Id Options * - * @deprecated 100.2.0 created new class that correctly loads theme options and whose name follows naming convention + * @deprecated 100.1.7 created new class that correctly loads theme options and whose name follows naming convention * @see \Magento\Widget\Model\ResourceModel\Widget\Instance\Options\Themes */ class ThemeId implements \Magento\Framework\Option\ArrayInterface diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index d07e84186b2c9..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; @@ -88,7 +103,7 @@ public function __construct( * * @return \Magento\Framework\Math\Random * - * @deprecated 100.1.0 + * @deprecated 100.0.10 */ private function getMathRandom() { @@ -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; } @@ -127,10 +139,11 @@ public function getWidgetByClassType($type) * @param string $type Widget type * @return null|\Magento\Framework\Simplexml\Element * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ 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/Widget/Model/Widget/Instance.php b/app/code/Magento/Widget/Model/Widget/Instance.php index 4ca126e659e09..7f4e3ae8610ba 100644 --- a/app/code/Magento/Widget/Model/Widget/Instance.php +++ b/app/code/Magento/Widget/Model/Widget/Instance.php @@ -99,11 +99,13 @@ class Instance extends \Magento\Framework\Model\AbstractModel /** * @var \Magento\Catalog\Model\Product\Type + * @since 101.0.4 */ protected $_productType; /** * @var \Magento\Widget\Model\Config\Reader + * @since 101.0.4 */ protected $_reader; diff --git a/app/code/Magento/Widget/composer.json b/app/code/Magento/Widget/composer.json index 3f0f7fb212d4a..2cf8429095ce7 100644 --- a/app/code/Magento/Widget/composer.json +++ b/app/code/Magento/Widget/composer.json @@ -12,7 +12,8 @@ "magento/module-cms": "*", "magento/module-store": "*", "magento/module-theme": "*", - "magento/module-variable": "*" + "magento/module-variable": "*", + "magento/module-ui": "*" }, "suggest": { "magento/module-widget-sample-data": "*" diff --git a/app/code/Magento/Widget/view/adminhtml/templates/catalog/category/widget/tree.phtml b/app/code/Magento/Widget/view/adminhtml/templates/catalog/category/widget/tree.phtml index 72de9550654d3..5bb6756bf4ebe 100644 --- a/app/code/Magento/Widget/view/adminhtml/templates/catalog/category/widget/tree.phtml +++ b/app/code/Magento/Widget/view/adminhtml/templates/catalog/category/widget/tree.phtml @@ -5,19 +5,31 @@ */ /** @var \Magento\Catalog\Block\Adminhtml\Category\Widget\Chooser $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_divId = 'tree' . $block->getId() ?> <div id="<?= $block->escapeHtmlAttr($_divId) ?>" class="tree"></div> <script id="ie-deferred-loader" defer="defer" src="//:"></script> -<script> +<?php +$useMassaction = /* @noEscape */ $block->getUseMassaction() ? 1 : 0; +$isAnchorOnly = /* @noEscape */ $block->getIsAnchorOnly() ? 1 : 0; +$nodeClickListener = /* @noEscape */ $block->getNodeClickListener(); +$withEmpltyNode = /* @noEscape */ ($block->getWithEmptyNode() ? 'false' : 'true'); +$isVisible = (bool) $block->getRoot()->getIsVisible(); +$categoryId = (int) $block->getCategoryId(); +$rootId = (int) $block->getRoot()->getId(); +$isWasExpanded = (int) $block->getIsWasExpanded(); +$treeJson = /* @noEscape */ $block->getTreeJson(); +$scriptString = <<<script + require(['jquery', "prototype", "extjs/ext-tree-checkbox"], function(jQuery){ -var tree<?= $block->escapeJs($block->getId()) ?>; +var tree{$block->escapeJs($block->getId())}; -var useMassaction = <?= /* @noEscape */ $block->getUseMassaction() ? 1 : 0 ?>; +var useMassaction = {$useMassaction}; -var isAnchorOnly = <?= /* @noEscape */ $block->getIsAnchorOnly() ? 1 : 0 ?>; +var isAnchorOnly = {$isAnchorOnly}; Ext.tree.TreePanel.Enhanced = function(el, config) { @@ -41,9 +53,17 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { this.setRootNode(root); if (firstLoad) { - <?php if ($block->getNodeClickListener()) : ?> - this.addListener('click', <?= /* @noEscape */ $block->getNodeClickListener() ?>.createDelegate(this)); - <?php endif; ?> + +script; +if ($block->getNodeClickListener()): + $scriptString .= <<<script + + this.addListener('click', {$nodeClickListener}.createDelegate(this)); + +script; +endif; +$scriptString .= <<<script + } this.loader.buildCategoryTree(root, data); @@ -55,10 +75,10 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { jQuery(function() { - var emptyNodeAdded = <?= /* @noEscape */ ($block->getWithEmptyNode() ? 'false' : 'true') ?>; + var emptyNodeAdded = {$withEmpltyNode}; var categoryLoader = new Ext.tree.TreeLoader({ - dataUrl: '<?= $block->escapeUrl($block->getLoadTreeUrl()) ?>' + dataUrl: '{$block->escapeJs($block->getLoadTreeUrl())}' }); categoryLoader.buildCategoryTree = function(parent, config) @@ -77,7 +97,7 @@ jQuery(function() // Add empty node to reset category filter if(!emptyNodeAdded) { var empty = Object.clone(_node); - empty.text = '<?= $block->escapeJs($block->escapeHtml(__('None'))) ?>'; + empty.text = '{$block->escapeJs($block->escapeHtml(__('None')))}'; empty.children = []; empty.id = 'none'; empty.path = '1/none'; @@ -148,39 +168,42 @@ jQuery(function() }; categoryLoader.on("beforeload", function(treeLoader, node) { - $('<?= $block->escapeJs($_divId) ?>').fire('category:beforeLoad', {treeLoader:treeLoader}); + $('{$block->escapeJs($_divId)}').fire('category:beforeLoad', {treeLoader:treeLoader}); treeLoader.baseParams.id = node.attributes.id; }); - tree<?= $block->escapeJs($block->getId()) ?> = new Ext.tree.TreePanel.Enhanced('<?= $block->escapeJs($_divId) ?>', { + tree{$block->escapeJs($block->getId())} = new Ext.tree.TreePanel.Enhanced('{$block->escapeJs($_divId)}', { animate: false, loader: categoryLoader, enableDD: false, containerScroll: true, - rootVisible: '<?= (bool) $block->getRoot()->getIsVisible() ?>', + rootVisible: '{$isVisible}', useAjax: true, - currentNodeId: <?= (int) $block->getCategoryId() ?>, + currentNodeId: {$categoryId}, addNodeTo: false }); if (useMassaction) { - tree<?= $block->escapeJs($block->getId()) ?>.on('check', function(node) { - $('<?= $block->escapeJs($_divId) ?>').fire('node:changed', {node:node}); - }, tree<?= $block->escapeJs($block->getId()) ?>); + tree{$block->escapeJs($block->getId())}.on('check', function(node) { + $('{$block->escapeJs($_divId)}').fire('node:changed', {node:node}); + }, tree{$block->escapeJs($block->getId())}); } // set the root node var parameters = { text: 'Psw', draggable: false, - id: <?= (int) $block->getRoot()->getId() ?>, - expanded: <?= (int) $block->getIsWasExpanded() ?>, - category_id: <?= (int) $block->getCategoryId() ?> + id: {$rootId}, + expanded: {$isWasExpanded}, + category_id: {$categoryId} }; - tree<?= $block->escapeJs($block->getId()) ?>.loadTree({parameters:parameters, data:<?= /* @noEscape */ $block->getTreeJson() ?>},true); + tree{$block->escapeJs($block->getId())}.loadTree({parameters:parameters, data:{$treeJson}},true); }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml b/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml index c45ef65f4f242..6dab476115cee 100644 --- a/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml +++ b/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml @@ -5,6 +5,7 @@ */ /** @var \Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab\Main\Layout $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <fieldset class="fieldset"> @@ -16,7 +17,11 @@ </div> </fieldset> <script id="ie-deferred-loader" defer="defer" src="//:"></script> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$scriptString = <<<script + require([ 'jquery', 'mage/template', @@ -30,33 +35,68 @@ require([ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id="page_group_container_<%- data.id %>">'+ '<div class="fieldset-wrapper-title">'+ '<label for="widget_instance[<%- data.id %>][page_group]">Display on <span class="required">*</span></label>'+ - '<?= $block->getDisplayOnSelectHtml() ?>'+ + '{$block->getDisplayOnSelectHtml()}'+ '<div class="actions">'+ - <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getRemoveLayoutButtonHtml()) ?> + + {$jsonHelper->jsonEncode($block->getRemoveLayoutButtonHtml())} + '</div>'+ '</div>'+ '<div class="fieldset-wrapper-content">'+ -<?php foreach ($block->getDisplayOnContainers() as $container) : ?> - '<div class="no-display <?= $block->escapeJs($container['code']) ?> group_container" id="<?= $block->escapeJs($container['name']) ?>_<%- data.id %>">'+ - '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" value="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>]" />'+ - '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][page_id]" value="<%- data.page_id %>" />'+ - '<input disabled="disabled" type="hidden" class="layout_handle_pattern" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][layout_handle]" value="<?= $block->escapeJs($container['layout_handle']) ?>" />'+ + +script; +foreach ($block->getDisplayOnContainers() as $container): + $scriptString .= <<<script + '<div class="no-display {$block->escapeJs($container['code'])} group_container" '+ + 'id="{$block->escapeJs($container['name'])}_<%- data.id %>">'+ + '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" '+ + 'value="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}]" />'+ + '<input disabled="disabled" type="hidden" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][page_id]" '+ + 'value="<%- data.page_id %>" />'+ + '<input disabled="disabled" type="hidden" class="layout_handle_pattern" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][layout_handle]" '+ + 'value="{$block->escapeJs($container['layout_handle'])}" />'+ '<table class="data-table">'+ '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label><?= $block->escapeJs(__('%1', $container['label'])) ?></label></th>'+ - '<th><label><?= $block->escapeJs(__('Container')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Template')) ?></label></th>'+ + '<th><label>{$block->escapeJs(__('%1', $container['label']))}</label></th>'+ + '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ '<tr>'+ '<td>'+ - '<input disabled="disabled" type="radio" class="radio for_all" id="all_<?= $block->escapeJs($container['name']) ?>_<%- data.id %>" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][for]" value="all" onclick="WidgetInstance.togglePageGroupChooser(this)" checked="checked" /> '+ - '<label for="all_<?= $block->escapeJs($container['name']) ?>_<%- data.id %>"><?= $block->escapeJs(__('All')) ?></label><br />'+ - '<input disabled="disabled" type="radio" class="radio for_specific" id="specific_<?= $block->escapeJs($container['name']) ?>_<%- data.id %>" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][for]" value="specific" onclick="WidgetInstance.togglePageGroupChooser(this)" /> '+ - '<label for="specific_<?= $block->escapeJs($container['name']) ?>_<%- data.id %>"><?= $block->escapeJs(__('Specific %1', $container['label'])) ?></label>'+ + '<input disabled="disabled" type="radio" class="radio for_all" '+ + 'id="all_{$block->escapeJs($container['name'])}_<%- data.id %>" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][for]" '+ + 'value="all" checked="checked" /> '+ + '<label for="all_{$block->escapeJs($container['name'])}_<%- data.id %>">'+ + '{$block->escapeJs(__('All'))}</label><br />'+ + '<input disabled="disabled" type="radio" class="radio for_specific" '+ + 'id="specific_{$block->escapeJs($container['name'])}_<%- data.id %>" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][for]" '+ + 'value="specific" /> '+ + '<label for="specific_{$block->escapeJs($container['name'])}_<%- data.id %>">'+ + '{$block->escapeJs(__('Specific %1', $container['label']))}</label>'+ + +script; + + $scriptString1 = $secureRenderer->renderEventListenerAsTag( + "onclick", + "WidgetInstance.togglePageGroupChooser(this)", + "all_" . $block->escapeJs($container['name']) . "_<%- data.id %>" + ); + $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + + $scriptString1 = $secureRenderer->renderEventListenerAsTag( + "onclick", + "WidgetInstance.togglePageGroupChooser(this)", + "specific_" . $block->escapeJs($container['name']) . "_<%- data.id %>" + ); + $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + + $scriptString .= <<<script '</td>'+ '<td>'+ '<div class="block_reference_container">'+ @@ -71,33 +111,72 @@ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id=" '</tr>'+ '</tbody>'+ '</table>'+ - '<div class="no-display chooser_container" id="<?= $block->escapeJs($container['name']) ?>_ids_<%- data.id %>">'+ - '<input disabled="disabled" type="hidden" class="is_anchor_only" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][is_anchor_only]" value="<?= $block->escapeJs($container['is_anchor_only']) ?>" />'+ - '<input disabled="disabled" type="hidden" class="product_type_id" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][product_type_id]" value="<?= $block->escapeJs($container['product_type_id']) ?>" />'+ + '<div class="no-display chooser_container" id="{$block->escapeJs($container['name'])}_ids_<%- data.id %>">'+ + '<input disabled="disabled" type="hidden" class="is_anchor_only" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][is_anchor_only]" '+ + 'value="{$block->escapeJs($container['is_anchor_only'])}" />'+ + '<input disabled="disabled" type="hidden" class="product_type_id" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][product_type_id]" '+ + 'value="{$block->escapeJs($container['product_type_id'])}" />'+ '<p>' + - '<input disabled="disabled" type="text" class="input-text entities" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][entities]" value="<%- data.<?= $block->escapeJs($container['name']) ?>_entities %>" readonly="readonly" /> ' + - '<a class="widget-option-chooser" href="javascript:void(0)" onclick="WidgetInstance.displayEntityChooser(\'<?= $block->escapeJs($container['code']) ?>\', \'<?= $block->escapeJs($container['name']) ?>_ids_<%- data.id %>\')" title="<?= $block->escapeJs(__('Open Chooser')) ?>">' + - '<img src="<?= $block->escapeUrl($block->getViewFileUrl('images/rule_chooser_trigger.gif')) ?>" alt="<?= $block->escapeJs(__('Open Chooser')) ?>" />' + + '<input disabled="disabled" type="text" class="input-text entities" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][entities]" '+ + 'value="<%- data.{$block->escapeJs($container['name'])}_entities %>" readonly="readonly" /> ' + + '<a class="widget-option-chooser" href="#" '+ + 'title="{$block->escapeJs(__('Open Chooser'))}">' + + '<img src="{$block->escapeJs($block->getViewFileUrl('images/rule_chooser_trigger.gif'))}" '+ + 'alt="{$block->escapeJs(__('Open Chooser'))}" />' + '</a> ' + - '<a href="javascript:void(0)" onclick="WidgetInstance.hideEntityChooser(\'<?= $block->escapeJs($container['name']) ?>_ids_<%- data.id %>\')" title="<?= $block->escapeJs(__('Apply')) ?>">' + - '<img src="<?= $block->escapeUrl($block->getViewFileUrl('images/rule_component_apply.gif')) ?>" alt="<?= $block->escapeJs(__('Apply')) ?>" />' + + '<a id="widget-apply-<%- data.id %>" href="#" '+ + 'title="{$block->escapeJs(__('Apply'))}">' + + '<img src="{$block->escapeJs($block->getViewFileUrl('images/rule_component_apply.gif'))}" '+ + 'alt="{$block->escapeJs(__('Apply'))}" />' + '</a>' + '</p>'+ '<div class="chooser"></div>'+ '</div>'+ + +script; + + $scriptString1 = $secureRenderer->renderEventListenerAsTag( + "onclick", + "event.preventDefault(); + WidgetInstance.displayEntityChooser('" .$block->escapeJs($container['code']) . + "', '" . $block->escapeJs($container['name']) . "_ids_<%- data.id %>')", + "div#" . $block->escapeJs($container['name']) . "_ids_<%- data.id %> a.widget-option-chooser" + ); + $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + + $scriptString1 = $secureRenderer->renderEventListenerAsTag( + 'onclick', + "event.preventDefault(); + WidgetInstance.hideEntityChooser('" . $block->escapeJs($container['name']) . "_ids_<%- data.id %>')", + "a#widget-apply-<%- data.id %>" + ); + $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + $scriptString .= <<<script + '</div>'+ -<?php endforeach; ?> + +script; +endforeach; +$scriptString .= <<<script + '<div class="no-display all_pages group_container" id="all_pages_<%- data.id %>">'+ - '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" value="widget_instance[<%- data.id %>][all_pages]" />'+ - '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][all_pages][page_id]" value="<%- data.page_id %>" />'+ - '<input disabled="disabled" type="hidden" class="layout_handle_pattern" name="widget_instance[<%- data.id %>][all_pages][layout_handle]" value="default" />'+ - '<input disabled="disabled" type="hidden" class="for_all" name="widget_instance[<%- data.id %>][all_pages][for]" value="all" />'+ + '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" '+ + 'value="widget_instance[<%- data.id %>][all_pages]" />'+ + '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][all_pages][page_id]" '+ + 'value="<%- data.page_id %>" />'+ + '<input disabled="disabled" type="hidden" class="layout_handle_pattern" '+ + 'name="widget_instance[<%- data.id %>][all_pages][layout_handle]" value="default" />'+ + '<input disabled="disabled" type="hidden" class="for_all" name="widget_instance[<%- data.id %>][all_pages][for]" '+ + 'value="all" />'+ '<table class="data-table">'+ '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label><?= $block->escapeJs(__('Container')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Template')) ?></label></th>'+ + '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ '<th> </th>'+ '</tr>'+ '</thead>'+ @@ -119,21 +198,24 @@ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id=" '</table>'+ '</div>'+ '<div class="no-display ignore-validate pages group_container" id="pages_<%- data.id %>">'+ - '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" value="widget_instance[<%- data.id %>][pages]" />'+ - '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][pages][page_id]" value="<%- data.page_id %>" />'+ - '<input disabled="disabled" type="hidden" class="for_all" name="widget_instance[<%- data.id %>][pages][for]" value="all" />'+ + '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" '+ + 'value="widget_instance[<%- data.id %>][pages]" />'+ + '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][pages][page_id]" '+ + 'value="<%- data.page_id %>" />'+ + '<input disabled="disabled" type="hidden" class="for_all" name="widget_instance[<%- data.id %>][pages][for]" '+ + 'value="all" />'+ '<table class="data-table">'+ '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label><?= $block->escapeJs(__('Page')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Container')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Template')) ?></label></th>'+ + '<th><label>{$block->escapeJs(__('Page'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ '<tr>'+ - '<td><?= /* @noEscape */ $block->getLayoutsChooser() ?></td>'+ + '<td>{$block->getLayoutsChooser()}</td>'+ '<td>'+ '<div class="block_reference_container">'+ '<div class="block_reference"></div>'+ @@ -150,21 +232,24 @@ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id=" '</table>'+ '</div>'+ '<div class="no-display ignore-validate pages group_container" id="page_layouts_<%- data.id %>">'+ - '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" value="widget_instance[<%- data.id %>][page_layouts]" />'+ - '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][page_layouts][page_id]" value="<%- data.page_id %>" />'+ - '<input disabled="disabled" type="hidden" class="for_all" name="widget_instance[<%- data.id %>][page_layouts][for]" value="all" />'+ + '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" '+ + 'value="widget_instance[<%- data.id %>][page_layouts]" />'+ + '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][page_layouts][page_id]" '+ + 'value="<%- data.page_id %>" />'+ + '<input disabled="disabled" type="hidden" class="for_all" '+ + 'name="widget_instance[<%- data.id %>][page_layouts][for]" value="all" />'+ '<table class="data-table">'+ '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label><?= $block->escapeJs(__('Page')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Container')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Template')) ?></label></th>'+ + '<th><label>{$block->escapeJs(__('Page'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ '<tr>'+ - '<td><?= /* @noEscape */ $block->getPageLayoutsPageChooser() ?></td>'+ + '<td>{$block->getPageLayoutsPageChooser()}</td>'+ '<td>'+ '<div class="block_reference_container">'+ '<div class="block_reference"></div>'+ @@ -189,7 +274,7 @@ var WidgetInstance = { pageGroupTemplate : pageGroupTemplate, pageGroupContainerId : 'page_group_container', count : 0, - activePageGroups : $H({}), + activePageGroups : \$H({}), selectedItems : {}, addPageGroup : function(data) { @@ -266,7 +351,7 @@ var WidgetInstance = { }, addProductItemToSelection: function(groupId, item) { if (undefined == this.selectedItems[groupId]) { - this.selectedItems[groupId] = $H({}); + this.selectedItems[groupId] = \$H({}); } if (!isNaN(parseInt(item))) { this.selectedItems[groupId].set(item, 1); @@ -327,11 +412,11 @@ var WidgetInstance = { additional = {}; } if (type == 'categories') { - additional.url = '<?= $block->escapeUrl($block->getCategoriesChooserUrl()) ?>'; - additional.post_parameters = $H({'is_anchor_only':$(chooser).down('input.is_anchor_only').value}); + additional.url = '{$block->escapeJs($block->getCategoriesChooserUrl())}'; + additional.post_parameters = \$H({'is_anchor_only':$(chooser).down('input.is_anchor_only').value}); } else if (type == 'products') { - additional.url = '<?= $block->escapeUrl($block->getProductsChooserUrl()) ?>'; - additional.post_parameters = $H({'product_type_id':$(chooser).down('input.product_type_id').value}); + additional.url = '{$block->escapeUrl($block->getProductsChooserUrl())}'; + additional.post_parameters = \$H({'product_type_id':$(chooser).down('input.product_type_id').value}); } if (chooser && additional) { this.displayChooser(chooser, additional); @@ -347,7 +432,7 @@ var WidgetInstance = { displayChooser : function(chooser, additional) { chooser = $(chooser).down('div.chooser'); entities = chooser.up('div.chooser_container').down('input[type="text"].entities').value; - postParameters = $H({selected:entities}); + postParameters = \$H({selected:entities}); url = ''; if (additional) { if (additional.url) url = additional.url; @@ -436,13 +521,13 @@ var WidgetInstance = { selected = ''; parameters = {}; if (type == 'block_reference') { - url = '<?= $block->escapeUrl($block->getBlockChooserUrl()) ?>'; + url = '{$block->escapeJs($block->getBlockChooserUrl())}'; if (additional.selectedBlock) { selected = additional.selectedBlock; } parameters.layout = value; } else if (type == 'block_template') { - url = '<?= $block->escapeUrl($block->getTemplateChooserUrl()) ?>'; + url = '{$block->escapeJs($block->getTemplateChooserUrl())}'; if (additional.selectedTemplate) { selected = additional.selectedTemplate; } @@ -479,9 +564,18 @@ var WidgetInstance = { window.WidgetInstance = WidgetInstance; jQuery(function(){ - <?php foreach ($block->getPageGroups() as $pageGroup) : ?> - WidgetInstance.addPageGroup(<?= /* @noEscape */ $pageGroup ?>); - <?php endforeach; ?> + +script; +foreach ($block->getPageGroups() as $pageGroup): + $scriptString .= <<<script + + WidgetInstance.addPageGroup({$pageGroup}); + +script; +endforeach; + +$scriptString .= <<<script + Event.observe(document, 'product:changed', function(event){ WidgetInstance.checkProduct(event); }); @@ -497,4 +591,7 @@ jQuery(function(){ //]]> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Widget/view/adminhtml/templates/instance/js.phtml b/app/code/Magento/Widget/view/adminhtml/templates/instance/js.phtml index 90aee2baeb4f3..cb82c1dbca75a 100644 --- a/app/code/Magento/Widget/view/adminhtml/templates/instance/js.phtml +++ b/app/code/Magento/Widget/view/adminhtml/templates/instance/js.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script + require([ 'jquery', 'mage/template', @@ -27,4 +30,7 @@ setSettings = function(urlTemplate, codeElement, themeElement) { setLocation(url); }; }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Wishlist/Block/AbstractBlock.php b/app/code/Magento/Wishlist/Block/AbstractBlock.php index 981a0da1d241f..5f4a7c8f3814b 100644 --- a/app/code/Magento/Wishlist/Block/AbstractBlock.php +++ b/app/code/Magento/Wishlist/Block/AbstractBlock.php @@ -231,7 +231,7 @@ public function hasDescription($item) * Retrieve formatted Date * * @param string $date - * @deprecated + * @deprecated 101.1.1 * @return string */ public function getFormatedDate($date) diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist.php index d02f2229401c1..bc94f53a7625a 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist.php @@ -31,6 +31,7 @@ class Wishlist extends \Magento\Wishlist\Block\AbstractBlock /** * @var \Magento\Wishlist\Model\ResourceModel\Item\Collection + * @since 101.1.1 */ protected $_collection; @@ -101,6 +102,7 @@ private function paginateCollection() * Retrieve Wishlist Product Items collection * * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + * @since 101.1.1 */ public function getWishlistItems() { diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php index 40882ae00dae1..db92a10907d39 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php @@ -13,7 +13,7 @@ * Model for item column in customer wishlist. * * @api - * @deprecated + * @deprecated 101.1.2 * @since 100.0.2 */ class Actions extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php index 53f67626e956d..57d182dee4e1c 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php @@ -13,7 +13,7 @@ * Wishlist block customer item cart column. * * @api - * @deprecated + * @deprecated 101.1.2 * @since 100.0.2 */ class Comment extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php index c4c786961694b..f41146051ae96 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php @@ -13,7 +13,7 @@ * Edit item in customer wishlist table. * * @api - * @deprecated + * @deprecated 101.1.2 * @since 100.0.2 */ class Edit extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Image.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Image.php index 5595d189b15eb..c578e9d1c5d22 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Image.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Image.php @@ -53,6 +53,7 @@ public function __construct( * Identify the product from which thumbnail should be taken. * * @return \Magento\Catalog\Model\Product + * @since 101.0.5 */ public function getProductForThumbnail(\Magento\Wishlist\Model\Item $item) : \Magento\Catalog\Model\Product { diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php index b7eaf53fc23b5..092ede9ceb016 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php @@ -13,7 +13,7 @@ * Wishlist block customer item cart column. * * @api - * @deprecated + * @deprecated 101.1.2 * @since 100.0.2 */ class Info extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php index 09f5014edead6..472cd3cc70f09 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php @@ -13,7 +13,7 @@ * Delete item column in customer wishlist table * * @api - * @deprecated + * @deprecated 101.1.2 * @since 100.0.2 */ class Remove extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Link.php b/app/code/Magento/Wishlist/Block/Link.php index 2d78852f0fd32..c410a1254edee 100644 --- a/app/code/Magento/Wishlist/Block/Link.php +++ b/app/code/Magento/Wishlist/Block/Link.php @@ -75,7 +75,7 @@ public function getLabel() /** * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function getSortOrder() { diff --git a/app/code/Magento/Wishlist/Block/Rss/Link.php b/app/code/Magento/Wishlist/Block/Rss/Link.php index 3e716c863862b..28affa8d3372d 100644 --- a/app/code/Magento/Wishlist/Block/Rss/Link.php +++ b/app/code/Magento/Wishlist/Block/Rss/Link.php @@ -44,6 +44,7 @@ public function __construct( \Magento\Framework\Url\EncoderInterface $urlEncoder, array $data = [] ) { + $data['wishlistHelper'] = $this->wishlistHelper; parent::__construct($context, $data); $this->wishlistHelper = $wishlistHelper; $this->rssUrlBuilder = $rssUrlBuilder; @@ -51,6 +52,8 @@ public function __construct( } /** + * Return link. + * * @return string */ public function getLink() @@ -72,6 +75,8 @@ public function isRssAllowed() } /** + * Return link params. + * * @return array */ protected function getLinkParams() diff --git a/app/code/Magento/Wishlist/Block/Share/Email/Items.php b/app/code/Magento/Wishlist/Block/Share/Email/Items.php index 130c7cb136afb..077f8ce3c4930 100644 --- a/app/code/Magento/Wishlist/Block/Share/Email/Items.php +++ b/app/code/Magento/Wishlist/Block/Share/Email/Items.php @@ -59,6 +59,7 @@ public function __construct( * @param Item $item * * @return Product + * @since 101.2.0 */ public function getProductForThumbnail(Item $item): Product { diff --git a/app/code/Magento/Wishlist/Controller/Index/Cart.php b/app/code/Magento/Wishlist/Controller/Index/Cart.php index 023d0756bae6f..7e47a6b9d7d8a 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Cart.php +++ b/app/code/Magento/Wishlist/Controller/Index/Cart.php @@ -220,12 +220,13 @@ public function execute() $wishlist->save(); if (!$this->cart->getQuote()->getHasError()) { - $message = __( - 'You added %1 to your shopping cart.', - $this->escaper->escapeHtml($item->getProduct()->getName()) + $this->messageManager->addComplexSuccessMessage( + 'addCartSuccessMessage', + [ + 'product_name' => $item->getProduct()->getName(), + 'cart_url' => $this->cartHelper->getCartUrl() + ] ); - $this->messageManager->addSuccessMessage($message); - $productsToAdd = [ [ 'sku' => $item->getProduct()->getSku(), 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 38f100602972a..939cbe3a2c46f 100644 --- a/app/code/Magento/Wishlist/Controller/Shared/Cart.php +++ b/app/code/Magento/Wishlist/Controller/Shared/Cart.php @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Wishlist\Controller\Shared; use Magento\Catalog\Model\Product\Exception as ProductException; use Magento\Checkout\Helper\Cart as CartHelper; use Magento\Checkout\Model\Cart as CustomerCart; +use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context as ActionContext; -use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Redirect as ResultRedirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Escaper; use Magento\Framework\Exception\LocalizedException; @@ -23,7 +27,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Cart extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface +class Cart extends Action implements HttpPostActionInterface { /** * @var CustomerCart @@ -80,7 +84,7 @@ public function __construct( * If Product has required options - redirect * to product view page with message about needed defined required options * - * @return \Magento\Framework\Controller\Result\Redirect + * @return Redirect */ public function execute() { @@ -120,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 \Magento\Framework\Controller\Result\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/Adminhtml/ResourceModel/Item/Product/CollectionBuilder.php b/app/code/Magento/Wishlist/Model/Adminhtml/ResourceModel/Item/Product/CollectionBuilder.php new file mode 100644 index 0000000000000..aa54c17c243b0 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Adminhtml/ResourceModel/Item/Product/CollectionBuilder.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Adminhtml\ResourceModel\Item\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Wishlist\Model\ResourceModel\Item\Collection as WishlistItemCollection; +use Magento\Wishlist\Model\ResourceModel\Item\Product\CollectionBuilderInterface; + +/** + * Wishlist items products collection builder for adminhtml area + */ +class CollectionBuilder implements CollectionBuilderInterface +{ + /** + * @inheritDoc + */ + public function build(WishlistItemCollection $wishlistItemCollection, Collection $productCollection): Collection + { + return $productCollection; + } +} diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php index 61d444f786ca8..ce1a1cdc2942d 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php @@ -7,10 +7,11 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; -use Magento\CatalogInventory\Model\Stock; +use Magento\CatalogInventory\Model\ResourceModel\StockStatusFilterInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Sales\Model\ConfigInterface; +use Magento\Wishlist\Model\ResourceModel\Item\Product\CollectionBuilderInterface; /** * Wishlist item collection @@ -157,6 +158,21 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * @var ConfigInterface */ private $salesConfig; + /** + * @var CollectionBuilderInterface + */ + private $productCollectionBuilder; + /** + * @var StockStatusFilterInterface + */ + private $stockStatusFilter; + + /** + * Whether product table is joined in select + * + * @var bool + */ + private $isProductTableJoined = false; /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory @@ -176,10 +192,11 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * @param \Magento\Catalog\Model\Entity\AttributeFactory $catalogAttrFactory * @param \Magento\Wishlist\Model\ResourceModel\Item $resource * @param \Magento\Framework\App\State $appState - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * @param TableMaintainer|null $tableMaintainer - * @param ConfigInterface|null $salesConfig - * + * @param ConfigInterface|null $salesConfig + * @param CollectionBuilderInterface|null $productCollectionBuilder + * @param StockStatusFilterInterface|null $stockStatusFilter * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -202,7 +219,9 @@ public function __construct( \Magento\Framework\App\State $appState, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, TableMaintainer $tableMaintainer = null, - ConfigInterface $salesConfig = null + ConfigInterface $salesConfig = null, + ?CollectionBuilderInterface $productCollectionBuilder = null, + ?StockStatusFilterInterface $stockStatusFilter = null ) { $this->stockConfiguration = $stockConfiguration; $this->_adminhtmlSales = $adminhtmlSales; @@ -219,6 +238,10 @@ public function __construct( parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $connection, $resource); $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); $this->salesConfig = $salesConfig ?: ObjectManager::getInstance()->get(ConfigInterface::class); + $this->productCollectionBuilder = $productCollectionBuilder + ?: ObjectManager::getInstance()->get(CollectionBuilderInterface::class); + $this->stockStatusFilter = $stockStatusFilter + ?: ObjectManager::getInstance()->get(StockStatusFilterInterface::class); } /** @@ -309,12 +332,10 @@ protected function _assignProducts() $productCollection->setVisibility($this->_productVisibility->getVisibleInSiteIds()); } - $productCollection->addPriceData() - ->addTaxPercents() - ->addIdFilter($this->_productIds) - ->addAttributeToSelect($this->_wishlistConfig->getProductAttributes()) - ->addOptionsToResult() - ->addUrlRewrite(); + $productCollection->addIdFilter($this->_productIds) + ->addAttributeToSelect($this->_wishlistConfig->getProductAttributes()); + + $productCollection = $this->productCollectionBuilder->build($this, $productCollection); if ($this->_productSalable) { $productCollection = $this->_adminhtmlSales->applySalableProductTypesFilter($productCollection); @@ -352,6 +373,7 @@ protected function _assignProducts() /** * @inheritdoc + * @since 101.1.3 */ protected function _renderFiltersBefore() { @@ -361,15 +383,8 @@ protected function _renderFiltersBefore() $connection = $this->getConnection(); if ($this->_productInStock && !$this->stockConfiguration->isShowOutOfStock()) { - $inStockConditions = [ - "stockItem.product_id = {$mainTableName}.product_id", - $connection->quoteInto('stockItem.stock_status = ?', Stock::STOCK_IN_STOCK), - ]; - $this->getSelect()->join( - ['stockItem' => $this->getTable('cataloginventory_stock_status')], - join(' AND ', $inStockConditions), - [] - ); + $this->joinProductTable(); + $this->stockStatusFilter->execute($this->getSelect(), 'product_entity', 'stockItem'); } if ($this->_productVisible) { @@ -391,7 +406,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 + ), [] ); } @@ -575,10 +594,12 @@ protected function _joinProductNameTable() $storeId = $this->_storeManager->getStore(\Magento\Store\Model\Store::ADMIN_CODE)->getId(); $entityMetadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); + $linkField = $entityMetadata->getLinkField(); + $this->joinProductTable(); $this->getSelect()->join( ['product_name_table' => $attribute->getBackendTable()], - 'product_name_table.' . $entityMetadata->getLinkField() . ' = main_table.product_id' . + 'product_name_table.' . $linkField . ' = product_entity.' . $linkField . ' AND product_name_table.store_id = ' . $storeId . ' AND product_name_table.attribute_id = ' . @@ -588,6 +609,7 @@ protected function _joinProductNameTable() $this->_isProductNameJoined = true; } + return $this; } @@ -660,4 +682,21 @@ protected function _afterLoadData() return $this; } + + /** + * Join product table to select if not already joined + * + * @return void + */ + private function joinProductTable(): void + { + if (!$this->isProductTableJoined) { + $this->getSelect()->join( + ['product_entity' => $this->getTable('catalog_product_entity')], + 'product_entity.entity_id = main_table.product_id', + [] + ); + $this->isProductTableJoined = true; + } + } } diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilder.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilder.php new file mode 100644 index 0000000000000..05255d1fe39f7 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilder.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\ResourceModel\Item\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Wishlist\Model\ResourceModel\Item\Collection as WishlistItemCollection; + +/** + * Wishlist items products collection builder + */ +class CollectionBuilder implements CollectionBuilderInterface +{ + /** + * @inheritDoc + */ + public function build(WishlistItemCollection $wishlistItemCollection, Collection $productCollection): Collection + { + return $productCollection->addPriceData() + ->addTaxPercents() + ->addOptionsToResult() + ->addUrlRewrite(); + } +} diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilderInterface.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilderInterface.php new file mode 100644 index 0000000000000..1984d92e08a60 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilderInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\ResourceModel\Item\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Wishlist\Model\ResourceModel\Item\Collection as WishlistItemCollection; + +/** + * Wishlist items products collection builder + */ +interface CollectionBuilderInterface +{ + /** + * Modify product collection + * + * @param WishlistItemCollection $wishlistItemCollection + * @param Collection $productCollection + * @return Collection + */ + public function build(WishlistItemCollection $wishlistItemCollection, Collection $productCollection): Collection; +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist.php b/app/code/Magento/Wishlist/Model/Wishlist.php index 9b7ff5177afae..f544dd374d734 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist.php @@ -12,9 +12,8 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductFactory; -use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Api\StockRegistryInterface; -use Magento\CatalogInventory\Model\Configuration; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; @@ -27,7 +26,6 @@ use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\DateTime; -use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Wishlist\Helper\Data; @@ -150,14 +148,9 @@ class Wishlist extends AbstractModel implements IdentityInterface private $serializer; /** - * @var ScopeConfigInterface + * @var StockConfigurationInterface */ - private $scopeConfig; - - /** - * @var StockRegistryInterface|null - */ - private $stockRegistry; + private $stockConfiguration; /** * Constructor @@ -181,7 +174,9 @@ class Wishlist extends AbstractModel implements IdentityInterface * @param Json|null $serializer * @param StockRegistryInterface|null $stockRegistry * @param ScopeConfigInterface|null $scopeConfig + * @param StockConfigurationInterface|null $stockConfiguration * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Context $context, @@ -202,7 +197,8 @@ public function __construct( array $data = [], Json $serializer = null, StockRegistryInterface $stockRegistry = null, - ScopeConfigInterface $scopeConfig = null + ScopeConfigInterface $scopeConfig = null, + ?StockConfigurationInterface $stockConfiguration = null ) { $this->_useCurrentWebsite = $useCurrentWebsite; $this->_catalogProduct = $catalogProduct; @@ -217,8 +213,8 @@ public function __construct( $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->productRepository = $productRepository; - $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); - $this->stockRegistry = $stockRegistry ?: ObjectManager::getInstance()->get(StockRegistryInterface::class); + $this->stockConfiguration = $stockConfiguration + ?: ObjectManager::getInstance()->get(StockConfigurationInterface::class); } /** @@ -226,6 +222,7 @@ public function __construct( * * @param int $customerId * @param bool $create Create wishlist if don't exists + * * @return $this */ public function loadByCustomerId($customerId, $create = false) @@ -274,6 +271,7 @@ public function generateSharingCode() * Load by sharing code * * @param string $code + * * @return $this */ public function loadByCode($code) @@ -370,6 +368,7 @@ protected function _addCatalogProduct(Product $product, $qty = 1, $forciblySetQt * Retrieve wishlist item collection * * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + * * @throws NoSuchEntityException */ public function getItemCollection() @@ -379,7 +378,7 @@ public function getItemCollection() $this )->addStoreFilter( $this->getSharedStoreIds() - )->setVisibilityFilter(); + )->setVisibilityFilter($this->_useCurrentWebsite); } return $this->_itemCollection; @@ -389,6 +388,7 @@ public function getItemCollection() * Retrieve wishlist item collection * * @param int $itemId + * * @return false|Item */ public function getItem($itemId) @@ -403,7 +403,9 @@ public function getItem($itemId) * Adding item to wishlist * * @param Item $item + * * @return $this + * * @throws Exception */ public function addItem(Item $item) @@ -424,9 +426,12 @@ public function addItem(Item $item) * @param int|Product $product * @param DataObject|array|string|null $buyRequest * @param bool $forciblySetQty + * * @return Item|string + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * * @throws LocalizedException * @throws InvalidArgumentException */ @@ -457,7 +462,7 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false throw new LocalizedException(__('Cannot specify product.')); } - if ($this->isInStock($productId)) { + if (!$this->stockConfiguration->isShowOutOfStock($storeId) && !$product->getIsSalable()) { throw new LocalizedException(__('Cannot add product without stock to wishlist.')); } @@ -465,6 +470,7 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false $_buyRequest = $buyRequest; } elseif (is_string($buyRequest)) { $isInvalidItemConfiguration = false; + $buyRequestData = []; try { $buyRequestData = $this->serializer->unserialize($buyRequest); if (!is_array($buyRequestData)) { @@ -482,6 +488,9 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false } else { $_buyRequest = new DataObject(); } + if ($_buyRequest->getData('action') !== 'updateItem') { + $_buyRequest->setData('action', 'add'); + } /* @var $product Product */ $cartCandidates = $product->getTypeInstance()->processConfiguration($_buyRequest, clone $product); @@ -502,6 +511,7 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false $errors = []; $items = []; + $item = null; foreach ($cartCandidates as $candidate) { if ($candidate->getParentProductId()) { @@ -529,7 +539,9 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false * Set customer id * * @param int $customerId + * * @return $this + * * @throws LocalizedException */ public function setCustomerId($customerId) @@ -541,6 +553,7 @@ public function setCustomerId($customerId) * Retrieve customer id * * @return int + * * @throws LocalizedException */ public function getCustomerId() @@ -552,6 +565,7 @@ public function getCustomerId() * Retrieve data for save * * @return array + * * @throws LocalizedException */ public function getDataForSave() @@ -567,6 +581,7 @@ public function getDataForSave() * Retrieve shared store ids for current website or all stores if $current is false * * @return array + * * @throws NoSuchEntityException */ public function getSharedStoreIds() @@ -590,6 +605,7 @@ public function getSharedStoreIds() * Set shared store ids * * @param array $storeIds + * * @return $this */ public function setSharedStoreIds($storeIds) @@ -602,6 +618,7 @@ public function setSharedStoreIds($storeIds) * Retrieve wishlist store object * * @return \Magento\Store\Model\Store + * * @throws NoSuchEntityException */ public function getStore() @@ -616,6 +633,7 @@ public function getStore() * Set wishlist store * * @param Store $store + * * @return $this */ public function setStore($store) @@ -649,29 +667,13 @@ public function isSalable() return false; } - /** - * Retrieve if product has stock or config is set for showing out of stock products - * - * @param int $productId - * @return bool - */ - private function isInStock($productId) - { - /** @var StockItemInterface $stockItem */ - $stockItem = $this->stockRegistry->getStockItem($productId); - $showOutOfStock = $this->scopeConfig->isSetFlag( - Configuration::XML_PATH_SHOW_OUT_OF_STOCK, - ScopeInterface::SCOPE_STORE - ); - $isInStock = $stockItem ? $stockItem->getIsInStock() : false; - return !$isInStock && !$showOutOfStock; - } - /** * Check customer is owner this wishlist * * @param int $customerId + * * @return bool + * * @throws LocalizedException */ public function isOwner($customerId) @@ -696,10 +698,13 @@ public function isOwner($customerId) * @param int|Item $itemId * @param DataObject $buyRequest * @param null|array|DataObject $params + * * @return $this + * * @throws LocalizedException * * @see \Magento\Catalog\Helper\Product::addParamsToBuyRequest() + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -726,15 +731,16 @@ public function updateItem($itemId, $buyRequest, $params = null) } $params->setCurrentConfig($item->getBuyRequest()); $buyRequest = $this->_catalogProduct->addParamsToBuyRequest($buyRequest, $params); + $buyRequest->setData('action', 'updateItem'); $product->setWishlistStoreId($item->getStoreId()); $items = $this->getItemCollection(); $isForceSetQuantity = true; - foreach ($items as $_item) { - /* @var $_item Item */ - if ($_item->getProductId() == $product->getId() && $_item->representProduct( - $product - ) && $_item->getId() != $item->getId() + foreach ($items as $wishlistItem) { + /* @var $wishlistItem Item */ + if ($wishlistItem->getProductId() == $product->getId() + && $wishlistItem->getId() != $item->getId() + && $wishlistItem->representProduct($product) ) { // We do not add new wishlist item, but updating the existing one $isForceSetQuantity = false; @@ -748,10 +754,11 @@ public function updateItem($itemId, $buyRequest, $params = null) throw new LocalizedException(__($resultItem)); } + if ($resultItem->getDescription() != $item->getDescription()) { + $resultItem->setDescription($item->getDescription())->save(); + } + if ($resultItem->getId() != $itemId) { - if ($resultItem->getDescription() != $item->getDescription()) { - $resultItem->setDescription($item->getDescription())->save(); - } $item->isDeleted(true); $this->setDataChanges(true); } else { diff --git a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php new file mode 100644 index 0000000000000..7acfb503a5ad0 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php @@ -0,0 +1,164 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Adding products to wishlist + */ +class AddProductsToWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'; + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var BuyRequestBuilder + */ + private $buyRequestBuilder; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @param ProductRepositoryInterface $productRepository + * @param BuyRequestBuilder $buyRequestBuilder + * @param WishlistResourceModel $wishlistResource + */ + public function __construct( + ProductRepositoryInterface $productRepository, + BuyRequestBuilder $buyRequestBuilder, + WishlistResourceModel $wishlistResource + ) { + $this->productRepository = $productRepository; + $this->buyRequestBuilder = $buyRequestBuilder; + $this->wishlistResource = $wishlistResource; + } + + /** + * Adding products to wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItems + * + * @return WishlistOutput + * + * @throws AlreadyExistsException + */ + public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutput + { + foreach ($wishlistItems as $wishlistItem) { + $this->addItemToWishlist($wishlist, $wishlistItem); + } + + $wishlistOutput = $this->prepareOutput($wishlist); + + if ($wishlist->isObjectNew() || count($wishlistOutput->getErrors()) !== count($wishlistItems)) { + $this->wishlistResource->save($wishlist); + } + + return $wishlistOutput; + } + + /** + * Add product item to wishlist + * + * @param Wishlist $wishlist + * @param WishlistItem $wishlistItem + * + * @return void + */ + private function addItemToWishlist(Wishlist $wishlist, WishlistItem $wishlistItem): void + { + $sku = $wishlistItem->getParentSku() ?? $wishlistItem->getSku(); + + try { + $product = $this->productRepository->get($sku, false, null, true); + } catch (NoSuchEntityException $e) { + $this->addError( + __('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(), + self::ERROR_PRODUCT_NOT_FOUND + ); + + return; + } + + try { + $options = $this->buyRequestBuilder->build($wishlistItem, (int) $product->getId()); + $result = $wishlist->addNewItem($product, $options); + + if (is_string($result)) { + $this->addError($result); + } + } catch (LocalizedException $exception) { + $this->addError($exception->getMessage()); + } catch (\Throwable $e) { + $this->addError( + __( + 'Could not add the product with SKU "%sku" to the wishlist:: %message', + ['sku' => $sku, 'message' => $e->getMessage()] + )->render() + ); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php new file mode 100644 index 0000000000000..1cfa316c3cd01 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for bundle product buy requests + */ +class BundleDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'bundle'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItem, ?int $productId): array + { + $bundleOptionsData = []; + + foreach ($wishlistItem->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $optionId, $optionValueId, $optionQuantity] = $optionData; + + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } + + return $bundleOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php new file mode 100644 index 0000000000000..1f7ddce345b1c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Building buy request for all product types + */ +class BuyRequestBuilder +{ + /** + * @var BuyRequestDataProviderInterface[] + */ + private $providers; + + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + + /** + * @param DataObjectFactory $dataObjectFactory + * @param array $providers + */ + public function __construct( + DataObjectFactory $dataObjectFactory, + array $providers = [] + ) { + $this->dataObjectFactory = $dataObjectFactory; + $this->providers = $providers; + } + + /** + * Build product buy request for adding to wishlist + * + * @param WishlistItem $wishlistItemData + * @param int|null $productId + * + * @return DataObject + */ + public function build(WishlistItem $wishlistItemData, ?int $productId = null): DataObject + { + $requestData = [ + [ + 'qty' => $wishlistItemData->getQuantity(), + ] + ]; + + foreach ($this->providers as $provider) { + $requestData[] = $provider->execute($wishlistItemData, $productId); + } + + return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php new file mode 100644 index 0000000000000..fac45d7f86c7c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Build buy request for adding products to wishlist + */ +interface BuyRequestDataProviderInterface +{ + /** + * Provide buy request data from add to wishlist item request + * + * @param WishlistItem $wishlistItemData + * @param int|null $productId + * + * @return array + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array; +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php new file mode 100644 index 0000000000000..8bf12206336a8 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for custom options buy requests + */ +class CustomizableOptionDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'custom-option'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $customizableOptionsData = []; + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [$optionType, $optionId, $optionValue] = $optionData; + + if ($optionType == self::PROVIDER_OPTION_TYPE) { + $customizableOptionsData[$optionId][] = $optionValue; + } + } + + foreach ($wishlistItemData->getEnteredOptions() as $option) { + $optionData = \explode('/', base64_decode($option->getUid())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [$optionType, $optionId] = $optionData; + + if ($optionType == self::PROVIDER_OPTION_TYPE) { + $customizableOptionsData[$optionId][] = $option->getValue(); + } + } + + if (empty($customizableOptionsData)) { + return $customizableOptionsData; + } + + $result = ['options' => $this->flattenOptionValues($customizableOptionsData)]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Flatten option values for non-multiselect customizable options + * + * @param array $customizableOptionsData + * + * @return array + */ + private function flattenOptionValues(array $customizableOptionsData): array + { + foreach ($customizableOptionsData as $optionId => $optionValue) { + if (count($optionValue) === 1) { + $customizableOptionsData[$optionId] = $optionValue[0]; + } + } + + return $customizableOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php new file mode 100644 index 0000000000000..1ad1a0b64706a --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for downloadable product buy requests + */ +class DownloadableLinkDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'downloadable'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItem, ?int $productId): array + { + $linksData = []; + + foreach ($wishlistItem->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $linkId] = $optionData; + + $linksData[] = $linkId; + } + + return $linksData ? ['links' => $linksData] : []; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php new file mode 100644 index 0000000000000..01e29bcf80c0b --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for configurable product buy requests + */ +class SuperAttributeDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'configurable'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $configurableData = []; + + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $attributeId, $valueIndex] = $optionData; + + $configurableData[$attributeId] = $valueIndex; + } + + if (empty($configurableData)) { + return $configurableData; + } + + $result = ['super_attribute' => $configurableData]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php new file mode 100644 index 0000000000000..a11f631f1f5aa --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for grouped product buy requests + */ +class SuperGroupDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'grouped'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $groupedData = []; + + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $simpleProductId, $quantity] = $optionData; + + $groupedData[$simpleProductId] = $quantity; + } + + if (empty($groupedData)) { + return $groupedData; + } + + $result = ['super_group' => $groupedData]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Config.php b/app/code/Magento/Wishlist/Model/Wishlist/Config.php new file mode 100644 index 0000000000000..041e9f1ca0a21 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Config.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Provides wishlist configuration + */ +class Config +{ + const XML_PATH_WISHLIST_ACTIVE = 'wishlist/general/active'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check whether the wishlist is enabled or not + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_WISHLIST_ACTIVE, + ScopeInterface::SCOPE_STORES + ); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php new file mode 100644 index 0000000000000..edbf84781da38 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents entered options + */ +class EnteredOption +{ + /** + * @var string + */ + private $uid; + + /** + * @var string + */ + private $value; + + /** + * @param string $uid + * @param string $value + */ + public function __construct(string $uid, string $value) + { + $this->uid = $uid; + $this->value = $value; + } + + /** + * Get entered option id + * + * @return string + */ + public function getUid(): string + { + return $this->uid; + } + + /** + * Get entered option value + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php new file mode 100644 index 0000000000000..cb8420169fa8a --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents error item + */ +class Error +{ + /** + * @var string + */ + private $message; + + /** + * @var string + */ + private $code; + + /** + * @param string $message + * @param string $code + */ + public function __construct(string $message, string $code) + { + $this->message = $message; + $this->code = $code; + } + + /** + * Get error message + * + * @return string + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * Get error code + * + * @return string + */ + public function getCode(): string + { + return $this->code; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php new file mode 100644 index 0000000000000..129a61c0a2a6c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents selected option + */ +class SelectedOption +{ + /** + * @var string + */ + private $id; + + /** + * @param string $id + */ + public function __construct(string $id) + { + $this->id = $id; + } + + /** + * Get selected option id + * + * @return string + */ + public function getId(): string + { + return $this->id; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php new file mode 100644 index 0000000000000..236b7f1eee72d --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents Wishlist Item data + */ +class WishlistItem +{ + /** + * @var float + */ + private $quantity; + + /** + * @var string|null + */ + private $sku; + + /** + * @var string + */ + private $parentSku; + + /** + * @var int|null + */ + private $id; + + /** + * @var string|null + */ + private $description; + + /** + * @var SelectedOption[] + */ + private $selectedOptions; + + /** + * @var EnteredOption[] + */ + private $enteredOptions; + + /** + * @param float $quantity + * @param string|null $sku + * @param string|null $parentSku + * @param int|null $id + * @param string|null $description + * @param array|null $selectedOptions + * @param array|null $enteredOptions + */ + public function __construct( + float $quantity, + string $sku = null, + string $parentSku = null, + int $id = null, + string $description = null, + array $selectedOptions = null, + array $enteredOptions = null + ) { + $this->quantity = $quantity; + $this->sku = $sku; + $this->parentSku = $parentSku; + $this->id = $id; + $this->description = $description; + $this->selectedOptions = $selectedOptions; + $this->enteredOptions = $enteredOptions; + } + + /** + * Get wishlist item id + * + * @return int|null + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * Get wishlist item description + * + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * Get sku + * + * @return string|null + */ + public function getSku(): ?string + { + return $this->sku; + } + + /** + * Get quantity + * + * @return float + */ + public function getQuantity(): float + { + return $this->quantity; + } + + /** + * Get parent sku + * + * @return string|null + */ + public function getParentSku(): ?string + { + return $this->parentSku; + } + + /** + * Get selected options + * + * @return SelectedOption[]|null + */ + public function getSelectedOptions(): ?array + { + return $this->selectedOptions; + } + + /** + * Get entered options + * + * @return EnteredOption[]|null + */ + public function getEnteredOptions(): ?array + { + return $this->enteredOptions; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php new file mode 100644 index 0000000000000..622f072e8d668 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +use Magento\Framework\Exception\InputException; + +/** + * Create WishlistItem DTO + */ +class WishlistItemFactory +{ + /** + * Create wishlist item DTO + * + * @param array $data + * + * @return WishlistItem + */ + public function create(array $data): WishlistItem + { + return new WishlistItem( + $data['quantity'] ?? 0, + $data['sku'] ?? null, + $data['parent_sku'] ?? null, + isset($data['wishlist_item_id']) ? (int) $data['wishlist_item_id'] : null, + $data['description'] ?? null, + isset($data['selected_options']) ? $this->createSelectedOptions($data['selected_options']) : [], + isset($data['entered_options']) ? $this->createEnteredOptions($data['entered_options']) : [] + ); + } + + /** + * Create array of Entered Options + * + * @param array $options + * + * @return EnteredOption[] + */ + private function createEnteredOptions(array $options): array + { + return \array_map( + function (array $option) { + if (!isset($option['uid'], $option['value'])) { + throw new InputException( + __('Required fields are not present EnteredOption.uid, EnteredOption.value') + ); + } + return new EnteredOption($option['uid'], $option['value']); + }, + $options + ); + } + + /** + * Create array of Selected Options + * + * @param string[] $options + * + * @return SelectedOption[] + */ + private function createSelectedOptions(array $options): array + { + return \array_map( + function ($option) { + return new SelectedOption($option); + }, + $options + ); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php new file mode 100644 index 0000000000000..fc7db9ec910fb --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +use Magento\Wishlist\Model\Wishlist; + +/** + * DTO represent output for \Magento\WishlistGraphQl\Model\Resolver\AddProductsToWishlistResolver + */ +class WishlistOutput +{ + /** + * @var Wishlist + */ + private $wishlist; + + /** + * @var Error[] + */ + private $errors; + + /** + * @param Wishlist $wishlist + * @param Error[] $errors + */ + public function __construct(Wishlist $wishlist, array $errors) + { + $this->wishlist = $wishlist; + $this->errors = $errors; + } + + /** + * Get Wishlist + * + * @return Wishlist + */ + public function getWishlist(): Wishlist + { + return $this->wishlist; + } + + /** + * Get errors happened during adding products to wishlist + * + * @return Error[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php new file mode 100644 index 0000000000000..d143830064752 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Wishlist\Model\Item as WishlistItem; +use Magento\Wishlist\Model\ItemFactory as WishlistItemFactory; +use Magento\Wishlist\Model\ResourceModel\Item as WishlistItemResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Remove product items from wishlist + */ +class RemoveProductsFromWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'; + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var WishlistItemFactory + */ + private $wishlistItemFactory; + + /** + * @var WishlistItemResource + */ + private $wishlistItemResource; + + /** + * @param WishlistItemFactory $wishlistItemFactory + * @param WishlistItemResource $wishlistItemResource + */ + public function __construct( + WishlistItemFactory $wishlistItemFactory, + WishlistItemResource $wishlistItemResource + ) { + $this->wishlistItemFactory = $wishlistItemFactory; + $this->wishlistItemResource = $wishlistItemResource; + } + + /** + * Removing items from wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItemsIds + * + * @return WishlistOutput + */ + public function execute(Wishlist $wishlist, array $wishlistItemsIds): WishlistOutput + { + foreach ($wishlistItemsIds as $wishlistItemId) { + $this->removeItemFromWishlist((int) $wishlistItemId); + } + + return $this->prepareOutput($wishlist); + } + + /** + * Remove product item from wishlist + * + * @param int $wishlistItemId + * + * @return void + */ + private function removeItemFromWishlist(int $wishlistItemId): void + { + try { + /** @var WishlistItem $wishlistItem */ + $wishlistItem = $this->wishlistItemFactory->create(); + $this->wishlistItemResource->load($wishlistItem, $wishlistItemId); + if (!$wishlistItem->getId()) { + $this->addError( + __('Could not find a wishlist item with ID "%id"', ['id' => $wishlistItemId])->render(), + self::ERROR_PRODUCT_NOT_FOUND + ); + } + + $this->wishlistItemResource->delete($wishlistItem); + } catch (\Exception $e) { + $this->addError( + __( + 'We can\'t delete the item with ID "%id" from the Wish List right now.', + ['id' => $wishlistItemId] + )->render() + ); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php new file mode 100644 index 0000000000000..4abcada138362 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Wishlist\Model\Item as WishlistItem; +use Magento\Wishlist\Model\ItemFactory as WishlistItemFactory; +use Magento\Wishlist\Model\ResourceModel\Item as WishlistItemResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem as WishlistItemData; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Updating product items in wishlist + */ +class UpdateProductsInWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var BuyRequestBuilder + */ + private $buyRequestBuilder; + + /** + * @var WishlistItemFactory + */ + private $wishlistItemFactory; + + /** + * @var WishlistItemResource + */ + private $wishlistItemResource; + + /** + * @param BuyRequestBuilder $buyRequestBuilder + * @param WishlistItemFactory $wishlistItemFactory + * @param WishlistItemResource $wishlistItemResource + */ + public function __construct( + BuyRequestBuilder $buyRequestBuilder, + WishlistItemFactory $wishlistItemFactory, + WishlistItemResource $wishlistItemResource + ) { + $this->buyRequestBuilder = $buyRequestBuilder; + $this->wishlistItemFactory = $wishlistItemFactory; + $this->wishlistItemResource = $wishlistItemResource; + } + + /** + * Adding products to wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItems + * + * @return WishlistOutput + */ + public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutput + { + foreach ($wishlistItems as $wishlistItem) { + $this->updateItemInWishlist($wishlist, $wishlistItem); + } + + return $this->prepareOutput($wishlist); + } + + /** + * Update product item in wishlist + * + * @param Wishlist $wishlist + * @param WishlistItemData $wishlistItemData + * + * @return void + */ + private function updateItemInWishlist(Wishlist $wishlist, WishlistItemData $wishlistItemData): void + { + try { + $options = $this->buyRequestBuilder->build($wishlistItemData); + /** @var WishlistItem $wishlistItem */ + $wishlistItem = $this->wishlistItemFactory->create(); + $this->wishlistItemResource->load($wishlistItem, $wishlistItemData->getId()); + $wishlistItem->setDescription($wishlistItemData->getDescription()); + $resultItem = $wishlist->updateItem($wishlistItem, $options); + + if (is_string($resultItem)) { + $this->addError($resultItem); + } + } catch (LocalizedException $exception) { + $this->addError($exception->getMessage()); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Observer/AddToCart.php b/app/code/Magento/Wishlist/Observer/AddToCart.php index 1ab24d87efbf7..e31a8993670c6 100644 --- a/app/code/Magento/Wishlist/Observer/AddToCart.php +++ b/app/code/Magento/Wishlist/Observer/AddToCart.php @@ -15,7 +15,7 @@ /** * Class AddToCart - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @package Magento\Wishlist\Observer */ class AddToCart implements ObserverInterface diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/AssertStorefrontWishListInvalidEmailsMessageActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/AssertStorefrontWishListInvalidEmailsMessageActionGroup.xml new file mode 100644 index 0000000000000..bdb5e702132dc --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/AssertStorefrontWishListInvalidEmailsMessageActionGroup.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="AssertStorefrontWishListInvalidEmailsMessageActionGroup"> + <arguments> + <argument name="message" type="string"/> + </arguments> + <see userInput="{{message}}" selector="{{StorefrontCustomerWishlistShareSection.errorEmailMessage}}" stepKey="successMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToCartFromWishlistUsingSidebarActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToCartFromWishlistUsingSidebarActionGroup.xml index a28a80c57fb67..512ffdd9ba442 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToCartFromWishlistUsingSidebarActionGroup.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToCartFromWishlistUsingSidebarActionGroup.xml @@ -19,5 +19,6 @@ <click selector="{{StorefrontCustomerWishlistSidebarSection.ProductAddToCartByName(product.name)}}" stepKey="AddProductToCartFromWishlistUsingSidebarClickAddToCartFromWishlist"/> <waitForElement selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="AddProductToCartFromWishlistUsingSidebarWaitForSuccessMessage"/> <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="AddProductToCartFromWishlistUsingSidebarSeeProductNameAddedToCartFromWishlist"/> + <seeLink userInput="shopping cart" stepKey="seeLinkInSuccessMsg"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerShareWishlistActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerShareWishlistActionGroup.xml index 1f7ac9fc85f50..57404f54a64b2 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerShareWishlistActionGroup.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerShareWishlistActionGroup.xml @@ -8,7 +8,8 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="StorefrontCustomerShareWishlistActionGroup"> + <actionGroup name="StorefrontCustomerShareWishlistActionGroup" deprecated="Use StorefrontShareCustomerWishlistActionGroup"> + <!-- Deprecated due to Hardcoded WishList Data Using. 18-19 lines --> <annotations> <description>Shares the Wish List from the Storefront Wish List page. PLEASE NOTE: The details for sharing are Hardcoded using 'Wishlist'.</description> </annotations> diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontShareCustomerWishlistActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontShareCustomerWishlistActionGroup.xml new file mode 100644 index 0000000000000..6cabeeac1242f --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontShareCustomerWishlistActionGroup.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="StorefrontShareCustomerWishlistActionGroup"> + <arguments> + <argument name="email" type="string"/> + <argument name="message" type="string"/> + </arguments> + + <click selector="{{StorefrontCustomerWishlistProductSection.productShareWishList}}" stepKey="clickMyWishListButton"/> + <fillField userInput="{{email}}" selector="{{StorefrontCustomerWishlistShareSection.ProductShareWishlistEmail}}" stepKey="fillEmailsForShare"/> + <fillField userInput="{{message}}" selector="{{StorefrontCustomerWishlistShareSection.ProductShareWishlistTextMessage}}" stepKey="fillShareMessage"/> + <click selector="{{StorefrontCustomerWishlistShareSection.ProductShareWishlistButton}}" stepKey="sendWishlist"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml b/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml index 4a25a8d449dd3..63b864f682455 100755 --- a/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml @@ -18,4 +18,15 @@ <data key="min_email_text_length_limit">1</data> <data key="max_email_text_length_limit">10000</data> </entity> + <entity name="notValidEmails" type="wishlist"> + <data key="id">null</data> + <var key="product" entityType="product" entityKey="id"/> + <var key="customer_email" entityType="customer" entityKey="email"/> + <var key="customer_password" entityType="customer" entityKey="password"/> + <data key="shareInfo_emails">JohnDoe123456789@,JohnDoe987654321example.com,JohnDoe123456abc@@example.com</data> + <data key="shareInfo_message">Sharing message.</data> + <data key="default_email_text_length_limit">255</data> + <data key="min_email_text_length_limit">1</data> + <data key="max_email_text_length_limit">10000</data> + </entity> </entities> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml index 76b99ba56a327..3f16133be96a9 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml @@ -12,5 +12,6 @@ <element name="ProductShareWishlistEmail" type="input" selector="#email_address"/> <element name="ProductShareWishlistTextMessage" type="input" selector="#message"/> <element name="ProductShareWishlistButton" type="button" selector=".action.submit.primary" timeout="30"/> + <element name="errorEmailMessage" type="input" selector="#email_address-error"/> </section> </sections> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml new file mode 100644 index 0000000000000..a2219d5145f17 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.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="AdminDeleteCustomerWishlistItemTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Wishlist items deleting"/> + <title value="Admin deletes an item from customer wishlist"/> + <description value="Admin Should be able delete items from customer wishlist"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-35170"/> + <group value="wishlist"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> + <argument name="category" value="$createCategory$"/> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogoutBeforeCheck"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="navigateToCustomerEditPage"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="AdminNavigateCustomerWishlistTabActionGroup" stepKey="navigateToWishlistTab"/> + <actionGroup ref="AdminCustomerFindWishlistItemActionGroup" stepKey="findWishlistItem"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AdminCustomerDeleteWishlistItemActionGroup" stepKey="deleteItem"/> + <actionGroup ref="AssertAdminCustomerNoItemsInWishlistActionGroup" stepKey="assertNoItems"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginOnStoreFront"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="NavigateThroughCustomerTabsActionGroup" stepKey="navigateToWishlist"> + <argument name="navigationItemName" value="My Wish List"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCustomerWishlistIsEmptyActionGroup" stepKey="assertNoItemsInWishlist"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml index e3c4baf8aa813..6a0603fcee502 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml @@ -34,8 +34,7 @@ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfPresent"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> <argument name="keyword" value="_defaultProduct.name"/> @@ -46,8 +45,7 @@ <argument name="image" value="MagentoLogo"/> </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex1"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex1"/> <click selector="{{AdminProductGridSection.selectRowBasedOnName(colorProductAttribute1.name)}}" stepKey="selectProductToAddImage1"/> <waitForPageLoad stepKey="waitForProductEditPageLoad1"/> <actionGroup ref="AddProductImageActionGroup" stepKey="addImageForChildProduct"> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml index eed4dc8d4767e..aafd8b0b0d4d3 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml @@ -45,7 +45,7 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearWebsitesGridFilter"/> <!--Clear products filter--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsFilters"/> <!--Logout everywhere--> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> @@ -67,7 +67,7 @@ <click selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="acceptStoreSwitchingForProduct1"/> <click selector="{{AdminProductFormSection.visibilityUseDefault}}" stepKey="uncheckVisibilityUseDefaultValueForProduct1"/> <selectOption userInput="Not Visible Individually" selector="{{AdminProductFormSection.visibility}}" stepKey="makeProductNotVisibleOnSecondaryStoreView"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveEditedProductForProduct1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveEditedProductForProduct1"/> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct2"> <argument name="product" value="$$secondProduct$$"/> </actionGroup> @@ -82,7 +82,7 @@ <click selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="acceptStoreSwitchingForProduct2"/> <click selector="{{AdminProductFormSection.visibilityUseDefault}}" stepKey="uncheckVisibilityUseDefaultValueForProduct2"/> <selectOption userInput="Not Visible Individually" selector="{{AdminProductFormSection.visibility}}" stepKey="makeProductNotVisibleOnDefaultStoreView"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveEditedProductForProduct2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveEditedProductForProduct2"/> <!-- Sign in as customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml index a6cab4b9f9715..c279adbfe876c 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml @@ -26,16 +26,24 @@ <requiredEntity createDataKey="categorySecond"/> </createData> <createData entity="Simple_US_Customer" stepKey="customer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="categoryFirst" stepKey="deleteCategoryFirst"/> <deleteData createDataKey="categorySecond" stepKey="deleteCategorySecond"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <!-- Sign in as customer --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml index a23788d2c508f..31bc9f6a31de7 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml @@ -45,8 +45,12 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml index 4ad87095ecd30..da2cec8284c46 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml @@ -105,8 +105,12 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct3"/> </createData> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDisabledCustomerWishlistFunctionalityTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDisabledCustomerWishlistFunctionalityTest.xml new file mode 100644 index 0000000000000..65cce1dcfc1c3 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDisabledCustomerWishlistFunctionalityTest.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="StorefrontDisabledCustomerWishlistFunctionalityTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Disabled Wishlist Functionality"/> + <title value="Wishlist Functionality is disabled in system configurations and not visible on FE"/> + <description value="Customer should not see wishlist functionality if it's disabled"/> + <testCaseId value="MC-35200"/> + <severity value="AVERAGE"/> + <group value="wishlist"/> + <group value="configuration"/> + </annotations> + <before> + <magentoCLI command="config:set wishlist/general/active 0" stepKey="disableWishlist"/> + <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <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> + <magentoCLI command="config:set wishlist/general/active 1" stepKey="enableWishlist"/> + <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup" stepKey="assertItemIsNotPresent"> + <argument name="itemName" value="My Wish List"/> + </actionGroup> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup" stepKey="assertButtonIsAbsent"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml index a5081ca2ad338..05a42314ddb71 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml @@ -107,8 +107,12 @@ <requiredEntity createDataKey="createConfigChildProduct3"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml index 97551a596e978..b2364b72f7db8 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml @@ -36,8 +36,12 @@ </after> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Sign in as customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.xml index 329978462c107..c6b6dc6886f96 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.xml @@ -48,6 +48,12 @@ <argument name="productVar" value="$createProduct$"/> </actionGroup> - <actionGroup ref="StorefrontCustomerShareWishlistActionGroup" stepKey="shareWishlist"/> + <actionGroup ref="StorefrontShareCustomerWishlistActionGroup" stepKey="shareWishlist"> + <argument name="email" value="{{Wishlist.shareInfo_emails}}"/> + <argument name="message" value="{{Wishlist.shareInfo_message}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontCustomerMessagesActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="Your wish list has been shared."/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest.xml new file mode 100644 index 0000000000000..281272293e6a9 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Sharing wishlist with more than Maximum Allowed Emails qty"/> + <title value="Sharing wishlist with more than Maximum Allowed Emails qty"/> + <description value="Customer should not have a possibility share wishlist with more than maximum allowed emails qty"/> + <testCaseId value="MC-35167"/> + <severity value="AVERAGE"/> + <group value="wishlist"/> + <group value="configuration"/> + </annotations> + <before> + <magentoCLI command="config:set wishlist/email/number_limit 1" stepKey="changeEmailsQtyLimit"/> + <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <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> + <magentoCLI command="config:set wishlist/email/number_limit 10" stepKey="returnDefaultValue"/> + <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontShareCustomerWishlistActionGroup" stepKey="shareWishList"> + <argument name="email" value="{{Wishlist.shareInfo_emails}}"/> + <argument name="message" value="{{Wishlist.shareInfo_message}}"/> + </actionGroup> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertMessage"> + <argument name="message" value="Maximum of 1 emails can be sent."/> + <argument name="messageType" value="error"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedTextLengthLimitTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedTextLengthLimitTest.xml new file mode 100644 index 0000000000000..65e67f75eb7e8 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedTextLengthLimitTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontShareWishlistWithMoreThanMaximumAllowedTextLengthLimitTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Sharing wishlist with more than Maximum Allowed Text Length Limit"/> + <title value="Sharing wishlist with more than Maximum Allowed Text Length Limit"/> + <description value="Customer should not have a possibility share wishlist with more than maximum allowed Email Text Length Limit"/> + <testCaseId value="MC-35647"/> + <severity value="AVERAGE"/> + <group value="wishlist"/> + <group value="configuration"/> + </annotations> + <before> + <magentoCLI command="config:set wishlist/email/text_limit 10" stepKey="changeTextLengthLimit"/> + <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <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> + <magentoCLI command="config:set wishlist/email/text_limit 255" stepKey="returnDefaultValue"/> + <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontShareCustomerWishlistActionGroup" stepKey="shareWishList"> + <argument name="email" value="{{Wishlist.shareInfo_emails}}"/> + <argument name="message" value="{{Wishlist.shareInfo_message}}"/> + </actionGroup> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertMessage"> + <argument name="message" value="Message length must not exceed 10 symbols"/> + <argument name="messageType" value="error"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml new file mode 100644 index 0000000000000..0438a1b58e771 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.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="StorefrontShareWishlistWithNotValidEmailAddressTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Customer Wishlist"/> + <title value="Customer is not able to share wishlist with invalid email addresses"/> + <description value="Customer is not able to share wishlist with invalid email addresses"/> + <severity value="AVERAGE"/> + <group value="wishlist"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> + <argument name="category" value="$createCategory$"/> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontShareCustomerWishlistActionGroup" stepKey="shareWishList"> + <argument name="email" value="{{notValidEmails.shareInfo_emails}}"/> + <argument name="message" value="{{notValidEmails.shareInfo_message}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontWishListInvalidEmailsMessageActionGroup" stepKey="assertErrorMessage"> + <argument name="message" value="Please enter valid email addresses, separated by commas. For example, johndoe@domain.com, johnsmith@domain.com."/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml index 08698658588ae..86d09783e0f55 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml @@ -27,8 +27,12 @@ </before> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml index 72f5bab1e6af5..f5958f5efd414 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml @@ -28,11 +28,15 @@ <executeJS function="return window.location.host" stepKey="hostname"/> <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php index 58d888d8ccb6e..47bb930cde3b9 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php @@ -10,8 +10,8 @@ use Magento\Catalog\Helper\Product as ProductHelper; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Exception as ProductException; -use Magento\Checkout\Model\Cart as CheckoutCart; use Magento\Checkout\Helper\Cart as CartHelper; +use Magento\Checkout\Model\Cart as CheckoutCart; use Magento\Framework\App\Action\Context; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Response\RedirectInterface; @@ -620,14 +620,8 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) ->method('getName') ->willReturn($productName); - $this->escaperMock->expects($this->once()) - ->method('escapeHtml') - ->with($productName, null) - ->willReturn($productName); - $this->messageManagerMock->expects($this->once()) - ->method('addSuccessMessage') - ->with('You added ' . $productName . ' to your shopping cart.', null) + ->method('addComplexSuccessMessage') ->willReturnSelf(); $this->cartHelperMock->expects($this->once()) 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/Wishlist/Test/Unit/Model/ResourceModel/Item/CollectionTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/ResourceModel/Item/CollectionTest.php index 72705acb8cd06..28705d54e6e20 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/ResourceModel/Item/CollectionTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/ResourceModel/Item/CollectionTest.php @@ -54,7 +54,8 @@ class CollectionTest extends TestCase /** @var string */ protected $sql = "SELECT `main_table`.* FROM `testMainTableName` AS `main_table` - INNER JOIN `testBackendTableName` AS `product_name_table` ON product_name_table.entity_id = main_table.product_id + INNER JOIN `testEntityTableName` AS `product_entity` ON product_entity.entity_id = main_table.product_id + INNER JOIN `testBackendTableName` AS `product_name_table` ON product_name_table.entity_id = product_entity.entity_id AND product_name_table.store_id = 1 AND product_name_table.attribute_id = 12 WHERE (INSTR(product_name_table.value, 'TestProductName'))"; @@ -90,18 +91,13 @@ protected function setUp(): void ->expects($this->any()) ->method('getConnection') ->willReturn($connection); - $resource - ->expects($this->any()) - ->method('getMainTable') - ->willReturn('testMainTableName'); - $resource - ->expects($this->any()) - ->method('getTableName') - ->willReturn('testMainTableName'); $resource ->expects($this->any()) ->method('getTable') - ->willReturn('testMainTableName'); + ->willReturnOnConsecutiveCalls( + 'testMainTableName', + 'testEntityTableName' + ); $catalogConfFactory = $this->createPartialMock( ConfigFactory::class, diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php index 19bfb3598f0e3..369f77e527287 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php @@ -13,6 +13,7 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Type\AbstractType; use Magento\Catalog\Model\ProductFactory; +use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Model\Stock\Item as StockItem; use Magento\CatalogInventory\Model\Stock\StockItemRepository; @@ -132,6 +133,10 @@ class WishlistTest extends TestCase * @var StockRegistryInterface|MockObject */ private $stockRegistry; + /** + * @var StockConfigurationInterface|MockObject + */ + private $stockConfiguration; protected function setUp(): void { @@ -194,6 +199,8 @@ protected function setUp(): void ->method('getEventDispatcher') ->willReturn($this->eventDispatcher); + $this->stockConfiguration = $this->createMock(StockConfigurationInterface::class); + $this->wishlist = new Wishlist( $context, $this->registry, @@ -213,7 +220,8 @@ protected function setUp(): void [], $this->serializer, $this->stockRegistry, - $this->scopeConfig + $this->scopeConfig, + $this->stockConfiguration ); } @@ -243,34 +251,22 @@ public function testLoadByCustomerId() /** * @param int|Item|MockObject $itemId - * @param DataObject $buyRequest + * @param DataObject|MockObject $buyRequest * @param null|array|DataObject $param * @throws LocalizedException * * @dataProvider updateItemDataProvider */ - public function testUpdateItem($itemId, $buyRequest, $param) + public function testUpdateItem($itemId, $buyRequest, $param): void { $storeId = 1; $productId = 1; $stores = [(new DataObject())->setId($storeId)]; - $newItem = $this->getMockBuilder(Item::class) - ->setMethods( - ['setProductId', 'setWishlistId', 'setStoreId', 'setOptions', 'setProduct', 'setQty', 'getItem', 'save'] - ) - ->disableOriginalConstructor() - ->getMock(); - $newItem->expects($this->any())->method('setProductId')->willReturnSelf(); - $newItem->expects($this->any())->method('setWishlistId')->willReturnSelf(); - $newItem->expects($this->any())->method('setStoreId')->willReturnSelf(); - $newItem->expects($this->any())->method('setOptions')->willReturnSelf(); - $newItem->expects($this->any())->method('setProduct')->willReturnSelf(); - $newItem->expects($this->any())->method('setQty')->willReturnSelf(); - $newItem->expects($this->any())->method('getItem')->willReturn(2); - $newItem->expects($this->any())->method('save')->willReturnSelf(); + $newItem = $this->prepareWishlistItem(); $this->itemFactory->expects($this->once())->method('create')->willReturn($newItem); + $this->productHelper->expects($this->once())->method('addParamsToBuyRequest')->willReturn($buyRequest); $this->storeManager->expects($this->any())->method('getStores')->willReturn($stores); $this->storeManager->expects($this->any())->method('getStore')->willReturn($stores[0]); @@ -312,6 +308,7 @@ public function testUpdateItem($itemId, $buyRequest, $param) $newProduct->expects($this->once()) ->method('getTypeInstance') ->willReturn($instanceType); + $newProduct->expects($this->any())->method('getIsSalable')->willReturn(true); $item = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() @@ -355,18 +352,64 @@ public function testUpdateItem($itemId, $buyRequest, $param) ); } + /** + * Prepare wishlist item mock. + * + * @return MockObject + */ + private function prepareWishlistItem(): MockObject + { + $newItem = $this->getMockBuilder(Item::class) + ->setMethods( + ['setProductId', 'setWishlistId', 'setStoreId', 'setOptions', 'setProduct', 'setQty', 'getItem', 'save'] + ) + ->disableOriginalConstructor() + ->getMock(); + $newItem->expects($this->any())->method('setProductId')->willReturnSelf(); + $newItem->expects($this->any())->method('setWishlistId')->willReturnSelf(); + $newItem->expects($this->any())->method('setStoreId')->willReturnSelf(); + $newItem->expects($this->any())->method('setOptions')->willReturnSelf(); + $newItem->expects($this->any())->method('setProduct')->willReturnSelf(); + $newItem->expects($this->any())->method('setQty')->willReturnSelf(); + $newItem->expects($this->any())->method('getItem')->willReturn(2); + $newItem->expects($this->any())->method('save')->willReturnSelf(); + + return $newItem; + } + /** * @return array */ - public function updateItemDataProvider() + public function updateItemDataProvider(): array { + $dataObjectMock = $this->createMock(DataObject::class); + $dataObjectMock->expects($this->once()) + ->method('setData') + ->with('action', 'updateItem') + ->willReturnSelf(); + $dataObjectMock->expects($this->once()) + ->method('getData') + ->with('action') + ->willReturn('updateItem'); + return [ - '0' => [1, new DataObject(), null] + '0' => [1, $dataObjectMock, null] ]; } - public function testAddNewItem() + /** + * @param bool $getIsSalable + * @param bool $isShowOutOfStock + * @param string $throwException + * + * @dataProvider addNewItemDataProvider + */ + public function testAddNewItem(bool $getIsSalable, bool $isShowOutOfStock, string $throwException): void { + if ($throwException) { + $this->expectExceptionMessage($throwException); + } + $this->stockConfiguration->method('isShowOutOfStock')->willReturn($isShowOutOfStock); $productId = 1; $storeId = 1; $buyRequest = json_encode( @@ -384,34 +427,31 @@ public function testAddNewItem() $instanceType = $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() ->getMock(); - $instanceType->expects($this->once()) - ->method('processConfiguration') + $instanceType->method('processConfiguration') ->willReturn('product'); $productMock = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getId', 'hasWishlistStoreId', 'getStoreId', 'getTypeInstance']) + ->setMethods(['getId', 'hasWishlistStoreId', 'getStoreId', 'getTypeInstance', 'getIsSalable']) ->getMock(); - $productMock->expects($this->once()) - ->method('getId') + $productMock->method('getId') ->willReturn($productId); - $productMock->expects($this->once()) - ->method('hasWishlistStoreId') + $productMock->method('hasWishlistStoreId') ->willReturn(false); - $productMock->expects($this->once()) - ->method('getStoreId') + $productMock->method('getStoreId') ->willReturn($storeId); - $productMock->expects($this->once()) - ->method('getTypeInstance') + $productMock->method('getTypeInstance') ->willReturn($instanceType); + $productMock->expects($this->any()) + ->method('getIsSalable') + ->willReturn($getIsSalable); $this->productRepository->expects($this->once()) ->method('getById') ->with($productId, false, $storeId) ->willReturn($productMock); - $this->serializer->expects($this->once()) - ->method('unserialize') + $this->serializer->method('unserialize') ->willReturnCallback( function ($value) { return json_decode($value, true); @@ -430,4 +470,17 @@ function ($value) { $this->assertEquals($result, $this->wishlist->addNewItem($productMock, $buyRequest)); } + + /** + * @return array[] + */ + public function addNewItemDataProvider(): array + { + return [ + [false, false, 'Cannot add product without stock to wishlist'], + [false, true, ''], + [true, false, ''], + [true, true, ''], + ]; + } } diff --git a/app/code/Magento/Wishlist/etc/adminhtml/di.xml b/app/code/Magento/Wishlist/etc/adminhtml/di.xml index 124b8c17c3f36..be4d1966a46e9 100644 --- a/app/code/Magento/Wishlist/etc/adminhtml/di.xml +++ b/app/code/Magento/Wishlist/etc/adminhtml/di.xml @@ -24,4 +24,6 @@ <type name="Magento\Catalog\Model\ResourceModel\Product"> <plugin name="cleanups_wishlist_item_after_product_delete" type="Magento\Wishlist\Plugin\Model\ResourceModel\Product" /> </type> + <preference for="Magento\Wishlist\Model\ResourceModel\Item\Product\CollectionBuilderInterface" + type="Magento\Wishlist\Model\Adminhtml\ResourceModel\Item\Product\CollectionBuilder"/> </config> diff --git a/app/code/Magento/Wishlist/etc/di.xml b/app/code/Magento/Wishlist/etc/di.xml index c0230a5326f40..924bdfa3eb584 100644 --- a/app/code/Magento/Wishlist/etc/di.xml +++ b/app/code/Magento/Wishlist/etc/di.xml @@ -78,4 +78,6 @@ </argument> </arguments> </type> + <preference for="Magento\Wishlist\Model\ResourceModel\Item\Product\CollectionBuilderInterface" + type="Magento\Wishlist\Model\ResourceModel\Item\Product\CollectionBuilder"/> </config> diff --git a/app/code/Magento/Wishlist/etc/graphql/di.xml b/app/code/Magento/Wishlist/etc/graphql/di.xml new file mode 100644 index 0000000000000..9726376bf30be --- /dev/null +++ b/app/code/Magento/Wishlist/etc/graphql/di.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="super_attribute" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\SuperAttributeDataProvider</item> + <item name="customizable_option" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\CustomizableOptionDataProvider</item> + <item name="bundle" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\BundleDataProvider</item> + <item name="downloadable" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\DownloadableLinkDataProvider</item> + <item name="grouped" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\SuperGroupDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml b/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml index e364087405ed9..0ee4233029105 100644 --- a/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml +++ b/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml @@ -99,6 +99,7 @@ <item name="caption" xsi:type="string" translate="true">Delete</item> <item name="url" xsi:type="string">#</item> <item name="onclick" xsi:type="string">return wishlistControl.removeItem($wishlist_item_id);</item> + <item name="class" xsi:type="string">wishlist-remove-button</item> </item> </argument> </arguments> diff --git a/app/code/Magento/Wishlist/view/adminhtml/templates/customer/edit/tab/wishlist.phtml b/app/code/Magento/Wishlist/view/adminhtml/templates/customer/edit/tab/wishlist.phtml index e6c0608f4b450..7ee04bf192f29 100644 --- a/app/code/Magento/Wishlist/view/adminhtml/templates/customer/edit/tab/wishlist.phtml +++ b/app/code/Magento/Wishlist/view/adminhtml/templates/customer/edit/tab/wishlist.phtml @@ -5,8 +5,10 @@ */ /** @var \Magento\Framework\View\Element\Template $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script + require([ "Magento_Ui/js/modal/confirm", "prototype", @@ -19,13 +21,14 @@ if (!urlParams) { urlParams = ''; } - var url = <?= $block->escapeJs($block->escapeUrl($block->getJsObjectName())) ?>.url + '?ajax=true' + urlParams; + var url = {$block->escapeJs($block->getJsObjectName())}.url + '?ajax=true' + urlParams; new Ajax.Updater( - <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>.containerId, + {$block->escapeJs($block->getJsObjectName())}.containerId, url, { parameters: {form_key: FORM_KEY}, - onComplete: <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>.initGrid.bind(<?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>), + onComplete: {$block->escapeJs($block->getJsObjectName())}.initGrid + .bind({$block->escapeJs($block->getJsObjectName())}), evalScripts:true } ); @@ -48,7 +51,7 @@ var self = this; confirm({ - content: '<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to remove this item?'))) ?>', + content: '{$block->escapeJs(__('Are you sure you want to remove this item?'))}', actions: { confirm: function () { self.reload('&delete=' + itemId); @@ -61,11 +64,14 @@ productConfigure.addListType( 'wishlist', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/wishlist_product_composite_wishlist/configure'))) ?>', - urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/wishlist_product_composite_wishlist/update'))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('customer/wishlist_product_composite_wishlist/configure'))}', + urlConfirm: '{$block->escapeJs($block->getUrl('customer/wishlist_product_composite_wishlist/update'))}' } ); //--> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/rss/email.phtml b/app/code/Magento/Wishlist/view/frontend/templates/rss/email.phtml index 4a97173fde318..f221e50608ca4 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/rss/email.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/rss/email.phtml @@ -5,11 +5,19 @@ */ /* @var \Magento\Wishlist\Block\Rss\EmailLink $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +/** @var \Magento\Wishlist\Helper\Data $wishlistHelper */ +$wishlistHelper = $block->getData('wishlistHelper'); ?> -<?php if ($block->getLink()) : ?> -<p style="font-size:12px; line-height:16px; margin:0 0 16px;"> - <?= $block->escapeHtml(__("RSS link to %1's wishlist", $this->helper(\Magento\Wishlist\Helper\Data::class)->getCustomerName())) ?> +<?php if ($block->getLink()): ?> +<p id="wishlist-rss-email-link"> + <?= $block->escapeHtml(__("RSS link to %1's wishlist", $wishlistHelper->getCustomerName())) ?> <br /> <a href="<?= $block->escapeUrl($block->getLink()) ?>"><?= $block->escapeUrl($block->getLink()) ?></a> </p> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "font-size:12px; line-height:16px; margin:0 0 16px;", + 'p#wishlist-rss-email-link' + ) ?> <?php endif; ?> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/shared.phtml b/app/code/Magento/Wishlist/view/frontend/templates/shared.phtml index 0fcaa6c853ff0..0d3158abb0532 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/shared.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/shared.phtml @@ -5,10 +5,12 @@ */ /** @var \Magento\Wishlist\Block\Share\Wishlist $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->hasWishlistItems()) : ?> - <form class="form shared wishlist" action="<?= $block->escapeUrl($block->getUrl('wishlist/index/update')) ?>" method="post"> +<?php if ($block->hasWishlistItems()): ?> + <form class="form shared wishlist" action="<?= $block->escapeUrl($block->getUrl('wishlist/index/update')) ?>" + method="post"> <div class="wishlist table-wrapper"> <table class="table data wishlist" id="wishlist-table"> <caption class="table-caption"><?= $block->escapeHtml(__('Wish List')) ?></caption> @@ -20,14 +22,15 @@ </tr> </thead> <tbody> - <?php foreach ($block->getWishlistItems() as $item) : ?> + <?php foreach ($block->getWishlistItems() as $item): ?> <?php $product = $item->getProduct(); $isVisibleProduct = $product->isVisibleInSiteVisibility(); ?> <tr> <td data-th="<?= $block->escapeHtmlAttr(__('Product')) ?>" class="col product"> - <a class="product photo" href="<?= $block->escapeUrl($block->getProductUrl($item)) ?>" title="<?= $block->escapeHtmlAttr($product->getName()) ?>"> + <a class="product photo" href="<?= $block->escapeUrl($block->getProductUrl($item)) ?>" + title="<?= $block->escapeHtmlAttr($product->getName()) ?>"> <?= $block->getImage($product, 'customer_shared_wishlist')->toHtml() ?> </a> <strong class="product name"> @@ -45,10 +48,13 @@ ?> <?= $block->getDetailsHtml($item) ?> </td> - <td data-th="<?= $block->escapeHtmlAttr(__('Comment')) ?>" class="col comment"><?= /* @noEscape */ $block->getEscapedDescription($item) ?></td> - <td data-th="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" class="col actions" data-role="add-to-links"> - <?php if ($product->isSaleable()) : ?> - <?php if ($isVisibleProduct) : ?> + <td data-th="<?= $block->escapeHtmlAttr(__('Comment')) ?>" + class="col comment"><?= /* @noEscape */ $block->getEscapedDescription($item) ?> + </td> + <td data-th="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" class="col actions" + data-role="add-to-links"> + <?php if ($product->isSaleable()): ?> + <?php if ($isVisibleProduct): ?> <button type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" data-post='<?= /* @noEscape */ $block->getSharedItemAddToCartUrl($item) ?>' @@ -57,9 +63,16 @@ </button> <?php endif ?> <?php endif; ?> - <a href="#" data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($item) ?>' onclick="location.assign(this.href); return false;" class="action towishlist" data-action="add-to-wishlist"> + <a href="#" data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($item) ?>' + id="wishlist-shared-item-<?= /* @noEscape */ $item->getId() ?>" + class="action towishlist" data-action="add-to-wishlist"> <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "location.assign(this.href); event.preventDefault();", + 'a#wishlist-shared-item-' . $item->getId() + ) ?> </td> </tr> <?php endforeach ?> @@ -68,7 +81,7 @@ </div> <div class="actions-toolbar"> - <?php if ($block->isSaleable()) : ?> + <?php if ($block->isSaleable()): ?> <div class="primary"> <button type="button" title="<?= $block->escapeHtmlAttr(__('Add All to Cart')) ?>" @@ -85,6 +98,6 @@ </div> </div> </form> -<?php else : ?> +<?php else: ?> <div class="message info empty"><div><?= $block->escapeHtml(__('Wish List is empty now.')) ?></div></div> <?php endif ?> diff --git a/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php new file mode 100644 index 0000000000000..9cc1404613e41 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Mapper; + +use Magento\Wishlist\Model\Wishlist; + +/** + * Prepares the wishlist output as associative array + */ +class WishlistDataMapper +{ + /** + * Mapping the review data + * + * @param Wishlist $wishlist + * + * @return array + */ + public function map(Wishlist $wishlist): array + { + return [ + 'id' => $wishlist->getId(), + 'sharing_code' => $wishlist->getSharingCode(), + 'updated_at' => $wishlist->getUpdatedAt(), + 'items_count' => $wishlist->getItemsCount(), + 'name' => $wishlist->getName(), + 'model' => $wishlist, + ]; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php new file mode 100644 index 0000000000000..840c4638614c4 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php @@ -0,0 +1,158 @@ +<?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 as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\AddProductsToWishlist as AddProductsToWishlistModel; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Adding products to wishlist resolver + */ +class AddProductsToWishlist implements ResolverInterface +{ + /** + * @var AddProductsToWishlistModel + */ + private $addProductsToWishlist; + + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @param WishlistResourceModel $wishlistResource + * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig + * @param AddProductsToWishlistModel $addProductsToWishlist + * @param WishlistDataMapper $wishlistDataMapper + */ + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig, + AddProductsToWishlistModel $addProductsToWishlist, + WishlistDataMapper $wishlistDataMapper + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; + $this->addProductsToWishlist = $addProductsToWishlist; + $this->wishlistDataMapper = $wishlistDataMapper; + } + + /** + * @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(); + + /* Guest checking */ + if (null === $customerId || 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItems = $this->getWishlistItems($args['wishlistItems']); + $wishlistOutput = $this->addProductsToWishlist->execute($wishlist, $wishlistItems); + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'user_errors' => array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get wishlist items + * + * @param array $wishlistItemsData + * + * @return array + */ + private function getWishlistItems(array $wishlistItemsData): array + { + $wishlistItems = []; + + foreach ($wishlistItemsData as $wishlistItemData) { + $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); + } + + return $wishlistItems; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if ($wishlistId !== null && $wishlistId > 0) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php index a84ce0e965b6d..b73afe27883dd 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php @@ -9,9 +9,11 @@ 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\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; use Magento\Wishlist\Model\WishlistFactory; /** @@ -24,12 +26,21 @@ class CustomerWishlistResolver implements ResolverInterface */ private $wishlistFactory; + /** + * @var WishlistConfig + */ + private $wishlistConfig; + /** * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig */ - public function __construct(WishlistFactory $wishlistFactory) - { + public function __construct( + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig + ) { $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; } /** @@ -42,6 +53,10 @@ public function resolve( array $value = null, array $args = null ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); + } + if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); } 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 new file mode 100644 index 0000000000000..66a6c7b86ea37 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php @@ -0,0 +1,144 @@ +<?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 as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\Wishlist\RemoveProductsFromWishlist as RemoveProductsFromWishlistModel; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Removing products from wishlist resolver + */ +class RemoveProductsFromWishlist implements ResolverInterface +{ + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var RemoveProductsFromWishlistModel + */ + private $removeProductsFromWishlist; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @param WishlistFactory $wishlistFactory + * @param WishlistResourceModel $wishlistResource + * @param WishlistConfig $wishlistConfig + * @param WishlistDataMapper $wishlistDataMapper + * @param RemoveProductsFromWishlistModel $removeProductsFromWishlist + */ + public function __construct( + WishlistFactory $wishlistFactory, + WishlistResourceModel $wishlistResource, + WishlistConfig $wishlistConfig, + WishlistDataMapper $wishlistDataMapper, + RemoveProductsFromWishlistModel $removeProductsFromWishlist + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistConfig = $wishlistConfig; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistDataMapper = $wishlistDataMapper; + $this->removeProductsFromWishlist = $removeProductsFromWishlist; + } + + /** + * @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(); + + /* Guest checking */ + if ($customerId === null || 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItemsIds = $args['wishlistItemsIds']; + $wishlistOutput = $this->removeProductsFromWishlist->execute($wishlist, $wishlistItemsIds); + + if (!empty($wishlistItemsIds)) { + $this->wishlistResource->save($wishlist); + } + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'user_errors' => \array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if ($wishlistId !== null && $wishlistId > 0) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} 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 new file mode 100644 index 0000000000000..47a408d55555b --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php @@ -0,0 +1,163 @@ +<?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 as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\Wishlist\UpdateProductsInWishlist as UpdateProductsInWishlistModel; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Update wishlist items resolver + */ +class UpdateProductsInWishlist implements ResolverInterface +{ + /** + * @var UpdateProductsInWishlistModel + */ + private $updateProductsInWishlist; + + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @param WishlistResourceModel $wishlistResource + * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig + * @param UpdateProductsInWishlistModel $updateProductsInWishlist + * @param WishlistDataMapper $wishlistDataMapper + */ + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig, + UpdateProductsInWishlistModel $updateProductsInWishlist, + WishlistDataMapper $wishlistDataMapper + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; + $this->updateProductsInWishlist = $updateProductsInWishlist; + $this->wishlistDataMapper = $wishlistDataMapper; + } + + /** + * @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(); + + /* Guest checking */ + if (null === $customerId || $customerId === 0) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItems = $args['wishlistItems']; + $wishlistItems = $this->getWishlistItems($wishlistItems); + $wishlistOutput = $this->updateProductsInWishlist->execute($wishlist, $wishlistItems); + + if (count($wishlistOutput->getErrors()) !== count($wishlistItems)) { + $this->wishlistResource->save($wishlist); + } + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'user_errors' => \array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get DTO wishlist items + * + * @param array $wishlistItemsData + * + * @return array + */ + private function getWishlistItems(array $wishlistItemsData): array + { + $wishlistItems = []; + + foreach ($wishlistItemsData as $wishlistItemData) { + $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); + } + + return $wishlistItems; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if (null !== $wishlistId && 0 < $wishlistId) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} 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 792928ab61aaf..f31b403a514fb 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php @@ -8,10 +8,12 @@ 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; @@ -30,14 +32,24 @@ class WishlistResolver implements ResolverInterface */ private $wishlistFactory; + /** + * @var WishlistConfig + */ + private $wishlistConfig; + /** * @param WishlistResourceModel $wishlistResource * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig */ - public function __construct(WishlistResourceModel $wishlistResource, WishlistFactory $wishlistFactory) - { + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig + ) { $this->wishlistResource = $wishlistResource; $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; } /** @@ -50,6 +62,10 @@ public function resolve( array $value = null, array $args = null ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); + } + $customerId = $context->getUserId(); /* Guest checking */ diff --git a/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php b/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php index 8385d3ca852a4..017462b4c94c6 100644 --- a/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php +++ b/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php @@ -14,6 +14,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\GraphQl\Model\Query\ContextExtensionInterface; use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config; use Magento\Wishlist\Model\WishlistFactory; use Magento\WishlistGraphQl\Model\Resolver\CustomerWishlistResolver; use PHPUnit\Framework\MockObject\MockObject; @@ -48,6 +49,11 @@ class CustomerWishlistResolverTest extends TestCase */ private $resolver; + /** + * @var Config|MockObject + */ + private $wishlistConfigMock; + /** * Build the Testing Environment */ @@ -74,9 +80,12 @@ protected function setUp(): void ->setMethods(['loadByCustomerId', 'getId', 'getSharingCode', 'getUpdatedAt', 'getItemsCount']) ->getMock(); + $this->wishlistConfigMock = $this->createMock(Config::class); + $objectManager = new ObjectManager($this); $this->resolver = $objectManager->getObject(CustomerWishlistResolver::class, [ - 'wishlistFactory' => $this->wishlistFactoryMock + 'wishlistFactory' => $this->wishlistFactoryMock, + 'wishlistConfig' => $this->wishlistConfigMock ]); } @@ -85,6 +94,8 @@ protected function setUp(): void */ public function testThrowExceptionWhenUserNotAuthorized(): void { + $this->wishlistConfigMock->method('isEnabled')->willReturn(true); + // Given $this->extensionAttributesMock->method('getIsCustomer') ->willReturn(false); @@ -107,6 +118,8 @@ public function testThrowExceptionWhenUserNotAuthorized(): void */ public function testFactoryCreatesWishlistByAuthorizedCustomerId(): void { + $this->wishlistConfigMock->method('isEnabled')->willReturn(true); + // Given $this->extensionAttributesMock->method('getIsCustomer') ->willReturn(true); 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 deaa66921ba7c..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: "The wishlist query returns 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"), @@ -32,3 +47,50 @@ type WishlistItem { added_at: String @doc(description: "The time when the customer added the item to the wish list"), product: ProductInterface @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\ProductResolver") } + +type Mutation { + addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput @doc(description: "Adds one or more products to the specified wish list. This mutation supports all product types") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\AddProductsToWishlist") + removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput @doc(description: "Removes one or more products from the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\RemoveProductsFromWishlist") + updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput @doc(description: "Updates one or more products in the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\UpdateProductsInWishlist") +} + +input WishlistItemInput @doc(description: "Defines the items to add to a wish list") { + sku: String @doc(description: "The SKU of the product to add. For complex product types, specify the child product SKU") + quantity: Float @doc(description: "The amount or number of items to add") + parent_sku: String @doc(description: "For complex product types, the SKU of the parent product") + selected_options: [ID!] @doc(description: "An array of strings corresponding to options the customer selected") + entered_options: [EnteredOptionInput!] @doc(description: "An array of options that the customer entered") +} + +type AddProductsToWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with all items that were successfully added") + user_errors:[WishListUserInputError!]! @doc(description: "An array of errors encountered while adding products to a wish list") +} + +type RemoveProductsFromWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with after items were successfully deleted") + user_errors:[WishListUserInputError!]! @doc(description:"An array of errors encountered while deleting products from a wish list") +} + +input WishlistItemUpdateInput @doc(description: "Defines updates to items in a wish list") { + wishlist_item_id: ID @doc(description: "The ID of the wishlist item to update") + quantity: Float @doc(description: "The new amount or number of this item") + description: String @doc(description: "Describes the update") + selected_options: [ID!] @doc(description: "An array of strings corresponding to options the customer selected") + entered_options: [EnteredOptionInput!] @doc(description: "An array of options that the customer entered") +} + +type UpdateProductsInWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with all items that were successfully updated") + user_errors: [WishListUserInputError!]! @doc(description:"An array of errors encountered while updating products in a wish list") +} + +type WishListUserInputError @doc(description:"An error encountered while performing operations with WishList.") { + message: String! @doc(description: "A localized error message") + code: WishListUserInputErrorType! @doc(description: "Wishlist-specific error code") +} + +enum WishListUserInputErrorType { + PRODUCT_NOT_FOUND + UNDEFINED +} diff --git a/app/design/adminhtml/Magento/backend/Magento_CatalogPermissions/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_CatalogPermissions/web/css/source/_module.less index 6ce041fc19ac8..f0c98c891b5ba 100644 --- a/app/design/adminhtml/Magento/backend/Magento_CatalogPermissions/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_CatalogPermissions/web/css/source/_module.less @@ -17,3 +17,7 @@ } } } + +.warning-enable-permissions { + color: #f00; +} 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/adminhtml/Magento/backend/web/css/styles-old.less b/app/design/adminhtml/Magento/backend/web/css/styles-old.less index 247316ab0361b..3087e7762a2d8 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles-old.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles-old.less @@ -5105,7 +5105,7 @@ .adminhtml-locks-index { .admin__scope-old { .grid .col-name { - &:extend(.col-570 all); + &:extend(.col-570-max all); } } } diff --git a/app/design/adminhtml/Magento/backend/web/css/styles.less b/app/design/adminhtml/Magento/backend/web/css/styles.less index 80d0923ced75a..02f8edc2b493b 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles.less @@ -53,6 +53,10 @@ td.col-date.col-date-min-width.col-created_at { min-width: 14rem; } +.colorRed { + color:red; +} + // ToDo UI: Temporary. Should be changed @import 'source/components/_calendar-temp.less'; @import 'source/components/_rules-temp.less'; diff --git a/app/design/adminhtml/Magento/backend/web/mui/styles/_table.less b/app/design/adminhtml/Magento/backend/web/mui/styles/_table.less index 70ae82045b3d1..bdd4ba3ec4fb5 100644 --- a/app/design/adminhtml/Magento/backend/web/mui/styles/_table.less +++ b/app/design/adminhtml/Magento/backend/web/mui/styles/_table.less @@ -414,7 +414,7 @@ td.col-type { } tbody tr:nth-child(odd) td { - &:extend(.data-table tbody tr:nth-child(odd) td); + &:extend(.data-table tbody tr:nth-child(odd) td all); } tfoot tr:last-child td { @@ -601,13 +601,13 @@ td.col-type { } .label { - &:extend(.grid-actions .export .label); + &:extend(.grid-actions .export .label all); padding: 0; width: auto; } .action- { - &:extend(.grid-actions .export .action-); + &:extend(.grid-actions .export .action- all); vertical-align: top; } } diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less index f57420deb621d..4b48bbe99ced2 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less @@ -457,11 +457,26 @@ .action { &.delete { &:extend(.abs-remove-button-for-blocks all); - line-height: unset; position: absolute; right: 0; top: -1px; - width: auto; + } + } + + .block-wishlist { + .action { + &.delete { + line-height: unset; + width: auto; + } + } + } + + .block-compare { + .action { + &.delete { + right: initial; + } } } @@ -814,6 +829,7 @@ &:extend(.abs-remove-button-for-blocks all); left: -6px; position: absolute; + right: 0; top: 0; } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_checkout-agreements.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_checkout-agreements.less index ff4f07a983940..b8be2b33a4475 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_checkout-agreements.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_checkout-agreements.less @@ -13,6 +13,30 @@ margin-bottom: @indent__base; } + .checkout-agreement.field { + .lib-vendor-prefix-display(); + + &.required { + label:after { + content: none; + } + + .action-show { + &:after { + content: '*'; + .lib-typography( + @_font-size: @form-field-label-asterisk__font-size, + @_color: @form-field-label-asterisk__color, + @_font-family: @form-field-label-asterisk__font-family, + @_font-weight: @form-field-label-asterisk__font-weight, + @_line-height: @form-field-label-asterisk__line-height, + @_font-style: @form-field-label-asterisk__font-style + ); + } + } + } + } + .action-show { &:extend(.abs-action-button-as-link all); vertical-align: baseline; diff --git a/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less index 09759d95c4b10..8434812f20719 100644 --- a/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less @@ -82,6 +82,10 @@ .field { margin-right: 5px; + &.newsletter { + max-width: 220px; + } + .control { width: 100%; } diff --git a/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js b/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js index 87632a6962cc5..cae30c83d95bc 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js +++ b/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js @@ -5,7 +5,6 @@ var config = { deps: [ - 'Magento_Theme/js/responsive', 'Magento_Theme/js/theme' ] }; diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js b/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js deleted file mode 100644 index 011417f54ad9a..0000000000000 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'jquery', - 'matchMedia', - 'mage/tabs', - 'domReady!' -], function ($, mediaCheck) { - 'use strict'; - - mediaCheck({ - media: '(min-width: 768px)', - - /** - * Switch to Desktop Version. - */ - entry: function () { - var galleryElement; - - (function () { - - var productInfoMain = $('.product-info-main'), - productInfoAdditional = $('#product-info-additional'); - - if (productInfoAdditional.length) { - productInfoAdditional.addClass('hidden'); - productInfoMain.removeClass('responsive'); - } - - })(); - - galleryElement = $('[data-role=media-gallery]'); - - if (galleryElement.length && galleryElement.data('mageZoom')) { - galleryElement.zoom('enable'); - } - - if (galleryElement.length && galleryElement.data('mageGallery')) { - galleryElement.gallery('option', 'disableLinks', true); - galleryElement.gallery('option', 'showNav', false); - galleryElement.gallery('option', 'showThumbs', true); - } - }, - - /** - * Switch to Mobile Version. - */ - exit: function () { - var galleryElement; - - $('.action.toggle.checkout.progress').on('click.gotoCheckoutProgress', function () { - var myWrapper = '#checkout-progress-wrapper'; - - scrollTo(myWrapper + ' .title'); - $(myWrapper + ' .title').addClass('active'); - $(myWrapper + ' .content').show(); - }); - - $('body').on('click.checkoutProgress', '#checkout-progress-wrapper .title', function () { - $(this).toggleClass('active'); - $('#checkout-progress-wrapper .content').toggle(); - }); - - galleryElement = $('[data-role=media-gallery]'); - - setTimeout(function () { - if (galleryElement.length && galleryElement.data('mageZoom')) { - galleryElement.zoom('disable'); - } - - if (galleryElement.length && galleryElement.data('mageGallery')) { - galleryElement.gallery('option', 'disableLinks', false); - galleryElement.gallery('option', 'showNav', true); - galleryElement.gallery('option', 'showThumbs', false); - } - }, 2000); - } - }); -}); diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js b/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js index ab8a6063f29a7..e4edd3bd8662c 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js +++ b/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js @@ -12,21 +12,9 @@ define([ ], function ($, keyboardHandler) { 'use strict'; - if ($('body').hasClass('checkout-cart-index')) { - if ($('#co-shipping-method-form .fieldset.rates').length > 0 && - $('#co-shipping-method-form .fieldset.rates :checked').length === 0 - ) { - $('#block-shipping').on('collapsiblecreate', function () { - $('#block-shipping').collapsible('forceActivate'); - }); - } - } - $('.cart-summary').mage('sticky', { container: '#maincontent' }); - $('.panel.header > .header.links').clone().appendTo('#store\\.links'); - keyboardHandler.apply(); }); 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/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less index d0b7aa1523ad6..e205b20efd17c 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less @@ -998,6 +998,15 @@ } } } + + .block-compare { + .action { + &.delete { + left: 0; + right: initial; + } + } + } } } @@ -1005,6 +1014,7 @@ .compare.wrapper { display: none; } + .catalog-product_compare-index { .columns { .column { diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less index 87990c3e48280..5d9746317af55 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less @@ -424,7 +424,14 @@ .cart-container { .form-cart { .actions.main { - text-align: center; + .lib-vendor-prefix-display(); + .lib-vendor-prefix-flex-direction(column); + .lib-vendor-box-align(center); + + .clear, + .continue { + .lib-css(margin, 0 0 @indent__m 0); + } } } } diff --git a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less index a72f31d72ce48..21ed451a69d10 100644 --- a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less @@ -81,6 +81,10 @@ .block.newsletter { max-width: 44%; width: max-content; + + .field.newsletter { + max-width: 220px; + } .form.subscribe { > .field, diff --git a/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less index e8adcc2f0e4f3..104dd6c4d5b92 100644 --- a/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less @@ -194,3 +194,17 @@ } } } + +#registrant-options { + .item { + .control { + table { + .col.qty { + .input-qty { + display: none; + } + } + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html index 4442c172a08e5..5b58659bcf48c 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html @@ -10,7 +10,7 @@ "var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", "layout handle=\"sales_email_order_creditmemo_items\" creditmemo=$creditmemo order=$order":"Credit Memo Items Grid", -"var billing.name":"Guest Customer Name (Billing)", +"var order_data.customer_name":"Guest Customer Name (Billing)", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", @@ -28,7 +28,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html index 9b09760c1fa4a..91f790715eda3 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html @@ -8,7 +8,7 @@ <!--@vars { "var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", "var store_email":"Store Email", @@ -19,7 +19,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html index 6e35fd2609dff..036b3b6d96dcb 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html @@ -7,7 +7,7 @@ <!--@subject {{trans "Invoice for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "layout handle=\"sales_email_order_invoice_items\" invoice=$invoice order=$order":"Invoice Items Grid", @@ -28,7 +28,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html index f9e1498763cba..759e9766f335a 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name invoice" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", @@ -19,7 +19,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html index 024f6daf76ace..e51b952281ed5 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html @@ -8,7 +8,7 @@ <!--@vars { "var formattedBillingAddress|raw":"Billing Address", "var order_data.email_customer_note|escape|nl2br":"Email Order Note", -"var order.billing_address.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var created_at_formatted":"Order Created At (datetime)", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order":"Order Items Grid", @@ -27,7 +27,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.billing_address.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send you a tracking number."}} diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html index 5f23898b50018..3f8e8ace3428e 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", @@ -18,7 +18,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html index 18684fb052b4e..d1d9d21f1ee9a 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html @@ -7,7 +7,7 @@ <!--@subject {{trans "Your %store_name order has shipped" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var comment|escape|nl2br":"Shipment Comment", @@ -30,7 +30,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html index 5887ff73c6398..37478644ddf9c 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name shipment" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", @@ -19,7 +19,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/design/frontend/Magento/luma/web/css/source/_extends.less b/app/design/frontend/Magento/luma/web/css/source/_extends.less index ce86b690f6252..8ae1776daf239 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_extends.less +++ b/app/design/frontend/Magento/luma/web/css/source/_extends.less @@ -1570,10 +1570,16 @@ margin-bottom: @indent__base; .actions.main { - .continue, - .clear { + .continue { display: none; } + + .clear { + .lib-button-as-link( + @_margin: 0 @indent__base 0 0 + ); + font-weight: @font-weight__regular; + } } } } diff --git a/app/etc/NonComposerComponentRegistration.php b/app/etc/NonComposerComponentRegistration.php index a7377ebfca3af..831f1aa27382e 100644 --- a/app/etc/NonComposerComponentRegistration.php +++ b/app/etc/NonComposerComponentRegistration.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); //Register components (via a list of glob patterns) namespace Magento\NonComposerComponentRegistration; @@ -11,23 +12,23 @@ /** * Include files from a list of glob patterns - * - * @throws RuntimeException - * @return void */ -$main = function () -{ +(static function (): void { $globPatterns = require __DIR__ . '/registration_globlist.php'; - $baseDir = dirname(dirname(__DIR__)) . '/'; + $baseDir = \dirname(__DIR__, 2) . '/'; foreach ($globPatterns as $globPattern) { // Sorting is disabled intentionally for performance improvement - $files = glob($baseDir . $globPattern, GLOB_NOSORT); + $files = \glob($baseDir . $globPattern, GLOB_NOSORT); if ($files === false) { throw new RuntimeException("glob(): error with '$baseDir$globPattern'"); } - array_map(function ($file) { require_once $file; }, $files); - } -}; -$main(); + \array_map( + static function (string $file): void { + require_once $file; + }, + $files + ); + } +})(); diff --git a/app/etc/di.xml b/app/etc/di.xml index 7b91941fe05d6..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" /> @@ -209,6 +210,8 @@ <preference for="Magento\Framework\MessageQueue\QueueFactoryInterface" type="Magento\Framework\MessageQueue\QueueFactory" /> <preference for="Magento\Framework\Search\Request\IndexScopeResolverInterface" type="Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver"/> <preference for="Magento\Framework\HTTP\ClientInterface" type="Magento\Framework\HTTP\Client\Curl" /> + <preference for="Magento\Framework\Interception\ConfigLoaderInterface" type="Magento\Framework\Interception\PluginListGenerator" /> + <preference for="Magento\Framework\Interception\ConfigWriterInterface" type="Magento\Framework\Interception\PluginListGenerator" /> <type name="Magento\Framework\Model\ResourceModel\Db\TransactionManager" shared="false" /> <type name="Magento\Framework\Acl\Data\Cache"> <arguments> @@ -426,6 +429,17 @@ <argument name="reader" xsi:type="object">Magento\Framework\ObjectManager\Config\Reader\Dom\Proxy</argument> <argument name="cacheId" xsi:type="string">plugin-list</argument> <argument name="scopePriorityScheme" xsi:type="array"> + <item name="primary" xsi:type="string">primary</item> + <item name="first" xsi:type="string">global</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\Interception\PluginListGenerator"> + <arguments> + <argument name="reader" xsi:type="object">Magento\Framework\ObjectManager\Config\Reader\Dom\Proxy</argument> + <argument name="logger" xsi:type="object">\Psr\Log\LoggerInterface\Proxy</argument> + <argument name="scopePriorityScheme" xsi:type="array"> + <item name="primary" xsi:type="string">primary</item> <item name="first" xsi:type="string">global</item> </argument> </arguments> @@ -1819,4 +1833,12 @@ </argument> </arguments> </type> + <type name="Magento\Framework\View\TemplateEngine\Php"> + <arguments> + <argument name="blockVariables" xsi:type="array"> + <item name="secureRenderer" xsi:type="object">Magento\Framework\View\Helper\SecureHtmlRenderer\Proxy</item> + <item name="escaper" xsi:type="object">Magento\Framework\Escaper</item> + </argument> + </arguments> + </type> </config> diff --git a/composer.json b/composer.json index d487ad5975040..57fbfaaa35c2b 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "colinmollenhour/credis": "1.11.1", "colinmollenhour/php-redis-session-abstract": "~1.4.0", "composer/composer": "^1.9", - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "guzzlehttp/guzzle": "^6.3.3", "laminas/laminas-captcha": "^2.7.1", "laminas/laminas-code": "~3.4.1", @@ -64,13 +64,13 @@ "laminas/laminas-uri": "^2.5.1", "laminas/laminas-validator": "^2.6.0", "laminas/laminas-view": "~2.11.2", - "magento/composer": "1.6.x-dev", + "magento/composer": "1.6.0", "magento/magento-composer-installer": ">=0.1.11", "magento/zendframework1": "~1.14.2", "monolog/monolog": "^1.17", "paragonie/sodium_compat": "^1.6", "pelago/emogrifier": "^3.1.0", - "php-amqplib/php-amqplib": "~2.7.0||~2.10.0", + "php-amqplib/php-amqplib": "~2.10.0", "phpseclib/mcrypt_compat": "1.0.8", "phpseclib/phpseclib": "2.0.*", "ramsey/uuid": "~3.8.0", @@ -88,7 +88,7 @@ "friendsofphp/php-cs-fixer": "~2.16.0", "lusitanian/oauth": "~0.8.10", "magento/magento-coding-standard": "*", - "magento/magento2-functional-testing-framework": "dev-3.0.0-RC3", + "magento/magento2-functional-testing-framework": "^3.0", "pdepend/pdepend": "~2.7.1", "phpcompatibility/php-compatibility": "^9.3", "phpmd/phpmd": "^2.8.0", @@ -164,6 +164,7 @@ "magento/module-encryption-key": "*", "magento/module-fedex": "*", "magento/module-gift-message": "*", + "magento/module-gift-message-graph-ql": "*", "magento/module-google-adwords": "*", "magento/module-google-analytics": "*", "magento/module-google-optimizer": "*", @@ -193,6 +194,7 @@ "magento/module-login-as-customer": "*", "magento/module-login-as-customer-admin-ui": "*", "magento/module-login-as-customer-api": "*", + "magento/module-login-as-customer-assistance": "*", "magento/module-login-as-customer-frontend-ui": "*", "magento/module-login-as-customer-log": "*", "magento/module-login-as-customer-quote": "*", @@ -204,7 +206,24 @@ "magento/module-media-content-cms": "*", "magento/module-media-gallery": "*", "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-ui": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-integration": "*", + "magento/module-media-gallery-synchronization": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-synchronization": "*", + "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": "*", @@ -214,6 +233,7 @@ "magento/module-mysql-mq": "*", "magento/module-new-relic-reporting": "*", "magento/module-newsletter": "*", + "magento/module-newsletter-graph-ql": "*", "magento/module-offline-payments": "*", "magento/module-offline-shipping": "*", "magento/module-page-cache": "*", @@ -226,12 +246,16 @@ "magento/module-product-video": "*", "magento/module-quote": "*", "magento/module-quote-analytics": "*", + "magento/module-quote-bundle-options": "*", + "magento/module-quote-configurable-options": "*", + "magento/module-quote-downloadable-links": "*", "magento/module-quote-graph-ql": "*", "magento/module-related-product-graph-ql": "*", "magento/module-release-notification": "*", "magento/module-reports": "*", "magento/module-require-js": "*", "magento/module-review": "*", + "magento/module-review-graph-ql": "*", "magento/module-review-analytics": "*", "magento/module-robots": "*", "magento/module-rss": "*", diff --git a/composer.lock b/composer.lock index 6a47e7e44ab69..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": "e86af25d9a4a1942c437cca58f9f1efb", + "content-hash": "a03edc1c8ee05f82886eebd6ed288df8", "packages": [ { "name": "colinmollenhour/cache-backend-file", @@ -220,16 +220,16 @@ }, { "name": "composer/composer", - "version": "1.10.6", + "version": "1.10.9", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "be81b9c4735362c26876bdbfd3b5bc7e7f711c88" + "reference": "83c3250093d5491600a822e176b107a945baf95a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/be81b9c4735362c26876bdbfd3b5bc7e7f711c88", - "reference": "be81b9c4735362c26876bdbfd3b5bc7e7f711c88", + "url": "https://api.github.com/repos/composer/composer/zipball/83c3250093d5491600a822e176b107a945baf95a", + "reference": "83c3250093d5491600a822e176b107a945baf95a", "shasum": "" }, "require": { @@ -237,7 +237,7 @@ "composer/semver": "^1.0", "composer/spdx-licenses": "^1.2", "composer/xdebug-handler": "^1.1", - "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", + "justinrainbow/json-schema": "^5.2.10", "php": "^5.3.2 || ^7.0", "psr/log": "^1.0", "seld/jsonlint": "^1.4", @@ -248,12 +248,11 @@ "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0" }, "conflict": { - "symfony/console": "2.8.38", - "symfony/phpunit-bridge": "3.4.40" + "symfony/console": "2.8.38" }, "require-dev": { "phpspec/prophecy": "^1.10", - "symfony/phpunit-bridge": "^3.4" + "symfony/phpunit-bridge": "^4.2" }, "suggest": { "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", @@ -302,12 +301,16 @@ "url": "https://packagist.com", "type": "custom" }, + { + "url": "https://github.com/composer", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2020-05-06T08:28:10+00:00" + "time": "2020-07-16T10:57:00+00:00" }, { "name": "composer/semver", @@ -372,16 +375,16 @@ }, { "name": "composer/spdx-licenses", - "version": "1.5.3", + "version": "1.5.4", "source": { "type": "git", "url": "https://github.com/composer/spdx-licenses.git", - "reference": "0c3e51e1880ca149682332770e25977c70cf9dae" + "reference": "6946f785871e2314c60b4524851f3702ea4f2223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/0c3e51e1880ca149682332770e25977c70cf9dae", - "reference": "0c3e51e1880ca149682332770e25977c70cf9dae", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/6946f785871e2314c60b4524851f3702ea4f2223", + "reference": "6946f785871e2314c60b4524851f3702ea4f2223", "shasum": "" }, "require": { @@ -428,20 +431,34 @@ "spdx", "validator" ], - "time": "2020-02-14T07:44:31+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-07-15T15:35:07+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.4.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7" + "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/1ab9842d69e64fb3a01be6b656501032d1b78cb7", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", + "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", "shasum": "" }, "require": { @@ -476,9 +493,17 @@ { "url": "https://packagist.com", "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" } ], - "time": "2020-03-01T12:26:26+00:00" + "time": "2020-06-04T11:16:35+00:00" }, { "name": "container-interop/container-interop", @@ -678,16 +703,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.5.3", + "version": "6.5.5", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e" + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aab4ebd862aa7d04f01a4b51849d657db56d882e", - "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", "shasum": "" }, "require": { @@ -695,7 +720,7 @@ "guzzlehttp/promises": "^1.0", "guzzlehttp/psr7": "^1.6.1", "php": ">=5.5", - "symfony/polyfill-intl-idn": "^1.11" + "symfony/polyfill-intl-idn": "^1.17.0" }, "require-dev": { "ext-curl": "*", @@ -741,7 +766,7 @@ "rest", "web service" ], - "time": "2020-04-18T10:38:46+00:00" + "time": "2020-06-16T21:01:06+00:00" }, { "name": "guzzlehttp/promises", @@ -867,16 +892,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.2.9", + "version": "5.2.10", "source": { "type": "git", "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "44c6787311242a979fa15c704327c20e7221a0e4" + "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/44c6787311242a979fa15c704327c20e7221a0e4", - "reference": "44c6787311242a979fa15c704327c20e7221a0e4", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", + "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", "shasum": "" }, "require": { @@ -929,7 +954,7 @@ "json", "schema" ], - "time": "2019-09-25T14:49:45+00:00" + "time": "2020-05-27T16:41:55+00:00" }, { "name": "laminas/laminas-captcha", @@ -1331,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" }, { @@ -1710,16 +1729,16 @@ }, { "name": "laminas/laminas-form", - "version": "2.14.5", + "version": "2.15.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-form.git", - "reference": "3e22e09751cf6ae031be87a44e092e7925ce5b7b" + "reference": "359cd372c565e18a17f32ccfeacdf21bba091ce2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-form/zipball/3e22e09751cf6ae031be87a44e092e7925ce5b7b", - "reference": "3e22e09751cf6ae031be87a44e092e7925ce5b7b", + "url": "https://api.github.com/repos/laminas/laminas-form/zipball/359cd372c565e18a17f32ccfeacdf21bba091ce2", + "reference": "359cd372c565e18a17f32ccfeacdf21bba091ce2", "shasum": "" }, "require": { @@ -1762,8 +1781,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.14.x-dev", - "dev-develop": "2.15.x-dev" + "dev-master": "2.15.x-dev", + "dev-develop": "2.16.x-dev" }, "laminas": { "component": "Laminas\\Form", @@ -1788,20 +1807,26 @@ "form", "laminas" ], - "time": "2020-03-29T12:46:16+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-07-14T13:53:27+00:00" }, { "name": "laminas/laminas-http", - "version": "2.11.2", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-http.git", - "reference": "8c66963b933c80da59433da56a44dfa979f3ec88" + "reference": "48bd06ffa3a6875e2b77d6852405eb7b1589d575" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-http/zipball/8c66963b933c80da59433da56a44dfa979f3ec88", - "reference": "8c66963b933c80da59433da56a44dfa979f3ec88", + "url": "https://api.github.com/repos/laminas/laminas-http/zipball/48bd06ffa3a6875e2b77d6852405eb7b1589d575", + "reference": "48bd06ffa3a6875e2b77d6852405eb7b1589d575", "shasum": "" }, "require": { @@ -1813,7 +1838,7 @@ "php": "^5.6 || ^7.0" }, "replace": { - "zendframework/zend-http": "self.version" + "zendframework/zend-http": "^2.11.2" }, "require-dev": { "laminas/laminas-coding-standard": "~1.0.0", @@ -1826,8 +1851,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.11.x-dev", - "dev-develop": "2.12.x-dev" + "dev-master": "2.12.x-dev", + "dev-develop": "2.13.x-dev" } }, "autoload": { @@ -1846,7 +1871,13 @@ "http client", "laminas" ], - "time": "2019-12-31T17:02:36+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-06-23T15:14:37+00:00" }, { "name": "laminas/laminas-hydrator", @@ -2232,16 +2263,16 @@ }, { "name": "laminas/laminas-mail", - "version": "2.10.1", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-mail.git", - "reference": "cfe0711446c8d9c392e9fc664c9ccc180fa89005" + "reference": "4c5545637eea3dc745668ddff1028692ed004c4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/cfe0711446c8d9c392e9fc664c9ccc180fa89005", - "reference": "cfe0711446c8d9c392e9fc664c9ccc180fa89005", + "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/4c5545637eea3dc745668ddff1028692ed004c4b", + "reference": "4c5545637eea3dc745668ddff1028692ed004c4b", "shasum": "" }, "require": { @@ -2271,8 +2302,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" + "dev-master": "2.11.x-dev", + "dev-develop": "2.12.x-dev" }, "laminas": { "component": "Laminas\\Mail", @@ -2300,7 +2331,7 @@ "type": "community_bridge" } ], - "time": "2020-04-21T16:42:19+00:00" + "time": "2020-06-30T20:17:23+00:00" }, { "name": "laminas/laminas-math", @@ -3292,26 +3323,20 @@ "laminas", "zf" ], - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-05-20T16:45:56+00:00" }, { "name": "magento/composer", - "version": "1.6.x-dev", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/magento/composer.git", - "reference": "f3e4bec8fc73a97a6cbc391b1b93d4c32566763d" + "reference": "fcc66f535d631788f2ba160ff547357086d9b2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/composer/zipball/f3e4bec8fc73a97a6cbc391b1b93d4c32566763d", - "reference": "f3e4bec8fc73a97a6cbc391b1b93d4c32566763d", + "url": "https://api.github.com/repos/magento/composer/zipball/fcc66f535d631788f2ba160ff547357086d9b2c9", + "reference": "fcc66f535d631788f2ba160ff547357086d9b2c9", "shasum": "" }, "require": { @@ -3334,7 +3359,7 @@ "AFL-3.0" ], "description": "Magento composer library helps to instantiate Composer application and run composer commands.", - "time": "2020-05-08T01:07:09+00:00" + "time": "2020-06-15T17:52:31+00:00" }, { "name": "magento/magento-composer-installer", @@ -3537,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" }, { @@ -3874,16 +3889,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.27", + "version": "2.0.28", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "34620af4df7d1988d8f0d7e91f6c8a3bf931d8dc" + "reference": "d1ca58cf33cb21046d702ae3a7b14fdacd9f3260" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/34620af4df7d1988d8f0d7e91f6c8a3bf931d8dc", - "reference": "34620af4df7d1988d8f0d7e91f6c8a3bf931d8dc", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d1ca58cf33cb21046d702ae3a7b14fdacd9f3260", + "reference": "d1ca58cf33cb21046d702ae3a7b14fdacd9f3260", "shasum": "" }, "require": { @@ -3976,7 +3991,7 @@ "type": "tidelift" } ], - "time": "2020-04-04T23:17:33+00:00" + "time": "2020-07-08T09:08:33+00:00" }, { "name": "psr/container", @@ -4339,30 +4354,20 @@ "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" }, { "name": "seld/phar-utils", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/Seldaek/phar-utils.git", - "reference": "8800503d56b9867d43d9c303b9cbcc26016e82f0" + "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8800503d56b9867d43d9c303b9cbcc26016e82f0", - "reference": "8800503d56b9867d43d9c303b9cbcc26016e82f0", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8674b1d84ffb47cc59a101f5d5a3b61e87d23796", + "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796", "shasum": "" }, "require": { @@ -4393,26 +4398,27 @@ "keywords": [ "phar" ], - "time": "2020-02-14T15:25:33+00:00" + "time": "2020-07-07T18:42:57+00:00" }, { "name": "symfony/console", - "version": "v4.4.8", + "version": "v4.4.10", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7" + "reference": "326b064d804043005526f5a0494cfb49edb59bb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/10bb3ee3c97308869d53b3e3d03f6ac23ff985f7", - "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7", + "url": "https://api.github.com/repos/symfony/console/zipball/326b064d804043005526f5a0494cfb49edb59bb0", + "reference": "326b064d804043005526f5a0494cfb49edb59bb0", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1|^2" }, "conflict": { @@ -4483,29 +4489,29 @@ "type": "tidelift" } ], - "time": "2020-03-30T11:41:10+00:00" + "time": "2020-05-30T20:06:45+00:00" }, { "name": "symfony/css-selector", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "5f8d5271303dad260692ba73dfa21777d38e124e" + "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/5f8d5271303dad260692ba73dfa21777d38e124e", - "reference": "5f8d5271303dad260692ba73dfa21777d38e124e", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/e544e24472d4c97b2d11ade7caacd446727c6bf9", + "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4550,24 +4556,24 @@ "type": "tidelift" } ], - "time": "2020-03-27T16:56:45+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.8", + "version": "v4.4.10", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "abc8e3618bfdb55e44c8c6a00abd333f831bbfed" + "reference": "a5370aaa7807c7a439b21386661ffccf3dff2866" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abc8e3618bfdb55e44c8c6a00abd333f831bbfed", - "reference": "abc8e3618bfdb55e44c8c6a00abd333f831bbfed", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a5370aaa7807c7a439b21386661ffccf3dff2866", + "reference": "a5370aaa7807c7a439b21386661ffccf3dff2866", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "symfony/event-dispatcher-contracts": "^1.1" }, "conflict": { @@ -4634,24 +4640,24 @@ "type": "tidelift" } ], - "time": "2020-03-27T16:54:36+00:00" + "time": "2020-05-20T08:37:50+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v1.1.7", + "version": "v1.1.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18" + "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c43ab685673fb6c8d84220c77897b1d6cdbe1d18", - "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/84e23fdcd2517bf37aecbd16967e83f0caee25a7", + "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "suggest": { "psr/event-dispatcher": "", @@ -4661,6 +4667,10 @@ "extra": { "branch-alias": { "dev-master": "1.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -4692,30 +4702,44 @@ "interoperability", "standards" ], - "time": "2019-09-17T09:54:03+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-06T13:19:58+00:00" }, { "name": "symfony/filesystem", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7cd0dafc4353a0f62e307df90b48466379c8cc91" + "reference": "6e4320f06d5f2cce0d96530162491f4465179157" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7cd0dafc4353a0f62e307df90b48466379c8cc91", - "reference": "7cd0dafc4353a0f62e307df90b48466379c8cc91", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/6e4320f06d5f2cce0d96530162491f4465179157", + "reference": "6e4320f06d5f2cce0d96530162491f4465179157", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4756,29 +4780,29 @@ "type": "tidelift" } ], - "time": "2020-04-12T14:40:17+00:00" + "time": "2020-05-30T20:35:19+00:00" }, { "name": "symfony/finder", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "600a52c29afc0d1caa74acbec8d3095ca7e9910d" + "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/600a52c29afc0d1caa74acbec8d3095ca7e9910d", - "reference": "600a52c29afc0d1caa74acbec8d3095ca7e9910d", + "url": "https://api.github.com/repos/symfony/finder/zipball/4298870062bfc667cb78d2b379be4bf5dec5f187", + "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4819,20 +4843,20 @@ "type": "tidelift" } ], - "time": "2020-03-27T16:56:45+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.17.0", + "version": "v1.18.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9" + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9", - "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", "shasum": "" }, "require": { @@ -4844,7 +4868,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -4891,25 +4919,26 @@ "type": "tidelift" } ], - "time": "2020-05-12T16:14:59+00:00" + "time": "2020-07-14T12:35:20+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.17.0", + "version": "v1.18.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "3bff59ea7047e925be6b7f2059d60af31bb46d6a" + "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3bff59ea7047e925be6b7f2059d60af31bb46d6a", - "reference": "3bff59ea7047e925be6b7f2059d60af31bb46d6a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", + "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", "shasum": "" }, "require": { "php": ">=5.3.3", - "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php70": "^1.10", "symfony/polyfill-php72": "^1.10" }, "suggest": { @@ -4918,7 +4947,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -4938,6 +4971,10 @@ "name": "Laurent Bassin", "email": "laurent@bassin.info" }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" @@ -4967,40 +5004,47 @@ "type": "tidelift" } ], - "time": "2020-05-12T16:47:27+00:00" + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.17.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fa79b11539418b02fc5e1897267673ba2c19419c" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c", - "reference": "fa79b11539418b02fc5e1897267673ba2c19419c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", "shasum": "" }, "require": { "php": ">=5.3.3" }, "suggest": { - "ext-mbstring": "For best performance" + "ext-intl": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, "files": [ "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5017,11 +5061,12 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "mbstring", + "intl", + "normalizer", "polyfill", "portable", "shim" @@ -5040,34 +5085,41 @@ "type": "tidelift" } ], - "time": "2020-05-12T16:47:27+00:00" + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.17.0", + "name": "symfony/polyfill-mbstring", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "f048e612a3905f34931127360bdd2def19a5e582" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/f048e612a3905f34931127360bdd2def19a5e582", - "reference": "f048e612a3905f34931127360bdd2def19a5e582", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "suggest": { + "ext-mbstring": "For best performance" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" + "Symfony\\Polyfill\\Mbstring\\": "" }, "files": [ "bootstrap.php" @@ -5087,10 +5139,11 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "mbstring", "polyfill", "portable", "shim" @@ -5109,34 +5162,39 @@ "type": "tidelift" } ], - "time": "2020-05-12T16:47:27+00:00" + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.17.0", + "name": "symfony/polyfill-php70", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc" + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a760d8964ff79ab9bf057613a5808284ec852ccc", - "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", + "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", "shasum": "" }, "require": { + "paragonie/random_compat": "~1.0|~2.0|~9.99", "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Polyfill\\Php70\\": "" }, "files": [ "bootstrap.php" @@ -5159,7 +5217,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -5181,37 +5239,41 @@ "type": "tidelift" } ], - "time": "2020-05-12T16:47:27+00:00" + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "symfony/process", - "version": "v4.4.8", + "name": "symfony/polyfill-php72", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "4b6a9a4013baa65d409153cbb5a895bf093dc7f4" + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "639447d008615574653fb3bc60d1986d7172eaae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4b6a9a4013baa65d409153cbb5a895bf093dc7f4", - "reference": "4b6a9a4013baa65d409153cbb5a895bf093dc7f4", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", + "reference": "639447d008615574653fb3bc60d1986d7172eaae", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Process\\": "" + "Symfony\\Polyfill\\Php72\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "bootstrap.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5220,16 +5282,22 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Process Component", + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "funding": [ { "url": "https://symfony.com/sponsor", @@ -5244,39 +5312,45 @@ "type": "tidelift" } ], - "time": "2020-04-15T15:56:18+00:00" + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "symfony/service-contracts", - "version": "v2.0.1", + "name": "symfony/polyfill-php73", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "144c5e51266b281231e947b51223ba14acf1a749" + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749", - "reference": "144c5e51266b281231e947b51223ba14acf1a749", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca", + "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca", "shasum": "" }, "require": { - "php": "^7.2.5", - "psr/container": "^1.0" - }, - "suggest": { - "symfony/service-implementation": "" + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5292,64 +5366,295 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2019-11-18T17:27:11+00:00" + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "tedivm/jshrink", - "version": "v1.3.3", + "name": "symfony/polyfill-php80", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/tedious/JShrink.git", - "reference": "566e0c731ba4e372be2de429ef7d54f4faf4477a" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tedious/JShrink/zipball/566e0c731ba4e372be2de429ef7d54f4faf4477a", - "reference": "566e0c731ba4e372be2de429ef7d54f4faf4477a", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", "shasum": "" }, "require": { - "php": "^5.6|^7.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.8", - "php-coveralls/php-coveralls": "^1.1.0", - "phpunit/phpunit": "^6" + "php": ">=7.0.8" }, "type": "library", - "autoload": { - "psr-0": { - "JShrink": "src/" + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Robert Hafner", - "email": "tedivm@tedivm.com" + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Javascript Minifier built in PHP", - "homepage": "http://github.com/tedious/JShrink", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "javascript", - "minifier" + "compatibility", + "polyfill", + "portable", + "shim" ], - "time": "2019-06-28T18:11:46+00:00" - }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/process", + "version": "v4.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/c714958428a85c86ab97e3a0c96db4c4f381b7f5", + "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-30T20:06:45+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/58c7475e5457c5492c26cc740cc0ad7464be9442", + "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.0" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-06T13:23:11+00:00" + }, + { + "name": "tedivm/jshrink", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/tedious/JShrink.git", + "reference": "566e0c731ba4e372be2de429ef7d54f4faf4477a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tedious/JShrink/zipball/566e0c731ba4e372be2de429ef7d54f4faf4477a", + "reference": "566e0c731ba4e372be2de429ef7d54f4faf4477a", + "shasum": "" + }, + "require": { + "php": "^5.6|^7.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.8", + "php-coveralls/php-coveralls": "^1.1.0", + "phpunit/phpunit": "^6" + }, + "type": "library", + "autoload": { + "psr-0": { + "JShrink": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Robert Hafner", + "email": "tedivm@tedivm.com" + } + ], + "description": "Javascript Minifier built in PHP", + "homepage": "http://github.com/tedious/JShrink", + "keywords": [ + "javascript", + "minifier" + ], + "time": "2019-06-28T18:11:46+00:00" + }, { "name": "true/punycode", "version": "v2.1.1", @@ -5451,16 +5756,16 @@ }, { "name": "webonyx/graphql-php", - "version": "v0.13.8", + "version": "v0.13.9", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "6829ae58f4c59121df1f86915fb9917a2ec595e8" + "reference": "d9a94fddcad0a35d4bced212b8a44ad1bc59bdf3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/6829ae58f4c59121df1f86915fb9917a2ec595e8", - "reference": "6829ae58f4c59121df1f86915fb9917a2ec595e8", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9a94fddcad0a35d4bced212b8a44ad1bc59bdf3", + "reference": "d9a94fddcad0a35d4bced212b8a44ad1bc59bdf3", "shasum": "" }, "require": { @@ -5499,7 +5804,13 @@ "api", "graphql" ], - "time": "2019-08-25T10:32:47+00:00" + "funding": [ + { + "url": "https://opencollective.com/webonyx-graphql-php", + "type": "open_collective" + } + ], + "time": "2020-07-02T05:49:25+00:00" }, { "name": "wikimedia/less.php", @@ -5720,16 +6031,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.138.7", + "version": "3.147.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "6b9f3fcea4dfa6092c628c790ca6d369a75453b7" + "reference": "8a561a4a1645ccdd06413a4f2defe55d35e0eecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6b9f3fcea4dfa6092c628c790ca6d369a75453b7", - "reference": "6b9f3fcea4dfa6092c628c790ca6d369a75453b7", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8a561a4a1645ccdd06413a4f2defe55d35e0eecc", + "reference": "8a561a4a1645ccdd06413a4f2defe55d35e0eecc", "shasum": "" }, "require": { @@ -5752,6 +6063,7 @@ "ext-pcntl": "*", "ext-sockets": "*", "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", "phpunit/phpunit": "^4.8.35|^5.4.3", "psr/cache": "^1.0", "psr/simple-cache": "^1.0", @@ -5800,7 +6112,7 @@ "s3", "sdk" ], - "time": "2020-05-22T18:11:09+00:00" + "time": "2020-07-20T18:18:31+00:00" }, { "name": "beberlei/assert", @@ -6018,16 +6330,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.4", + "version": "4.1.6", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "55d8d1d882fa0777e47de17b04c29b3c50fe29e7" + "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/55d8d1d882fa0777e47de17b04c29b3c50fe29e7", - "reference": "55d8d1d882fa0777e47de17b04c29b3c50fe29e7", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", + "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", "shasum": "" }, "require": { @@ -6105,7 +6417,7 @@ "type": "open_collective" } ], - "time": "2020-03-23T17:07:20+00:00" + "time": "2020-06-07T16:31:51+00:00" }, { "name": "codeception/lib-asserts", @@ -6249,16 +6561,16 @@ }, { "name": "codeception/module-webdriver", - "version": "1.0.8", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "da55466876d9e73c09917f495b923395b1cdf92a" + "reference": "09c167817393090ce3dbce96027d94656b1963ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/da55466876d9e73c09917f495b923395b1cdf92a", - "reference": "da55466876d9e73c09917f495b923395b1cdf92a", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/09c167817393090ce3dbce96027d94656b1963ce", + "reference": "09c167817393090ce3dbce96027d94656b1963ce", "shasum": "" }, "require": { @@ -6300,7 +6612,7 @@ "browser-testing", "codeception" ], - "time": "2020-04-29T13:45:52+00:00" + "time": "2020-05-31T08:47:24+00:00" }, { "name": "codeception/phpunit-wrapper", @@ -6348,16 +6660,16 @@ }, { "name": "codeception/stub", - "version": "3.6.1", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "a3ba01414cbee76a1bced9f9b6b169cc8d203880" + "reference": "468dd5fe659f131fc997f5196aad87512f9b1304" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/a3ba01414cbee76a1bced9f9b6b169cc8d203880", - "reference": "a3ba01414cbee76a1bced9f9b6b169cc8d203880", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/468dd5fe659f131fc997f5196aad87512f9b1304", + "reference": "468dd5fe659f131fc997f5196aad87512f9b1304", "shasum": "" }, "require": { @@ -6374,7 +6686,7 @@ "MIT" ], "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2020-02-07T18:42:28+00:00" + "time": "2020-07-03T15:54:43+00:00" }, { "name": "csharpru/vault-php", @@ -6530,22 +6842,22 @@ }, { "name": "doctrine/annotations", - "version": "1.10.2", + "version": "1.10.3", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "b9d758e831c70751155c698c2f7df4665314a1cb" + "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/b9d758e831c70751155c698c2f7df4665314a1cb", - "reference": "b9d758e831c70751155c698c2f7df4665314a1cb", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/5db60a4969eba0e0c197a19c077780aadbc43c5d", + "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d", "shasum": "" }, "require": { "doctrine/lexer": "1.*", "ext-tokenizer": "*", - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "require-dev": { "doctrine/cache": "1.*", @@ -6595,24 +6907,24 @@ "docblock", "parser" ], - "time": "2020-04-20T09:18:32+00:00" + "time": "2020-05-25T17:24:27+00:00" }, { "name": "doctrine/cache", - "version": "1.10.0", + "version": "1.10.2", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62" + "reference": "13e3381b25847283a91948d04640543941309727" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/382e7f4db9a12dc6c19431743a2b096041bcdd62", - "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62", + "url": "https://api.github.com/repos/doctrine/cache/zipball/13e3381b25847283a91948d04640543941309727", + "reference": "13e3381b25847283a91948d04640543941309727", "shasum": "" }, "require": { - "php": "~7.1" + "php": "~7.1 || ^8.0" }, "conflict": { "doctrine/common": ">2.2,<2.4" @@ -6677,7 +6989,21 @@ "redis", "xcache" ], - "time": "2019-11-29T15:36:20+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2020-07-07T18:54:01+00:00" }, { "name": "doctrine/inflector", @@ -6748,20 +7074,20 @@ }, { "name": "doctrine/instantiator", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^6.0", @@ -6800,24 +7126,38 @@ "constructor", "instantiate" ], - "time": "2019-10-21T16:45:58+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-05-29T17:27:14+00:00" }, { "name": "doctrine/lexer", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6" + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", - "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", "shasum": "" }, "require": { - "php": "^7.2" + "php": "^7.2 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^6.0", @@ -6862,20 +7202,34 @@ "parser", "php" ], - "time": "2019-10-30T14:39:59+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2020-05-25T17:44:05+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.16.3", + "version": "v2.16.4", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "83baf823a33a1cbd5416c8626935cf3f843c10b0" + "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/83baf823a33a1cbd5416c8626935cf3f843c10b0", - "reference": "83baf823a33a1cbd5416c8626935cf3f843c10b0", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1023c3458137ab052f6ff1e09621a721bfdeca13", + "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13", "shasum": "" }, "require": { @@ -6907,12 +7261,12 @@ "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", "phpunitgoodpractices/traits": "^1.8", - "symfony/phpunit-bridge": "^4.3 || ^5.0", + "symfony/phpunit-bridge": "^5.1", "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, "suggest": { "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters in cache signature.", + "ext-mbstring": "For handling non-UTF8 characters.", "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." @@ -6959,51 +7313,600 @@ "type": "github" } ], - "time": "2020-04-15T18:51:10+00:00" + "time": "2020-06-27T23:57:46+00:00" }, { - "name": "jms/metadata", - "version": "1.7.0", + "name": "hoa/consistency", + "version": "1.17.05.02", "source": { "type": "git", - "url": "https://github.com/schmittjoh/metadata.git", - "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8" + "url": "https://github.com/hoaproject/Consistency.git", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/e5854ab1aa643623dc64adde718a8eec32b957a8", - "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8", + "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", "shasum": "" }, "require": { - "php": ">=5.3.0" + "hoa/exception": "~1.0", + "php": ">=5.5.0" }, "require-dev": { - "doctrine/cache": "~1.0", - "symfony/cache": "~3.1" + "hoa/stream": "~1.0", + "hoa/test": "~2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { - "psr-0": { - "Metadata\\": "src/" - } + "psr-4": { + "Hoa\\Consistency\\": "." + }, + "files": [ + "Prelude.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" }, { - "name": "Johannes M. Schmitt", + "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", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/metadata.git", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/e5854ab1aa643623dc64adde718a8eec32b957a8", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "doctrine/cache": "~1.0", + "symfony/cache": "~3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Metadata\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, + { + "name": "Johannes M. Schmitt", "email": "schmittjoh@gmail.com" } ], @@ -7217,12 +8120,6 @@ "sftp", "storage" ], - "funding": [ - { - "url": "https://offset.earth/frankdejonge", - "type": "other" - } - ], "time": "2020-05-18T15:13:39+00:00" }, { @@ -7333,16 +8230,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "dev-3.0.0-RC3", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "aea30ae1df2fe6618478ba8813864c204561fde3" + "reference": "8a106ea029f222f4354854636861273c7577bee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/aea30ae1df2fe6618478ba8813864c204561fde3", - "reference": "aea30ae1df2fe6618478ba8813864c204561fde3", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8a106ea029f222f4354854636861273c7577bee9", + "reference": "8a106ea029f222f4354854636861273c7577bee9", "shasum": "" }, "require": { @@ -7360,6 +8257,7 @@ "ext-intl": "*", "ext-json": "*", "ext-openssl": "*", + "hoa/console": "~3.0", "monolog/monolog": "^1.17", "mustache/mustache": "~2.5", "php": "^7.3", @@ -7367,9 +8265,11 @@ "spomky-labs/otphp": "^10.0", "symfony/console": "^4.4", "symfony/finder": "^5.0", + "symfony/http-foundation": "^5.0", "symfony/mime": "^5.0", "symfony/process": "^4.4", - "vlucas/phpdotenv": "^2.4" + "vlucas/phpdotenv": "^2.4", + "weew/helpers-array": "^1.3" }, "replace": { "facebook/webdriver": "^1.7.1" @@ -7417,7 +8317,7 @@ "magento", "testing" ], - "time": "2020-05-22T19:17:05+00:00" + "time": "2020-08-19T19:57:27+00:00" }, { "name": "mikey179/vfsstream", @@ -7570,20 +8470,20 @@ }, { "name": "myclabs/deep-copy", - "version": "1.9.5", + "version": "1.10.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef" + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef", - "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "replace": { "myclabs/deep-copy": "self.version" @@ -7614,7 +8514,13 @@ "object", "object graph" ], - "time": "2020-01-17T21:11:47+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-06-29T13:22:24+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -8051,25 +8957,25 @@ }, { "name": "phpdocumentor/reflection-common", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b" + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b", - "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", "shasum": "" }, "require": { - "php": ">=7.1" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -8096,32 +9002,31 @@ "reflection", "static analysis" ], - "time": "2020-04-27T09:25:28+00:00" + "time": "2020-06-27T09:03:43+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.1.0", + "version": "5.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" + "reference": "3170448f5769fe19f456173d833734e0ff1b84df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", - "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/3170448f5769fe19f456173d833734e0ff1b84df", + "reference": "3170448f5769fe19f456173d833734e0ff1b84df", "shasum": "" }, "require": { - "ext-filter": "^7.1", - "php": "^7.2", - "phpdocumentor/reflection-common": "^2.0", - "phpdocumentor/type-resolver": "^1.0", - "webmozart/assert": "^1" + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" }, "require-dev": { - "doctrine/instantiator": "^1", - "mockery/mockery": "^1" + "mockery/mockery": "~1.3.2" }, "type": "library", "extra": { @@ -8149,34 +9054,33 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-02-22T12:28:44+00:00" + "time": "2020-07-20T20:05:34+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.1.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95" + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/7462d5f123dfc080dfdf26897032a6513644fc95", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", "shasum": "" }, "require": { - "php": "^7.2", + "php": "^7.2 || ^8.0", "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "ext-tokenizer": "^7.2", - "mockery/mockery": "~1" + "ext-tokenizer": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-1.x": "1.x-dev" } }, "autoload": { @@ -8195,7 +9099,7 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-02-18T18:59:58+00:00" + "time": "2020-06-27T10:12:23+00:00" }, { "name": "phpmd/phpmd", @@ -8269,24 +9173,24 @@ }, { "name": "phpoption/phpoption", - "version": "1.7.3", + "version": "1.7.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "4acfd6a4b33a509d8c88f50e5222f734b6aeebae" + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/4acfd6a4b33a509d8c88f50e5222f734b6aeebae", - "reference": "4acfd6a4b33a509d8c88f50e5222f734b6aeebae", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/994ecccd8f3283ecf5ac33254543eb0ac946d525", + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525", "shasum": "" }, "require": { "php": "^5.5.9 || ^7.0 || ^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.3", - "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0" + "bamarni/composer-bin-plugin": "^1.4.1", + "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { @@ -8320,37 +9224,47 @@ "php", "type" ], - "time": "2020-03-21T18:07:53+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2020-07-20T17:29:33+00:00" }, { "name": "phpspec/prophecy", - "version": "v1.10.3", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "451c3cd1418cf640de218914901e51b064abb093" + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", - "reference": "451c3cd1418cf640de218914901e51b064abb093", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", - "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + "doctrine/instantiator": "^1.2", + "php": "^7.2", + "phpdocumentor/reflection-docblock": "^5.0", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { - "phpspec/phpspec": "^2.5 || ^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10.x-dev" + "dev-master": "1.11.x-dev" } }, "autoload": { @@ -8383,7 +9297,7 @@ "spy", "stub" ], - "time": "2020-03-05T15:02:03+00:00" + "time": "2020-07-08T12:44:21+00:00" }, { "name": "phpstan/phpstan", @@ -8425,34 +9339,20 @@ "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" }, { "name": "phpunit/php-code-coverage", - "version": "8.0.1", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "31e94ccc084025d6abee0585df533eb3a792b96a" + "reference": "ca6647ffddd2add025ab3f21644a441d7c146cdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/31e94ccc084025d6abee0585df533eb3a792b96a", - "reference": "31e94ccc084025d6abee0585df533eb3a792b96a", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca6647ffddd2add025ab3f21644a441d7c146cdc", + "reference": "ca6647ffddd2add025ab3f21644a441d7c146cdc", "shasum": "" }, "require": { @@ -8503,24 +9403,30 @@ "testing", "xunit" ], - "time": "2020-02-19T13:41:19+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-05-23T08:02:54+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.1", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "4ac5b3e13df14829daa60a2eb4fdd2f2b7d33cf4" + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4ac5b3e13df14829daa60a2eb4fdd2f2b7d33cf4", - "reference": "4ac5b3e13df14829daa60a2eb4fdd2f2b7d33cf4", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/25fefc5b19835ca653877fe081644a3f8c1d915e", + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -8559,24 +9465,24 @@ "type": "github" } ], - "time": "2020-04-18T05:02:12+00:00" + "time": "2020-07-11T05:18:21+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "7579d5a1ba7f3ac11c80004d205877911315ae7a" + "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7579d5a1ba7f3ac11c80004d205877911315ae7a", - "reference": "7579d5a1ba7f3ac11c80004d205877911315ae7a", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f6eedfed1085dd1f4c599629459a0277d25f9a66", + "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "ext-pcntl": "*", @@ -8612,24 +9518,33 @@ "keywords": [ "process" ], - "time": "2020-02-07T06:06:11+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:53:53+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "526dc996cc0ebdfa428cd2dfccd79b7b53fee346" + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/526dc996cc0ebdfa428cd2dfccd79b7b53fee346", - "reference": "526dc996cc0ebdfa428cd2dfccd79b7b53fee346", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { @@ -8658,7 +9573,13 @@ "keywords": [ "template" ], - "time": "2020-02-01T07:43:44+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:55:37+00:00" }, { "name": "phpunit/php-timer", @@ -8707,31 +9628,25 @@ "keywords": [ "timer" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-04-20T06:00:37+00:00" }, { "name": "phpunit/php-token-stream", - "version": "4.0.1", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "cdc0db5aed8fbfaf475fbd95bfd7bab83c7a779c" + "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/cdc0db5aed8fbfaf475fbd95bfd7bab83c7a779c", - "reference": "cdc0db5aed8fbfaf475fbd95bfd7bab83c7a779c", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5672711b6b07b14d5ab694e700c62eeb82fcf374", + "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -8768,7 +9683,8 @@ "type": "github" } ], - "time": "2020-05-06T09:56:31+00:00" + "abandoned": true, + "time": "2020-06-27T06:36:25+00:00" }, { "name": "phpunit/phpunit", @@ -8856,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" }, { @@ -8964,20 +9870,20 @@ }, { "name": "sebastian/code-unit", - "version": "1.0.2", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ac958085bc19fcd1d36425c781ef4cbb5b06e2a5" + "reference": "c1e2df332c905079980b119c4db103117e5e5c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ac958085bc19fcd1d36425c781ef4cbb5b06e2a5", - "reference": "ac958085bc19fcd1d36425c781ef4cbb5b06e2a5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/c1e2df332c905079980b119c4db103117e5e5c90", + "reference": "c1e2df332c905079980b119c4db103117e5e5c90", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -9012,24 +9918,24 @@ "type": "github" } ], - "time": "2020-04-30T05:58:10+00:00" + "time": "2020-06-26T12:50:45+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "5b5dbe0044085ac41df47e79d34911a15b96d82e" + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5b5dbe0044085ac41df47e79d34911a15b96d82e", - "reference": "5b5dbe0044085ac41df47e79d34911a15b96d82e", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ee51f9bb0c6d8a43337055db3120829fa14da819", + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -9057,24 +9963,30 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2020-02-07T06:20:13+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:04:00+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.0", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "85b3435da967696ed618ff745f32be3ff4a2b8e8" + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85b3435da967696ed618ff745f32be3ff4a2b8e8", - "reference": "85b3435da967696ed618ff745f32be3ff4a2b8e8", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", "shasum": "" }, "require": { - "php": "^7.3", + "php": "^7.3 || ^8.0", "sebastian/diff": "^4.0", "sebastian/exporter": "^4.0" }, @@ -9121,24 +10033,30 @@ "compare", "equality" ], - "time": "2020-02-07T06:08:51+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:05:46+00:00" }, { "name": "sebastian/diff", - "version": "4.0.1", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3e523c576f29dacecff309f35e4cc5a5c168e78a" + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3e523c576f29dacecff309f35e4cc5a5c168e78a", - "reference": "3e523c576f29dacecff309f35e4cc5a5c168e78a", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0", @@ -9183,24 +10101,24 @@ "type": "github" } ], - "time": "2020-05-08T05:01:12+00:00" + "time": "2020-06-30T04:46:02+00:00" }, { "name": "sebastian/environment", - "version": "5.1.0", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "c753f04d68cd489b6973cf9b4e505e191af3b05c" + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/c753f04d68cd489b6973cf9b4e505e191af3b05c", - "reference": "c753f04d68cd489b6973cf9b4e505e191af3b05c", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -9242,29 +10160,29 @@ "type": "github" } ], - "time": "2020-04-14T13:36:52+00:00" + "time": "2020-06-26T12:07:24+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.0", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "80c26562e964016538f832f305b2286e1ec29566" + "reference": "571d721db4aec847a0e59690b954af33ebf9f023" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/80c26562e964016538f832f305b2286e1ec29566", - "reference": "80c26562e964016538f832f305b2286e1ec29566", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/571d721db4aec847a0e59690b954af33ebf9f023", + "reference": "571d721db4aec847a0e59690b954af33ebf9f023", "shasum": "" }, "require": { - "php": "^7.3", + "php": "^7.3 || ^8.0", "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.2" }, "type": "library", "extra": { @@ -9309,7 +10227,13 @@ "export", "exporter" ], - "time": "2020-02-07T06:10:52+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:08:55+00:00" }, { "name": "sebastian/finder-facade", @@ -9355,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" }, { @@ -9413,20 +10338,20 @@ }, { "name": "sebastian/object-enumerator", - "version": "4.0.0", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "e67516b175550abad905dc952f43285957ef4363" + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67516b175550abad905dc952f43285957ef4363", - "reference": "e67516b175550abad905dc952f43285957ef4363", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/074fed2d0a6d08e1677dd8ce9d32aecb384917b8", + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8", "shasum": "" }, "require": { - "php": "^7.3", + "php": "^7.3 || ^8.0", "sebastian/object-reflector": "^2.0", "sebastian/recursion-context": "^4.0" }, @@ -9456,24 +10381,30 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2020-02-07T06:12:23+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:11:32+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "f4fd0835cabb0d4a6546d9fe291e5740037aa1e7" + "reference": "127a46f6b057441b201253526f81d5406d6c7840" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f4fd0835cabb0d4a6546d9fe291e5740037aa1e7", - "reference": "f4fd0835cabb0d4a6546d9fe291e5740037aa1e7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/127a46f6b057441b201253526f81d5406d6c7840", + "reference": "127a46f6b057441b201253526f81d5406d6c7840", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -9501,7 +10432,13 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "time": "2020-02-07T06:19:40+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:12:55+00:00" }, { "name": "sebastian/phpcpd", @@ -9556,20 +10493,20 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.0", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cdd86616411fc3062368b720b0425de10bd3d579" + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cdd86616411fc3062368b720b0425de10bd3d579", - "reference": "cdd86616411fc3062368b720b0425de10bd3d579", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/062231bf61d2b9448c4fa5a7643b5e1829c11d63", + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -9605,24 +10542,30 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2020-02-07T06:18:20+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:14:17+00:00" }, { "name": "sebastian/resource-operations", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98" + "reference": "0653718a5a629b065e91f774595267f8dc32e213" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98", - "reference": "8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0653718a5a629b065e91f774595267f8dc32e213", + "reference": "0653718a5a629b065e91f774595267f8dc32e213", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -9650,32 +10593,38 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2020-02-07T06:13:02+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:16:22+00:00" }, { "name": "sebastian/type", - "version": "2.0.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "9e8f42f740afdea51f5f4e8cec2035580e797ee1" + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/9e8f42f740afdea51f5f4e8cec2035580e797ee1", - "reference": "9e8f42f740afdea51f5f4e8cec2035580e797ee1", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/86991e2b33446cd96e648c18bcdb1e95afb2c05a", + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.2-dev" } }, "autoload": { @@ -9696,24 +10645,30 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", - "time": "2020-02-07T06:13:43+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-05T08:31:53+00:00" }, { "name": "sebastian/version", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "0411bde656dce64202b39c2f4473993a9081d39e" + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/0411bde656dce64202b39c2f4473993a9081d39e", - "reference": "0411bde656dce64202b39c2f4473993a9081d39e", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/626586115d0ed31cb71483be55beb759b5af5a3c", + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "type": "library", "extra": { @@ -9739,7 +10694,13 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2020-01-21T06:36:37+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:18:43+00:00" }, { "name": "spomky-labs/otphp", @@ -9865,22 +10826,24 @@ }, { "name": "symfony/config", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "db1674e1a261148429f123871f30d211992294e7" + "reference": "b8623ef3d99fe62a34baf7a111b576216965f880" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/db1674e1a261148429f123871f30d211992294e7", - "reference": "db1674e1a261148429f123871f30d211992294e7", + "url": "https://api.github.com/repos/symfony/config/zipball/b8623ef3d99fe62a34baf7a111b576216965f880", + "reference": "b8623ef3d99fe62a34baf7a111b576216965f880", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/filesystem": "^4.4|^5.0", - "symfony/polyfill-ctype": "~1.8" + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.15" }, "conflict": { "symfony/finder": "<4.4" @@ -9898,7 +10861,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -9939,29 +10902,31 @@ "type": "tidelift" } ], - "time": "2020-04-15T15:59:10+00:00" + "time": "2020-05-23T13:08:13+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "92d8b3bd896a87cdd8aba0a3dd041bc072e8cfba" + "reference": "6508423eded583fc07e88a0172803e1a62f0310c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/92d8b3bd896a87cdd8aba0a3dd041bc072e8cfba", - "reference": "92d8b3bd896a87cdd8aba0a3dd041bc072e8cfba", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6508423eded583fc07e88a0172803e1a62f0310c", + "reference": "6508423eded583fc07e88a0172803e1a62f0310c", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "psr/container": "^1.0", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1.6|^2" }, "conflict": { - "symfony/config": "<5.0", + "symfony/config": "<5.1", "symfony/finder": "<4.4", "symfony/proxy-manager-bridge": "<4.4", "symfony/yaml": "<4.4" @@ -9971,7 +10936,7 @@ "symfony/service-implementation": "1.0" }, "require-dev": { - "symfony/config": "^5.0", + "symfony/config": "^5.1", "symfony/expression-language": "^4.4|^5.0", "symfony/yaml": "^4.4|^5.0" }, @@ -9985,7 +10950,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -10026,43 +10991,38 @@ "type": "tidelift" } ], - "time": "2020-04-28T17:58:55+00:00" + "time": "2020-06-12T08:11:32+00:00" }, { - "name": "symfony/http-foundation", - "version": "v5.0.8", + "name": "symfony/deprecation-contracts", + "version": "v2.1.3", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "e47fdf8b24edc12022ba52923150ec6484d7f57d" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e47fdf8b24edc12022ba52923150ec6484d7f57d", - "reference": "e47fdf8b24edc12022ba52923150ec6484d7f57d", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5e20b83385a77593259c9f8beb2c43cd03b2ac14", + "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14", "shasum": "" }, "require": { - "php": "^7.2.5", - "symfony/mime": "^4.4|^5.0", - "symfony/polyfill-mbstring": "~1.1" - }, - "require-dev": { - "predis/predis": "~1.0", - "symfony/expression-language": "^4.4|^5.0" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "2.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } }, "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -10071,15 +11031,15 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony HttpFoundation Component", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "funding": [ { @@ -10095,43 +11055,46 @@ "type": "tidelift" } ], - "time": "2020-04-18T20:50:06+00:00" + "time": "2020-06-06T08:49:21+00:00" }, { - "name": "symfony/mime", - "version": "v5.0.8", + "name": "symfony/http-foundation", + "version": "v5.1.2", "source": { "type": "git", - "url": "https://github.com/symfony/mime.git", - "reference": "5d6c81c39225a750f3f43bee15f03093fb9aaa0b" + "url": "https://github.com/symfony/http-foundation.git", + "reference": "f93055171b847915225bd5b0a5792888419d8d75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/5d6c81c39225a750f3f43bee15f03093fb9aaa0b", - "reference": "5d6c81c39225a750f3f43bee15f03093fb9aaa0b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f93055171b847915225bd5b0a5792888419d8d75", + "reference": "f93055171b847915225bd5b0a5792888419d8d75", "shasum": "" }, "require": { - "php": "^7.2.5", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" - }, - "conflict": { - "symfony/mailer": "<4.4" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.15" }, "require-dev": { - "egulias/email-validator": "^2.1.10", - "symfony/dependency-injection": "^4.4|^5.0" + "predis/predis": "~1.0", + "symfony/cache": "^4.4|^5.0", + "symfony/expression-language": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Mime\\": "" + "Symfony\\Component\\HttpFoundation\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -10151,12 +11114,8 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "A library to manipulate MIME messages", + "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "keywords": [ - "mime", - "mime-type" - ], "funding": [ { "url": "https://symfony.com/sponsor", @@ -10171,34 +11130,44 @@ "type": "tidelift" } ], - "time": "2020-04-17T03:29:44+00:00" + "time": "2020-06-15T06:52:54+00:00" }, { - "name": "symfony/options-resolver", - "version": "v5.0.8", + "name": "symfony/mime", + "version": "v5.1.2", "source": { "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "3707e3caeff2b797c0bfaadd5eba723dd44e6bf1" + "url": "https://github.com/symfony/mime.git", + "reference": "c0c418f05e727606e85b482a8591519c4712cf45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/3707e3caeff2b797c0bfaadd5eba723dd44e6bf1", - "reference": "3707e3caeff2b797c0bfaadd5eba723dd44e6bf1", + "url": "https://api.github.com/repos/symfony/mime/zipball/c0c418f05e727606e85b482a8591519c4712cf45", + "reference": "c0c418f05e727606e85b482a8591519c4712cf45", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "symfony/mailer": "<4.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10", + "symfony/dependency-injection": "^4.4|^5.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" + "Symfony\\Component\\Mime\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -10218,12 +11187,11 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony OptionsResolver Component", + "description": "A library to manipulate MIME messages", "homepage": "https://symfony.com", "keywords": [ - "config", - "configuration", - "options" + "mime", + "mime-type" ], "funding": [ { @@ -10239,41 +11207,39 @@ "type": "tidelift" } ], - "time": "2020-04-06T10:40:56+00:00" + "time": "2020-06-09T15:07:35+00:00" }, { - "name": "symfony/polyfill-php70", - "version": "v1.17.0", + "name": "symfony/options-resolver", + "version": "v5.1.2", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "82225c2d7d23d7e70515496d249c0152679b468e" + "url": "https://github.com/symfony/options-resolver.git", + "reference": "663f5dd5e14057d1954fe721f9709d35837f2447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/82225c2d7d23d7e70515496d249c0152679b468e", - "reference": "82225c2d7d23d7e70515496d249c0152679b468e", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/663f5dd5e14057d1954fe721f9709d35837f2447", + "reference": "663f5dd5e14057d1954fe721f9709d35837f2447", "shasum": "" }, "require": { - "paragonie/random_compat": "~1.0|~2.0|~9.99", - "php": ">=5.3.3" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php80": "^1.15" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "5.1-dev" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php70\\": "" + "Symfony\\Component\\OptionsResolver\\": "" }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -10282,21 +11248,20 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "description": "Symfony OptionsResolver Component", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "config", + "configuration", + "options" ], "funding": [ { @@ -10312,30 +11277,30 @@ "type": "tidelift" } ], - "time": "2020-05-12T16:47:27+00:00" + "time": "2020-05-23T13:08:13+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "a1d86d30d4522423afc998f32404efa34fcf5a73" + "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/a1d86d30d4522423afc998f32404efa34fcf5a73", - "reference": "a1d86d30d4522423afc998f32404efa34fcf5a73", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/0f7c58cf81dbb5dd67d423a89d577524a2ec0323", + "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "symfony/service-contracts": "^1.0|^2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -10376,24 +11341,25 @@ "type": "tidelift" } ], - "time": "2020-03-27T16:56:45+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/yaml", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "482fb4e710e5af3e0e78015f19aa716ad953392f" + "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/482fb4e710e5af3e0e78015f19aa716ad953392f", - "reference": "482fb4e710e5af3e0e78015f19aa716ad953392f", + "url": "https://api.github.com/repos/symfony/yaml/zipball/ea342353a3ef4f453809acc4ebc55382231d4d23", + "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -10405,10 +11371,13 @@ "suggest": { "symfony/console": "For validating YAML files using the lint command" }, + "bin": [ + "Resources/bin/yaml-lint" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -10449,20 +11418,20 @@ "type": "tidelift" } ], - "time": "2020-04-28T17:58:55+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.1.1", + "version": "v1.1.3", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "04f9ffae372a9816d4472dfb7bcf6126b623a9df" + "reference": "9f277171e296a3c8629c04ac93ec95ff0f208ccb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/04f9ffae372a9816d4472dfb7bcf6126b623a9df", - "reference": "04f9ffae372a9816d4472dfb7bcf6126b623a9df", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/9f277171e296a3c8629c04ac93ec95ff0f208ccb", + "reference": "9f277171e296a3c8629c04ac93ec95ff0f208ccb", "shasum": "" }, "require": { @@ -10582,7 +11551,7 @@ "MIT" ], "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "time": "2020-05-04T15:25:36+00:00" + "time": "2020-07-10T09:34:29+00:00" }, { "name": "theseer/fdomdocument", @@ -10626,23 +11595,23 @@ }, { "name": "theseer/tokenizer", - "version": "1.1.3", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" + "reference": "75a63c33a8577608444246075ea0af0d052e452a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.0" + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { @@ -10662,30 +11631,36 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2019-06-13T22:48:21+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v2.6.4", + "version": "v2.6.6", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "67d472b1794c986381a8950e4958e1adb779d561" + "reference": "e1d57f62db3db00d9139078cbedf262280701479" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/67d472b1794c986381a8950e4958e1adb779d561", - "reference": "67d472b1794c986381a8950e4958e1adb779d561", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/e1d57f62db3db00d9139078cbedf262280701479", + "reference": "e1d57f62db3db00d9139078cbedf262280701479", "shasum": "" }, "require": { "php": "^5.3.9 || ^7.0 || ^8.0", - "symfony/polyfill-ctype": "^1.9" + "symfony/polyfill-ctype": "^1.17" }, "require-dev": { "ext-filter": "*", "ext-pcre": "*", - "phpunit/phpunit": "^4.8.35 || ^5.0" + "phpunit/phpunit": "^4.8.35 || ^5.7.27" }, "suggest": { "ext-filter": "Required to use the boolean validator.", @@ -10734,27 +11709,28 @@ "type": "tidelift" } ], - "time": "2020-05-02T13:38:00+00:00" + "time": "2020-07-14T17:54:18+00:00" }, { "name": "webmozart/assert", - "version": "1.8.0", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6" + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6", - "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0", + "php": "^5.3.3 || ^7.0 || ^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { + "phpstan/phpstan": "<0.12.20", "vimeo/psalm": "<3.9.1" }, "require-dev": { @@ -10782,7 +11758,7 @@ "check", "validate" ], - "time": "2020-04-18T12:12:48+00:00" + "time": "2020-07-08T17:02:28+00:00" }, { "name": "weew/helpers-array", @@ -10824,10 +11800,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "magento/composer": 20, - "magento/magento2-functional-testing-framework": 20 - }, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { 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/acceptance/tests/_data/gif.gif b/dev/tests/acceptance/tests/_data/gif.gif index 0b082504ab982..f0937bc132829 100644 Binary files a/dev/tests/acceptance/tests/_data/gif.gif and b/dev/tests/acceptance/tests/_data/gif.gif differ diff --git a/dev/tests/acceptance/tests/_data/magento3.jpg b/dev/tests/acceptance/tests/_data/magento3.jpg index 79ed12ec0aea4..6dc8cd69e41c1 100644 Binary files a/dev/tests/acceptance/tests/_data/magento3.jpg and b/dev/tests/acceptance/tests/_data/magento3.jpg differ diff --git a/dev/tests/acceptance/tests/_data/png.png b/dev/tests/acceptance/tests/_data/png.png index c83255dcf558d..4ec47267e8125 100644 Binary files a/dev/tests/acceptance/tests/_data/png.png and b/dev/tests/acceptance/tests/_data/png.png differ diff --git a/dev/tests/acceptance/tests/functional/Magento/ConfigurableProductCatalogSearch/etc/csp_whitelist.xml b/dev/tests/acceptance/tests/functional/Magento/ConfigurableProductCatalogSearch/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..35af3809cb225 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/ConfigurableProductCatalogSearch/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="script-src"> + <values> + <value id="signifyd_cdn" type="host">cdn-scripts.signifyd.com</value> + </values> + </policy> + </policies> +</csp_whitelist> 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/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php new file mode 100644 index 0000000000000..57a607feedb0c --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php @@ -0,0 +1,79 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Model; + +/** + * Class represent simple container to save data + */ +class FixtureCallStorage +{ + /** @var array */ + private $storage = []; + + /** + * Add fixture to storage + * + * @param string $fixture + * @return void + */ + public function addFixtureToStorage(string $fixture): void + { + $this->storage[] = $fixture; + } + + /** + * Get fixture position in storage + * + * @param string $fixture + * @return null|int + */ + public function getFixturePosition(string $fixture): ?int + { + return array_search($fixture, $this->storage) ?: null; + } + + /** + * Get storage + * + * @return array + */ + public function getStorage(): array + { + return $this->storage; + } + + /** + * Get fixtures count in storage + * + * @param string $fixture + * @return int + */ + public function getFixturesCount(string $fixture = ''): int + { + $count = count($this->storage); + if ($fixture) { + $result = array_filter($this->storage, function ($storedFixture) use ($fixture) { + return $storedFixture === $fixture; + }); + $count = count($result); + } + + return $count; + } + + /** + * Clear storage + * + * @return void + */ + public function clearStorage(): void + { + $this->storage = []; + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/composer.json b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/composer.json new file mode 100644 index 0000000000000..47ac2d4ac4a3b --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-override-config-test", + "description": "module for override config check", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleOverrideConfig" + ] + ] + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..8c0badac4b1d1 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="test_section" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <group id="test_group" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="field_1" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> + <field id="field_2" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> + <field id="field_3" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> + </group> + </section> + </system> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/config.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/config.xml new file mode 100644 index 0000000000000..3b2f2a1ddde1e --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/config.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:module:Magento_Store:etc/config.xsd"> + <default> + <test_section> + <test_group> + <field_1>1st field default value</field_1> + <field_2>2nd field default value</field_2> + <field_3>3rd field default value</field_3> + </test_group> + </test_section> + </default> + <websites> + <base> + <test_section> + <test_group> + <field_3>3rd field website scope default value</field_3> + </test_group> + </test_section> + </base> + </websites> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/module.xml new file mode 100644 index 0000000000000..f9d63847959df --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/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_TestModuleOverrideConfig" /> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/registration.php b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/registration.php new file mode 100644 index 0000000000000..16ffc73cef00f --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig', __DIR__); +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/Test/Api/_files/overrides.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/Test/Api/_files/overrides.xml new file mode 100644 index 0000000000000..bda41e51aa5c8 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/Test/Api/_files/overrides.xml @@ -0,0 +1,150 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<overrides> + <!-- test node determine to which class config inside node should be applied --> + <test class="Magento\TestModuleOverrideConfig\MagentoApiConfigFixture\AddFixtureTest"> + <!-- Node bellow will add magentoConfigFixture to fixtures list + 'scopeType' required attribute and accept such values: store|website + 'scopeCode' store|website code + skip 'scopeType' and 'scopeCode' attributes to set value in default scope + 'path' required attribute determine config path, 'value' attribute determine which value will be set for provided path + to add fixture to fixtures list + --> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" value="overridden value for full class"/> + <!-- method node determine to which test method config inside node should be applied --> + <method name="testAddFixtureToMethod"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" value="overridden value for method"/> + <!-- dataSet node determine for which data set config inside should be applied --> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" value="overridden value for data set"/> + </dataSet> + </method> + <method name="testAddFixtureOnWebsiteScope"> + <magentoConfigFixture scopeType="website" scopeCode="base" path="test_section/test_group/field_1" value="overridden value for method on website scope"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiConfigFixture\RemoveFixtureTest"> + <!-- 'remove' attribute accept bool values, if value set to 'true' this node will remove matching fixture from fixtures list--> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" remove="true"/> + <method name="testRemoveFixtureForMethod"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_2" remove="true"/> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_3" remove="true"/> + </dataSet> + </method> + <method name="testRemoveWebsiteScopeFixture"> + <magentoConfigFixture scopeType="website" scopeCode="base" path="test_section/test_group/field_3" remove="true"/> + </method> + <method name="testRemoveWebsiteScopeFixtureWithScopeCode"> + <magentoConfigFixture scopeType="website" scopeCode="base" path="test_section/test_group/field_3" remove="true"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiConfigFixture\ReplaceFixtureTest"> + <!-- Node bellow will replace value for matching fixture + 'newValue' attribute determine to which value current value in matching fixture should be replaced --> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for class"/> + <method name="testReplaceFixtureForMethod"> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for data set"/> + </dataSet> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for method"/> + </method> + <method name="testReplaceFixtureViaThirdModule" > + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for data set from second module"/> + </dataSet> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for method from second module"/> + </method> + <method name="testReplaceWebsiteScopedFixture"> + <magentoConfigFixture scopeType="website" scopeCode="base" path="test_section/test_group/field_1" newValue="Overridden value for website scope"/> + </method> + <method name="testReplaceDefaultConfig"> + <magentoConfigFixture path="test_section/test_group/field_1" newValue="Overridden value for default scope"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\AddFixtureTest"> + <!-- 'path' attribute determine path to fixture for which config should be applied + if only this attribute specified the fixture with such path will be applied --> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php"/> + <method name="testAddFixtures"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php"/> + <dataSet name="first_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php"/> + </dataSet> + </method> + <method name="testAddSameFixtures"> + <!-- Few same data fixtures can be applied for one test --> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php"/> + </method> + <method name="testAddFixtureWithRequiredFixture"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture_with_required_fixture.php"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\RemoveFixtureTest"> + <!-- 'remove' attribute support boolean values, to remove fixture with specified path you need to set this 'remove' attribute to 'true' --> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" remove="true"/> + <method name="testRemoveFixtureForMethod"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <dataSet name="second_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + </dataSet> + </method> + <method name="testRemoveSameFixtures"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\ReplaceFixtureTest"> + <!-- Node bellow will call specified in 'newPath' attribute fixture instead of fixture specified in 'path' attribute + if such fixture exist in fixtures list --> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <!-- If you specify data fixture to replace you should also specify rollback fixture in the separate node--> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + + <method name="testReplaceFixturesForMethod"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <dataSet name="second_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" /> + </dataSet> + </method> + <method name="testReplaceFixtureViaThirdModule"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <dataSet name="first_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" /> + </dataSet> + </method> + <method name="testReplaceRequiredFixture"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\SortFixturesTest"> + <!-- 'after' attribute determine after which fixture current fixture should be placed, '-' value means that fixture shold be placed after all --> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" after="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <method name="testSortFixtures"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" after="-"/> + <dataSet name="first_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" before="-"/> + </dataSet> + </method> + </test> + <!-- 'skip' attribute accept boolean values and will mark test as skipped test for which it specified if value set to 'true'--> + <test class="Magento\TestModuleOverrideConfig\Skip\SkipClassTest" skip="true"/> + <test class="Magento\TestModuleOverrideConfig\Skip\SkipMethodTest"> + <method name="testMethodSkip" skip="true"/> + </test> + <test class="Magento\TestModuleOverrideConfig\Skip\SkipDataSetTest"> + <method name="testSkipDataSet"> + <dataSet name="first_data_set" skip="true"/> + </method> + </test> +</overrides> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/composer.json b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/composer.json new file mode 100644 index 0000000000000..43b7bec56945d --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-override-config2-test", + "description": "module for override config check", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleOverrideConfig2" + ] + ] + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/etc/module.xml new file mode 100644 index 0000000000000..6432681d22e1d --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestModuleOverrideConfig2"> + <sequence> + <module name="Magento_TestModuleOverrideConfig"/> + </sequence> + </module> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/registration.php b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/registration.php new file mode 100644 index 0000000000000..ac3f1763eb93b --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig2') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig2', __DIR__); +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml new file mode 100644 index 0000000000000..b0b114890041b --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<overrides> + <test class="Magento\TestModuleOverrideConfig\MagentoApiConfigFixture\ReplaceFixtureTest"> + <method name="testReplaceFixtureViaThirdModule"> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for data set from third module"/> + </dataSet> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for method from third module"/> + </method> + </test> + + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\AddFixtureTest"> + <method name="testAddSameFixtures"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixtureBeforeTransaction\AddFixtureTest"> + <method name="testAddSameFixtures"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\ReplaceFixtureTest"> + <method name="testReplaceFixtureViaThirdModule"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> + <dataSet name="first_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + </dataSet> + </method> + <method name="testReplaceRequiredFixtureViaThirdModule"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" newPath="Magento/TestModuleOverrideConfig3/_files/fixture1_third_module.php" /> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\SortFixturesTest"> + <method name="testSortFixtures"> + <dataSet name="first_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig3/_files/fixture1_third_module.php" before="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" /> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesInterface"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" value="overridden config fixture value for class"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <method name="testInterfaceInheritance"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_2" newValue="overridden config fixture value for method"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_3" remove="true"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesAbstractClass"> + <method name="testAbstractInheritance"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_2" remove="true"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <dataSet name="first_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_3" value="overridden config fixture value for data set from abstract"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php"/> + </dataSet> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="overridden config fixture value for data set from abstract"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipAbstractClass"> + <method name="testAbstractSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="first_data_set" skip="true"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipInterface"> + <method name="testInterfaceSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="second_data_set" skip="true"/> + </method> + </test> +</overrides> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/composer.json b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/composer.json new file mode 100644 index 0000000000000..432b2ef703a57 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-override-config3-test", + "description": "module for override config check", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleOverrideConfig3" + ] + ] + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/etc/module.xml new file mode 100644 index 0000000000000..ef3693fd036f3 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestModuleOverrideConfig3"> + <sequence> + <module name="Magento_TestModuleOverrideConfig2"/> + </sequence> + </module> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/registration.php b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/registration.php new file mode 100644 index 0000000000000..e3217d8f97e8a --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig3') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig3', __DIR__); +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php index 8061cb138660d..3ef6e6618c6c5 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php @@ -7,13 +7,14 @@ namespace Magento\TestFramework\Annotation; -use Magento\Config\Model\Config; use Magento\Config\Model\ResourceModel\Config as ConfigResource; -use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; +use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\TestFramework\Helper\Bootstrap; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; -use PHPUnit\Framework\TestCase; +use Magento\TestFramework\App\ApiMutableScopeConfig; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestFramework\Helper\Bootstrap; /** * @inheritDoc @@ -21,167 +22,162 @@ class ApiConfigFixture extends ConfigFixture { /** - * Original values for global configuration options that need to be restored + * Values need to be deleted form the database * * @var array */ - private $_globalConfigValues = []; + private $valuesToDeleteFromDatabase = []; /** - * Original values for store-scoped configuration options that need to be restored - * - * @var array + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - private $_storeConfigValues = []; + protected function setStoreConfigValue(array $matches, $configPathAndValue): void + { + $storeCode = $matches[0]; + [$configScope, $configPath, $requiredValue] = preg_split('/\s+/', $configPathAndValue, 3); + /** @var ConfigStorage $configStorage */ + $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); + if (!$configStorage->checkIsRecordExist($configPath, ScopeInterface::SCOPE_STORES, $storeCode)) { + $this->valuesToDeleteFromDatabase[$storeCode][$configPath ?? ''] = $requiredValue ?? ''; + } + + parent::setStoreConfigValue($matches, $configPathAndValue); + } /** - * Values need to be deleted form the database - * - * @var array + * @inheritdoc */ - private $_valuesToDeleteFromDatabase = []; + protected function setGlobalConfigValue($configPathAndValue): void + { + [$configPath, $requiredValue] = preg_split('/\s+/', $configPathAndValue, 2); + /** @var ConfigStorage $configStorage */ + $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); + if (!$configStorage->checkIsRecordExist($configPath)) { + $this->valuesToDeleteFromDatabase['global'][$configPath] = $requiredValue; + } + + $originalValue = $this->getScopeConfigValue($configPath, ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + $this->globalConfigValues[$configPath] = $originalValue; + $this->_setConfigValue($configPath, $requiredValue); + } /** - * Assign required config values and save original ones - * - * @param TestCase $test + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - protected function _assignConfigData(TestCase $test) + protected function setWebsiteConfigValue(array $matches, $configPathAndValue): void { - $annotations = $test->getAnnotations(); - if (!isset($annotations['method'][$this->annotation])) { - return; + $websiteCode = $matches[0]; + [$configScope, $configPath, $requiredValue] = preg_split('/\s+/', $configPathAndValue, 3); + /** @var ConfigStorage $configStorage */ + $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); + if (!$configStorage->checkIsRecordExist($configPath, ScopeInterface::SCOPE_WEBSITES, $websiteCode)) { + $this->valuesToDeleteFromDatabase[$websiteCode][$configPath ?? ''] = $requiredValue ?? ''; } - foreach ($annotations['method'][$this->annotation] as $configPathAndValue) { - if (preg_match('/^.+?(?=_store\s)/', $configPathAndValue, $matches)) { - /* Store-scoped config value */ - $storeCode = $matches[0]; - $parts = preg_split('/\s+/', $configPathAndValue, 3); - list($configScope, $configPath, $requiredValue) = $parts + ['', '', '']; - $originalValue = $this->_getConfigValue($configPath, $storeCode); - $this->_storeConfigValues[$storeCode][$configPath] = $originalValue; - if ($this->checkIfValueExist($configPath, $storeCode)) { - $this->_valuesToDeleteFromDatabase[$storeCode][$configPath] = $requiredValue; - } - $this->_setConfigValue($configPath, $requiredValue, $storeCode); - } else { - /* Global config value */ - list($configPath, $requiredValue) = preg_split('/\s+/', $configPathAndValue, 2); - - $originalValue = $this->_getConfigValue($configPath); - $this->_globalConfigValues[$configPath] = $originalValue; - if ($this->checkIfValueExist($configPath)) { - $this->_valuesToDeleteFromDatabase['global'][$configPath] = $requiredValue; - } - $this->_setConfigValue($configPath, $requiredValue); - } - } + parent::setWebsiteConfigValue($matches, $configPathAndValue); } /** - * Restore original values for changed config options + * @inheritDoc + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _restoreConfigData() { + /** @var ConfigResource $configResource */ $configResource = Bootstrap::getObjectManager()->get(ConfigResource::class); - /* Restore global values */ - foreach ($this->_globalConfigValues as $configPath => $originalValue) { - if (isset($this->_valuesToDeleteFromDatabase['global'][$configPath])) { + foreach ($this->globalConfigValues as $configPath => $originalValue) { + if (isset($this->valuesToDeleteFromDatabase['global'][$configPath])) { $configResource->deleteConfig($configPath); } else { $this->_setConfigValue($configPath, $originalValue); } } - $this->_globalConfigValues = []; - + $this->globalConfigValues = []; /* Restore store-scoped values */ - foreach ($this->_storeConfigValues as $storeCode => $originalData) { + foreach ($this->storeConfigValues as $storeCode => $originalData) { foreach ($originalData as $configPath => $originalValue) { - if (empty($storeCode)) { - $storeCode = null; + $storeCode = $storeCode ?: null; + if (isset($this->valuesToDeleteFromDatabase[$storeCode][$configPath])) { + $scopeId = $this->getIdByScopeType(ScopeInterface::SCOPE_STORES, $storeCode); + $configResource->deleteConfig($configPath, ScopeInterface::SCOPE_STORES, $scopeId); + } else { + $this->setScopeConfigValue( + $configPath, + $originalValue, + ScopeInterface::SCOPE_STORES, + $storeCode + ); } - if (isset($this->_valuesToDeleteFromDatabase[$storeCode][$configPath])) { - $scopeId = $this->getStoreIdByCode($storeCode); - $configResource->deleteConfig($configPath, 'stores', $scopeId); + } + } + $this->storeConfigValues = []; + /* Restore website-scoped values */ + foreach ($this->websiteConfigValues as $websiteCode => $originalData) { + foreach ($originalData as $configPath => $originalValue) { + $websiteCode = $websiteCode ?: null; + if (isset($this->valuesToDeleteFromDatabase[$websiteCode][$configPath])) { + $scopeId = $this->getIdByScopeType(ScopeInterface::SCOPE_WEBSITES, $websiteCode); + $configResource->deleteConfig($configPath, ScopeInterface::SCOPE_WEBSITES, $scopeId); } else { - $this->_setConfigValue($configPath, $originalValue, $storeCode); + $this->setScopeConfigValue( + $configPath, + $originalValue, + ScopeInterface::SCOPE_WEBSITES, + $websiteCode + ); } } } - $this->_storeConfigValues = []; + $this->websiteConfigValues = []; } /** - * Load configs by path and scope - * - * @param string $configPath - * @param string $storeCode - * @return Config[] + * @inheritdoc */ - private function loadConfigs(string $configPath, string $storeCode = null): array + protected function getMutableScopeConfig(): MutableScopeConfigInterface { - $configCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); - $collection = $configCollectionFactory->create(); - $scope = $storeCode ? 'stores' : 'default'; - $scopeId = $storeCode ? $this->getStoreIdByCode($storeCode) : 0; - - $collection->addScopeFilter($scope, $scopeId, $configPath); - return $collection->getItems(); + return Bootstrap::getObjectManager() + ->get(ApiMutableScopeConfig::class); } /** - * Check if config exist in the database - * - * @param string $configPath - * @param string|null $storeCode + * @inheritdoc */ - private function checkIfValueExist(string $configPath, string $storeCode = null): bool + protected function getScopeConfigValue(string $configPath, string $scopeType, string $scopeCode = null): ?string { - $configs = $this->loadConfigs($configPath, $storeCode); + /** @var ConfigStorage $configStorage */ + $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); + $result = $configStorage->getValueFromDb($configPath, $scopeType, $scopeCode); - return !(bool)$configs; + return $result ?: null; } /** - * Returns the store ID by the store code + * Get id by code * - * @param string $storeCode + * @param string $scopeType + * @param string|null $scopeId * @return int */ - private function getStoreIdByCode(string $storeCode): int + private function getIdByScopeType(string $scopeType, ?string $scopeId): int { + $id = 0; + /** @var StoreManagerInterface $storeManager */ $storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); - $store = $storeManager->getStore($storeCode); - return (int)$store->getId(); - } - - /** - * @inheritDoc - */ - protected function _setConfigValue($configPath, $value, $storeCode = false) - { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - if ($storeCode === false) { - $objectManager->get( - \Magento\TestFramework\App\ApiMutableScopeConfig::class - )->setValue( - $configPath, - $value, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT - ); - - return; + switch ($scopeType) { + case ScopeInterface::SCOPE_WEBSITES: + $id = (int)$storeManager->getWebsite($scopeId)->getId(); + break; + case ScopeInterface::SCOPE_STORES: + $id = (int)$storeManager->getStore($scopeId)->getId(); + break; + default: + break; } - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\TestFramework\App\ApiMutableScopeConfig::class - )->setValue( - $configPath, - $value, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $storeCode - ); + + return $id; } } 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 88ac682f6b282..59aa2e7c719bf 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php @@ -12,157 +12,49 @@ namespace Magento\TestFramework\Annotation; -class ApiDataFixture -{ - /** - * @var string - */ - protected $_fixtureBaseDir; - - /** - * Fixtures that have been applied - * - * @var array - */ - private $_appliedFixtures = []; +use Magento\Customer\Model\Metadata\AttributeMetadataCache; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; - /** - * Constructor - * - * @param string $fixtureBaseDir - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function __construct($fixtureBaseDir) - { - if (!is_dir($fixtureBaseDir)) { - throw new \Magento\Framework\Exception\LocalizedException( - __("Fixture base directory '%1' does not exist.", $fixtureBaseDir) - ); - } - $this->_fixtureBaseDir = realpath($fixtureBaseDir); - } +/** + * Implementation of the @magentoApiDataFixture DocBlock annotation. + */ +class ApiDataFixture extends DataFixture +{ + public const ANNOTATION = 'magentoApiDataFixture'; /** * Handler for 'startTest' event * - * @param \PHPUnit\Framework\TestCase $test + * @param TestCase $test */ - public function startTest(\PHPUnit\Framework\TestCase $test) + public function startTest(TestCase $test) { - \Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); + Bootstrap::getInstance()->reinitialize(); /** Apply method level fixtures if thy are available, apply class level fixtures otherwise */ - $this->_applyFixtures($this->_getFixtures('method', $test) ?: $this->_getFixtures('class', $test)); + $this->_applyFixtures( + $this->_getFixtures($test, 'method') ?: $this->_getFixtures($test, 'class'), + $test + ); } /** * Handler for 'endTest' event - */ - public function endTest() - { - $this->_revertFixtures(); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $objectManager->get(\Magento\Customer\Model\Metadata\AttributeMetadataCache::class)->clean(); - } - - /** - * Retrieve fixtures from annotation - * - * @param string $scope 'class' or 'method' - * @param \PHPUnit\Framework\TestCase $test - * @return array - * @throws \Magento\Framework\Exception\LocalizedException - */ - protected function _getFixtures($scope, \PHPUnit\Framework\TestCase $test) - { - $annotations = $test->getAnnotations(); - $result = []; - if (!empty($annotations[$scope]['magentoApiDataFixture'])) { - foreach ($annotations[$scope]['magentoApiDataFixture'] as $fixture) { - if (strpos($fixture, '\\') !== false) { - // usage of a single directory separator symbol streamlines search across the source code - throw new \Magento\Framework\Exception\LocalizedException( - __('Directory separator "\\" is prohibited in fixture declaration.') - ); - } - $fixtureMethod = [get_class($test), $fixture]; - if (is_callable($fixtureMethod)) { - $result[] = $fixtureMethod; - } else { - $result[] = $this->_fixtureBaseDir . '/' . $fixture; - } - } - } - return $result; - } - - /** - * Execute single fixture script - * - * @param string|array $fixture - * @throws \Throwable - */ - protected function _applyOneFixture($fixture) - { - try { - if (is_callable($fixture)) { - call_user_func($fixture); - } else { - require $fixture; - } - } catch (\Exception $e) { - throw new \Exception( - sprintf( - "Exception occurred when running the %s fixture: \n%s", - (\is_array($fixture) || is_scalar($fixture) ? json_encode($fixture) : 'callback'), - $e->getMessage() - ) - ); - } - $this->_appliedFixtures[] = $fixture; - } - - /** - * Execute fixture scripts if any * - * @param array $fixtures - * @throws \Magento\Framework\Exception\LocalizedException + * @param TestCase $test */ - protected function _applyFixtures(array $fixtures) + public function endTest(TestCase $test) { - /* Execute fixture scripts */ - foreach ($fixtures as $oneFixture) { - /* Skip already applied fixtures */ - if (!in_array($oneFixture, $this->_appliedFixtures, true)) { - $this->_applyOneFixture($oneFixture); - } - } + $this->_revertFixtures($test); + $objectManager = Bootstrap::getObjectManager(); + $objectManager->get(AttributeMetadataCache::class)->clean(); } /** - * Revert changes done by fixtures + * @inheritdoc */ - protected function _revertFixtures() + protected function getAnnotation(): string { - $appliedFixtures = array_reverse($this->_appliedFixtures); - foreach ($appliedFixtures as $fixture) { - if (is_callable($fixture)) { - $fixture[1] .= 'Rollback'; - if (is_callable($fixture)) { - $this->_applyOneFixture($fixture); - } - } else { - $fileInfo = pathinfo($fixture); - $extension = ''; - if (isset($fileInfo['extension'])) { - $extension = '.' . $fileInfo['extension']; - } - $rollbackScript = $fileInfo['dirname'] . '/' . $fileInfo['filename'] . '_rollback' . $extension; - if (file_exists($rollbackScript)) { - $this->_applyOneFixture($rollbackScript); - } - } - } - $this->_appliedFixtures = []; + return self::ANNOTATION; } } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/ApiSuiteLoader.php b/dev/tests/api-functional/framework/Magento/TestFramework/ApiSuiteLoader.php new file mode 100644 index 0000000000000..ea8ee3a358410 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/ApiSuiteLoader.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework; + +/** + * Custom suite loader for adding wrapper for tests. + */ +class ApiSuiteLoader extends SuiteLoader +{ + +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php b/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php index fa0cebece9a96..e94ba706e4796 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php @@ -5,31 +5,58 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\TestFramework\App; +use Magento\Config\Model\Config\Factory as ConfigFactory; use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\TestFramework\ObjectManager; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\ScopeInterface; /** * @inheritdoc */ class ApiMutableScopeConfig implements MutableScopeConfigInterface { + /** @var Config */ + private $testAppConfig; + + /** @var StoreRepositoryInterface */ + private $storeRepository; + + /** @var WebsiteRepositoryInterface */ + private $websiteRepository; + + /** @var ConfigFactory */ + private $configFactory; + /** - * @var Config + * @param ScopeConfigInterface $config + * @param StoreRepositoryInterface $storeRepository + * @param WebsiteRepositoryInterface $websiteRepository + * @param ConfigFactory $configFactory */ - private $testAppConfig; + public function __construct( + ScopeConfigInterface $config, + StoreRepositoryInterface $storeRepository, + WebsiteRepositoryInterface $websiteRepository, + ConfigFactory $configFactory + ) { + $this->testAppConfig = $config; + $this->storeRepository = $storeRepository; + $this->websiteRepository = $websiteRepository; + $this->configFactory = $configFactory; + } /** * @inheritdoc */ public function isSetFlag($path, $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null) { - return $this->getTestAppConfig()->isSetFlag($path, $scopeType, $scopeCode); + return $this->testAppConfig->isSetFlag($path, $scopeType, $scopeCode); } /** @@ -37,7 +64,7 @@ public function isSetFlag($path, $scopeType = ScopeConfigInterface::SCOPE_TYPE_D */ public function getValue($path, $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null) { - return $this->getTestAppConfig()->getValue($path, $scopeType, $scopeCode); + return $this->testAppConfig->getValue($path, $scopeType, $scopeCode); } /** @@ -46,11 +73,11 @@ public function getValue($path, $scopeType = ScopeConfigInterface::SCOPE_TYPE_DE public function setValue( $path, $value, - $scopeType = \Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null ) { $this->persistConfig($path, $value, $scopeType, $scopeCode); - return $this->getTestAppConfig()->setValue($path, $value, $scopeType, $scopeCode); + return $this->testAppConfig->setValue($path, $value, $scopeType, $scopeCode); } /** @@ -60,21 +87,7 @@ public function setValue( */ public function clean() { - $this->getTestAppConfig()->clean(); - } - - /** - * Retrieve test app config instance - * - * @return \Magento\TestFramework\App\Config - */ - private function getTestAppConfig() - { - if (!$this->testAppConfig) { - $this->testAppConfig = ObjectManager::getInstance()->get(ScopeConfigInterface::class); - } - - return $this->testAppConfig; + $this->testAppConfig->clean(); } /** @@ -84,18 +97,12 @@ private function getTestAppConfig() * @param string $value * @param string $scopeType * @param string|null $scopeCode + * @return void */ - private function persistConfig($path, $value, $scopeType, $scopeCode): void + private function persistConfig(string $path, string $value, string $scopeType, ?string $scopeCode): void { $pathParts = explode('/', $path); $store = 0; - if ($scopeType === \Magento\Store\Model\ScopeInterface::SCOPE_STORE - && $scopeCode !== null) { - $store = ObjectManager::getInstance() - ->get(\Magento\Store\Api\StoreRepositoryInterface::class) - ->get($scopeCode) - ->getId(); - } $configData = [ 'section' => $pathParts[0], 'website' => '', @@ -110,9 +117,15 @@ private function persistConfig($path, $value, $scopeType, $scopeCode): void ] ] ]; - ObjectManager::getInstance() - ->get(\Magento\Config\Model\Config\Factory::class) - ->create(['data' => $configData]) - ->save(); + if ($scopeType === ScopeInterface::SCOPE_STORE && $scopeCode !== null) { + $store = $this->storeRepository->get($scopeCode)->getId(); + $configData['store'] = $store; + } elseif ($scopeType === ScopeInterface::SCOPE_WEBSITES && $scopeCode !== null) { + $website = $this->websiteRepository->get($scopeCode)->getId(); + $configData['store'] = ''; + $configData['website'] = $website; + } + + $this->configFactory->create(['data' => $configData])->save(); } } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Bootstrap/WebapiDocBlock.php b/dev/tests/api-functional/framework/Magento/TestFramework/Bootstrap/WebapiDocBlock.php index a3a013ae812ad..7b7047d1aceba 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Bootstrap/WebapiDocBlock.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Bootstrap/WebapiDocBlock.php @@ -32,7 +32,7 @@ protected function _getSubscribers(\Magento\TestFramework\Application $applicati unset($subscribers[$key]); } } - $subscribers[] = new \Magento\TestFramework\Annotation\ApiDataFixture($this->_fixturesBaseDir); + $subscribers[] = new \Magento\TestFramework\Annotation\ApiDataFixture(); $subscribers[] = new ApiConfigFixture(); return $subscribers; diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Helper/CompareArraysRecursively.php b/dev/tests/api-functional/framework/Magento/TestFramework/Helper/CompareArraysRecursively.php new file mode 100644 index 0000000000000..d8a88c721c9fe --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Helper/CompareArraysRecursively.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Helper; + +/** + * Class for comparing arrays recursively + */ +class CompareArraysRecursively +{ + /** + * 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 execute(array $expected, array $actual): array + { + $diffResult = []; + + foreach ($expected as $key => $value) { + if (array_key_exists($key, $actual)) { + if (is_array($value)) { + $recursiveDiff = $this->execute($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/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 94eb5ddec8604..3de18a932f2cd 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php @@ -171,6 +171,11 @@ protected function assertResponseFields($actualResponse, $assertionMap) $expectedValue, "Value of '{$responseField}' field must not be NULL" ); + self::assertArrayHasKey( + $responseField, + $actualResponse, + "Response array does not contain key '{$responseField}'" + ); self::assertEquals( $expectedValue, $actualResponse[$responseField], diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebApiApplication.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebApiApplication.php index 992653a3a65d6..3c620e0edd6a7 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/WebApiApplication.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebApiApplication.php @@ -13,7 +13,7 @@ class WebApiApplication extends Application { /** - * {@inheritdoc} + * @inheritdoc */ public function run() { @@ -24,7 +24,7 @@ public function run() } /** - * {@inheritdoc} + * @inheritdoc */ public function install($cleanup) { @@ -45,19 +45,18 @@ public function install($cleanup) } continue; } - if (!empty($optionValue)) { - $installCmd .= " --$optionName=%s"; - $installArgs[] = $optionValue; - } + $installCmd .= " --$optionName=%s"; + $installArgs[] = $optionValue; } $this->_shell->execute($installCmd, $installArgs); } } /** - * Use the application as is + * @inheritdoc * - * {@inheritdoc} + * Return empty array of custom directories + * @return array */ protected function getCustomDirs() { diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php new file mode 100644 index 0000000000000..06605d156933d --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\WebapiWorkaround\Override; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\ConverterInterface; +use Magento\Framework\Config\SchemaLocatorInterface; +use Magento\Framework\View\File\CollectorInterface; +use Magento\TestFramework\Annotation\AdminConfigFixture; +use Magento\TestFramework\Annotation\ApiConfigFixture; +use Magento\TestFramework\Annotation\ApiDataFixture; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; +use Magento\TestFramework\WebapiWorkaround\Override\Config\Converter; +use Magento\TestFramework\WebapiWorkaround\Override\Config\FileCollector; +use Magento\TestFramework\WebapiWorkaround\Override\Config\SchemaLocator; +use Magento\TestFramework\Workaround\Override\Config as IntegrationConfig; + +/** + * Provides api tests configuration. + */ +class Config extends IntegrationConfig +{ + /** + * @inheritdoc + */ + protected const FIXTURE_TYPES = [ + ApiDataFixture::ANNOTATION, + ApiConfigFixture::ANNOTATION, + DataFixture::ANNOTATION, + DataFixtureBeforeTransaction::ANNOTATION, + AdminConfigFixture::ANNOTATION, + ]; + + /** + * @inheritdoc + */ + protected function getConverter(): ConverterInterface + { + return ObjectManager::getInstance()->create(Converter::class, ['types' => $this::FIXTURE_TYPES]); + } + + /** + * @inheritdoc + */ + protected function getSchemaLocator(): SchemaLocatorInterface + { + return ObjectManager::getInstance()->create(SchemaLocator::class); + } + + /** + * @inheritdoc + */ + protected function getFileCollector(): CollectorInterface + { + return ObjectManager::getInstance()->create(FileCollector::class); + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php new file mode 100644 index 0000000000000..c14e535187296 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\WebapiWorkaround\Override\Config; + +use Magento\TestFramework\Annotation\AdminConfigFixture; +use Magento\TestFramework\Annotation\ApiDataFixture; +use Magento\TestFramework\Annotation\ConfigFixture; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; +use Magento\TestFramework\Workaround\Override\Config\Converter as IntegrationConverter; + +/** + * Converter for api tests config + */ +class Converter extends IntegrationConverter +{ + /** + * Fill node attributes values + * + * @param \DOMElement $fixture + * @return array + */ + protected function fillAttributes(\DOMElement $fixture): array + { + $result = []; + switch ($fixture->nodeName) { + case DataFixtureBeforeTransaction::ANNOTATION: + case DataFixture::ANNOTATION: + case ApiDataFixture::ANNOTATION: + $result = $this->fillDataFixtureAttributes($fixture); + break; + case ConfigFixture::ANNOTATION: + $result = $this->fillConfigFixtureAttributes($fixture); + break; + case AdminConfigFixture::ANNOTATION: + $result = $this->fillAdminConfigFixtureAttributes($fixture); + break; + default: + break; + } + + return $result; + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/FileCollector.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/FileCollector.php new file mode 100644 index 0000000000000..29ff24a0c0478 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/FileCollector.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\WebapiWorkaround\Override\Config; + +use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Component\DirSearch; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\File\CollectorInterface; +use Magento\Framework\View\File\Factory as FileFactory; + +class FileCollector implements CollectorInterface +{ + /** + * @var DirSearch + */ + private $componentDirSearch; + + /** + * @var FileFactory + */ + private $fileFactory; + + /** + * @param DirSearch $dirSearch + * @param FileFactory $fileFactory + */ + public function __construct( + DirSearch $dirSearch, + FileFactory $fileFactory + ) { + $this->componentDirSearch = $dirSearch; + $this->fileFactory = $fileFactory; + } + + /** + * Retrieve files + * + * @param \Magento\Framework\View\Design\ThemeInterface $theme + * @param string $filePath + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @return \Magento\Framework\View\File[] + */ + public function getFiles(ThemeInterface $theme, $filePath) + { + $result = []; + $configFiles = $this->componentDirSearch->collectFilesWithContext( + ComponentRegistrar::MODULE, + 'Test/Api/_files/' . $filePath + ); + foreach ($configFiles as $file) { + $result[] = $this->fileFactory->create($file->getFullPath(), $file->getComponentName(), null, true); + } + return $result; + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/SchemaLocator.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/SchemaLocator.php new file mode 100644 index 0000000000000..6c671e25d2812 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/SchemaLocator.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\WebapiWorkaround\Override\Config; + +use Magento\Framework\Config\SchemaLocatorInterface; + +/** + * Schema locator for tests config + */ +class SchemaLocator implements SchemaLocatorInterface +{ + /** + * @inheritdoc + */ + public function getSchema() + { + return __DIR__ . '/../../etc/overrides.xsd'; + } + + /** + * @inheritdoc + */ + public function getPerFileSchema() + { + return null; + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Fixture/Resolver.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Fixture/Resolver.php new file mode 100644 index 0000000000000..e024e00a7e4e9 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Fixture/Resolver.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\WebapiWorkaround\Override\Fixture; + +use Magento\TestFramework\Annotation\AdminConfigFixture; +use Magento\TestFramework\Annotation\ApiConfigFixture; +use Magento\TestFramework\Annotation\ApiDataFixture; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; +use Magento\TestFramework\Workaround\Override\Fixture\Applier\AdminConfigFixture as AdminConfigFixtureApplier; +use Magento\TestFramework\Workaround\Override\Fixture\Applier\ApplierInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Applier\ConfigFixture as ConfigFixtureApplier; +use Magento\TestFramework\Workaround\Override\Fixture\Applier\DataFixture as DataFixtureApplier; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver as IntegrationResolver; + +/** + * Class determines fixture applying according to configurations + */ +class Resolver extends IntegrationResolver +{ + /** + * Get appropriate fixture applier according to fixture type + * + * @param string $fixtureType + * @return ApplierInterface + */ + protected function getApplierByFixtureType(string $fixtureType): ApplierInterface + { + switch ($fixtureType) { + case ApiDataFixture::ANNOTATION: + case DataFixture::ANNOTATION: + case DataFixtureBeforeTransaction::ANNOTATION: + $applier = $this->objectManager->get(DataFixtureApplier::class); + break; + case ApiConfigFixture::ANNOTATION: + $applier = $this->objectManager->get(ConfigFixtureApplier::class); + break; + case AdminConfigFixture::ANNOTATION: + $applier = $this->objectManager->get(AdminConfigFixtureApplier::class); + break; + default: + throw new \InvalidArgumentException(sprintf('Unsupported fixture type %s provided', $fixtureType)); + } + + return $applier; + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/etc/overrides.xsd b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/etc/overrides.xsd new file mode 100644 index 0000000000000..c0409afa9ea65 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/etc/overrides.xsd @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="overrides"> + <xs:complexType> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="test" type="test" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:complexType name="test"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="method" type="method" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoApiDataFixture" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoDataFixture" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoDataFixtureBeforeTransaction" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoConfigFixture" type="configFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoAdminConfigFixture" type="adminConfigFixture" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + <xs:attribute name="class" type="xs:string" use="required"/> + <xs:attribute name="skip" type="xs:boolean"/> + <xs:attribute name="skipMessage" type="xs:string"/> + </xs:complexType> + <xs:complexType name="method"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="dataSet" type="dataSet" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoApiDataFixture" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoDataFixture" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoDataFixtureBeforeTransaction" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoConfigFixture" type="configFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoAdminConfigFixture" type="adminConfigFixture" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + <xs:attribute name="name" type="xs:string" use="required"/> + <xs:attribute name="skip" type="xs:boolean"/> + <xs:attribute name="skipMessage" type="xs:string"/> + </xs:complexType> + <xs:complexType name="dataSet"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="magentoApiDataFixture" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoDataFixture" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoDataFixtureBeforeTransaction" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoConfigFixture" type="configFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoAdminConfigFixture" type="adminConfigFixture" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + <xs:attribute name="name" type="xs:string" use="required"/> + <xs:attribute name="skip" type="xs:boolean"/> + <xs:attribute name="skipMessage" type="xs:string"/> + </xs:complexType> + <xs:complexType name="dataFixture"> + <xs:attribute name="path" type="xs:string" use="required"/> + <xs:attribute name="newPath" type="xs:string"/> + <xs:attribute name="before" type="xs:string"/> + <xs:attribute name="after" type="xs:string"/> + <xs:attribute name="remove" type="xs:boolean"/> + </xs:complexType> + <xs:complexType name="configFixture"> + <xs:attribute name="path" type="xs:string" use="required"/> + <xs:attribute name="scopeType"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:enumeration value="store"/> + <xs:enumeration value="website"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="scopeCode" type="xs:string"/> + <xs:attribute name="value" type="xs:string"/> + <xs:attribute name="newValue" type="xs:string"/> + <xs:attribute name="remove" type="xs:boolean"/> + </xs:complexType> + <xs:complexType name="adminConfigFixture"> + <xs:attribute name="path" type="xs:string" use="required"/> + <xs:attribute name="value" type="xs:string"/> + <xs:attribute name="newValue" type="xs:string"/> + <xs:attribute name="remove" type="xs:boolean"/> + </xs:complexType> +</xs:schema> diff --git a/dev/tests/api-functional/framework/bootstrap.php b/dev/tests/api-functional/framework/bootstrap.php index 01580bc268d05..d3a9a6add5776 100644 --- a/dev/tests/api-functional/framework/bootstrap.php +++ b/dev/tests/api-functional/framework/bootstrap.php @@ -94,9 +94,19 @@ $themePackageList ) ); - unset($bootstrap, $application, $settings, $shell); + $overrideConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + Magento\TestFramework\WebapiWorkaround\Override\Config::class + ); + $overrideConfig->init(); + Magento\TestFramework\Workaround\Override\Fixture\Resolver::setInstance( + new \Magento\TestFramework\WebapiWorkaround\Override\Fixture\Resolver($overrideConfig) + ); + \Magento\TestFramework\Workaround\Override\Config::setInstance($overrideConfig); + unset($bootstrap, $application, $settings, $shell, $overrideConfig); } catch (\Exception $e) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $e . PHP_EOL; + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(1); } diff --git a/dev/tests/api-functional/phpunit_graphql.xml.dist b/dev/tests/api-functional/phpunit_graphql.xml.dist index aa1899d88f48e..e63008a10ee51 100644 --- a/dev/tests/api-functional/phpunit_graphql.xml.dist +++ b/dev/tests/api-functional/phpunit_graphql.xml.dist @@ -8,16 +8,21 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.2/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" beStrictAboutTestsThatDoNotTestAnything="false" bootstrap="./framework/bootstrap.php" + testSuiteLoaderClass="Magento\TestFramework\ApiSuiteLoader" + testSuiteLoaderFile="framework/Magento/TestFramework/ApiSuiteLoader.php" > <!-- Test suites definition --> <testsuites> <testsuite name="Magento GraphQL web API functional tests"> - <directory suffix="Test.php">testsuite/Magento/GraphQl</directory> + <file>testsuite/Magento/WebApiTest.php</file> + </testsuite> + <testsuite name="Magento GraphQL web API functional tests real suite"> + <directory>testsuite/Magento/GraphQl</directory> </testsuite> </testsuites> @@ -47,6 +52,7 @@ <const name="TESTS_MAGENTO_INSTALLATION" value="disabled"/> <!-- Magento mode for tests execution. Possible values are "default", "developer" and "production". --> <const name="TESTS_MAGENTO_MODE" value="default"/> + <const name="USE_OVERRIDE_CONFIG" value="enabled"/> </php> <!-- Test listeners --> diff --git a/dev/tests/api-functional/phpunit_rest.xml.dist b/dev/tests/api-functional/phpunit_rest.xml.dist index c5173e5dd432e..b949e6c6cffe2 100644 --- a/dev/tests/api-functional/phpunit_rest.xml.dist +++ b/dev/tests/api-functional/phpunit_rest.xml.dist @@ -8,18 +8,23 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.2/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" beStrictAboutTestsThatDoNotTestAnything="false" bootstrap="./framework/bootstrap.php" + testSuiteLoaderClass="Magento\TestFramework\ApiSuiteLoader" + testSuiteLoaderFile="framework/Magento/TestFramework/ApiSuiteLoader.php" > <!-- Test suites definition --> <testsuites> <testsuite name="Magento REST web API functional tests"> - <directory suffix="Test.php">testsuite</directory> + <file>testsuite/Magento/WebApiTest.php</file> + </testsuite> + <testsuite name="Magento REST web API functional tests real suite"> + <directory>testsuite</directory> + <directory>../../../app/code/*/*/Test/Api</directory> <exclude>testsuite/Magento/GraphQl</exclude> - <directory suffix="Test.php">../../../app/code/*/*/Test/Api</directory> </testsuite> </testsuites> @@ -53,6 +58,7 @@ <const name="TESTS_MAGENTO_INSTALLATION" value="disabled"/> <!-- Magento mode for tests execution. Possible values are "default", "developer" and "production". --> <const name="TESTS_MAGENTO_MODE" value="default"/> + <const name="USE_OVERRIDE_CONFIG" value="enabled"/> </php> <!-- Test listeners --> diff --git a/dev/tests/api-functional/phpunit_soap.xml.dist b/dev/tests/api-functional/phpunit_soap.xml.dist index 935f5113b67a7..8fc7ad8cebdc4 100644 --- a/dev/tests/api-functional/phpunit_soap.xml.dist +++ b/dev/tests/api-functional/phpunit_soap.xml.dist @@ -8,18 +8,23 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.2/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" beStrictAboutTestsThatDoNotTestAnything="false" bootstrap="./framework/bootstrap.php" + testSuiteLoaderClass="Magento\TestFramework\ApiSuiteLoader" + testSuiteLoaderFile="framework/Magento/TestFramework/ApiSuiteLoader.php" > <!-- Test suites definition --> <testsuites> <testsuite name="Magento SOAP web API functional tests"> - <directory suffix="Test.php">testsuite</directory> + <file>testsuite/Magento/WebApiTest.php</file> + </testsuite> + <testsuite name="Magento SOAP web API functional tests real suite"> + <directory>testsuite</directory> <!-- <exclude>testsuite/Magento/GraphQl</exclude> --> - <directory suffix="Test.php">../../../app/code/*/*/Test/Api</directory> + <directory>../../../app/code/*/*/Test/Api</directory> </testsuite> </testsuites> @@ -52,6 +57,7 @@ <const name="TESTS_MAGENTO_INSTALLATION" value="disabled"/> <!-- Magento mode for tests execution. Possible values are "default", "developer" and "production". --> <const name="TESTS_MAGENTO_MODE" value="default"/> + <const name="USE_OVERRIDE_CONFIG" value="enabled"/> </php> <!-- Test listeners --> 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/CategoryManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryManagementTest.php index bc3869df6a65b..1523bfe957901 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryManagementTest.php @@ -9,6 +9,7 @@ use Magento\TestFramework\TestCase\WebapiAbstract; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\CompareArraysRecursively; /** * Tests CategoryManagement @@ -19,6 +20,20 @@ class CategoryManagementTest extends WebapiAbstract const SERVICE_NAME = 'catalogCategoryManagementV1'; + /** + * @var CompareArraysRecursively + */ + private $compareArraysRecursively; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->compareArraysRecursively = $objectManager->create(CompareArraysRecursively::class); + } + /** * Tests getTree operation * @@ -40,8 +55,8 @@ public function testTree($rootCategoryId, $depth, $expected) ] ]; $result = $this->_webApiCall($serviceInfo, $requestData); - $expected = array_replace_recursive($result, $expected); - $this->assertEquals($expected, $result); + $diff = $this->compareArraysRecursively->execute($expected, $result); + self::assertEquals([], $diff, "Actual categories response doesn't equal expected data"); } /** 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 aba065a956d4f..461ab6c989104 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -7,14 +7,14 @@ namespace Magento\Catalog\Api; use Magento\Authorization\Model\Role; +use Magento\Authorization\Model\RoleFactory; use Magento\Authorization\Model\Rules; +use Magento\Authorization\Model\RulesFactory; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\Integration\Api\AdminTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; -use Magento\Authorization\Model\RoleFactory; -use Magento\Authorization\Model\RulesFactory; /** * Test repository web API. @@ -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,35 @@ 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]; + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php + */ + public function testUpdateWithDefaultSortByAttribute() + { + $categoryId = 333; + $categoryData = [ + 'name' => 'Update Category Test With default_sort_by Attribute', + 'is_active' => true, + "available_sort_by" => [], + 'custom_attributes' => [ + [ + 'attribute_code' => 'default_sort_by', + 'value' => ["name"], + ], + ], + ]; + $result = $this->updateCategory($categoryId, $categoryData); + $this->assertEquals($categoryId, $result['id']); + /** @var \Magento\Catalog\Model\Category $model */ + $model = Bootstrap::getObjectManager()->get(\Magento\Catalog\Model\Category::class); + $category = $model->load($categoryId); + $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()); + $this->createdCategories = [$categoryId]; } protected function getSimpleCategoryData($categoryData = []) @@ -447,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/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php index 64f51b93cde50..2b628c05ae736 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php @@ -7,14 +7,21 @@ use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Eav\Api\Data\AttributeOptionLabelInterface; +use Magento\Framework\Webapi\Rest\Request; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Class to test Eav Option Management functionality + */ class ProductAttributeOptionManagementInterfaceTest extends WebapiAbstract { const SERVICE_NAME = 'catalogProductAttributeOptionManagementV1'; const SERVICE_VERSION = 'V1'; const RESOURCE_PATH = '/V1/products/attributes'; + /** + * Test to get attribute options + */ public function testGetItems() { $testAttributeCode = 'quantity_and_stock_status'; @@ -29,64 +36,56 @@ public function testGetItems() ], ]; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $testAttributeCode . '/options', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'getItems', - ], - ]; - - $response = $this->_webApiCall($serviceInfo, ['attributeCode' => $testAttributeCode]); + $response = $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_GET, + 'getItems', + ['attributeCode' => $testAttributeCode] + ); $this->assertIsArray($response); $this->assertEquals($expectedOptions, $response); } /** + * Test to add attribute option + * + * @param array $optionData * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php * @dataProvider addDataProvider */ - public function testAdd($optionData) + public function testAdd(array $optionData) { $testAttributeCode = 'select_attribute'; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $testAttributeCode . '/options', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'add', - ], - ]; - - $response = $this->_webApiCall( - $serviceInfo, + $response = $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_POST, + 'add', [ 'attributeCode' => $testAttributeCode, 'option' => $optionData, ] ); - $this->assertNotNull($response); - $updatedData = $this->getAttributeOptions($testAttributeCode); - $lastOption = array_pop($updatedData); - $this->assertEquals( - $optionData[AttributeOptionInterface::STORE_LABELS][0][AttributeOptionLabelInterface::LABEL], - $lastOption['label'] - ); + $this->assertTrue(is_numeric($response)); + /* Check new option labels by stores */ + $expectedStoreLabels = [ + 'all' => $optionData[AttributeOptionLabelInterface::LABEL], + 'default' => $optionData[AttributeOptionInterface::STORE_LABELS][0][AttributeOptionLabelInterface::LABEL], + ]; + foreach ($expectedStoreLabels as $store => $label) { + $option = $this->getAttributeOption($testAttributeCode, $label, $store); + $this->assertNotNull($option); + $this->assertEquals($response, $option['value']); + } } /** + * Data provider for adding attribute option + * * @return array */ - public function addDataProvider() + public function addDataProvider(): array { $optionPayload = [ AttributeOptionInterface::LABEL => 'new color', @@ -114,62 +113,111 @@ public function addDataProvider() 'option_with_value_node_that_is_a_number' => [ array_merge($optionPayload, [AttributeOptionInterface::VALUE => '123']) ], - ]; } /** + * Test to delete attribute option + * * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php */ public function testDelete() { $attributeCode = 'select_attribute'; - //get option Id $optionList = $this->getAttributeOptions($attributeCode); $this->assertGreaterThan(0, count($optionList)); $lastOption = array_pop($optionList); $this->assertNotEmpty($lastOption['value']); $optionId = $lastOption['value']; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $attributeCode . '/options/' . $optionId, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'delete', - ], - ]; - $this->assertTrue($this->_webApiCall( - $serviceInfo, + $response = $this->webApiCallAttributeOptions( + $attributeCode, + Request::HTTP_METHOD_DELETE, + 'delete', [ 'attributeCode' => $attributeCode, 'optionId' => $optionId, - ] - )); + ], + $optionId + ); + $this->assertTrue($response); $updatedOptions = $this->getAttributeOptions($attributeCode); $this->assertEquals($optionList, $updatedOptions); } /** - * @param $testAttributeCode + * Perform Web API call to the system under test + * + * @param string $attributeCode + * @param string $httpMethod + * @param string $soapMethod + * @param array $arguments + * @param null $storeCode + * @param null $optionId * @return array|bool|float|int|string */ - private function getAttributeOptions($testAttributeCode) - { + private function webApiCallAttributeOptions( + string $attributeCode, + string $httpMethod, + string $soapMethod, + array $arguments = [], + $optionId = null, + $storeCode = null + ) { $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $testAttributeCode . '/options', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'resourcePath' => self::RESOURCE_PATH . '/' . $attributeCode . '/options' + . ($optionId ? '/' .$optionId : ''), + 'httpMethod' => $httpMethod, ], 'soap' => [ 'service' => self::SERVICE_NAME, 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'getItems', + 'operation' => self::SERVICE_NAME . $soapMethod, ], ]; - return $this->_webApiCall($serviceInfo, ['attributeCode' => $testAttributeCode]); + + return $this->_webApiCall($serviceInfo, $arguments, null, $storeCode); + } + + /** + * @param string $testAttributeCode + * @param string|null $storeCode + * @return array|bool|float|int|string + */ + private function getAttributeOptions(string $testAttributeCode, ?string $storeCode = null) + { + return $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_GET, + 'getItems', + ['attributeCode' => $testAttributeCode], + null, + $storeCode + ); + } + + /** + * @param string $attributeCode + * @param string $optionLabel + * @param string|null $storeCode + * @return array|null + */ + private function getAttributeOption( + string $attributeCode, + string $optionLabel, + ?string $storeCode = null + ): ?array { + $attributeOptions = $this->getAttributeOptions($attributeCode, $storeCode); + $option = null; + /** @var array $attributeOption */ + foreach ($attributeOptions as $attributeOption) { + if ($attributeOption['label'] === $optionLabel) { + $option = $attributeOption; + break; + } + } + + return $option; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionUpdateInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionUpdateInterfaceTest.php new file mode 100644 index 0000000000000..dc3648f68b10c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionUpdateInterfaceTest.php @@ -0,0 +1,234 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Api; + +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Api\Data\AttributeOptionLabelInterface; +use Magento\Framework\Webapi\Rest\Request; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Class to test update Product Attribute Options + */ +class ProductAttributeOptionUpdateInterfaceTest extends WebapiAbstract +{ + private const SERVICE_NAME_UPDATE = 'catalogProductAttributeOptionUpdateV1'; + private const SERVICE_NAME = 'catalogProductAttributeOptionManagementV1'; + private const SERVICE_VERSION = 'V1'; + private const RESOURCE_PATH = '/V1/products/attributes'; + + /** + * Test to update attribute option + * + * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php + */ + public function testUpdate() + { + $testAttributeCode = 'select_attribute'; + $optionData = [ + AttributeOptionInterface::LABEL => 'Fixture Option Changed', + AttributeOptionInterface::VALUE => 'option_value', + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Store Label Changed', + AttributeOptionLabelInterface::STORE_ID => 1, + ], + ], + ]; + + $existOptionLabel = 'Fixture Option'; + $existAttributeOption = $this->getAttributeOption($testAttributeCode, $existOptionLabel, 'all'); + $optionId = $existAttributeOption['value']; + + $response = $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_PUT, + 'update', + [ + 'attributeCode' => $testAttributeCode, + 'optionId' => $optionId, + 'option' => $optionData, + ], + $optionId + ); + + $this->assertTrue($response); + + /* Check update option labels by stores */ + $expectedStoreLabels = [ + 'all' => $optionData[AttributeOptionLabelInterface::LABEL], + 'default' => $optionData[AttributeOptionInterface::STORE_LABELS][0][AttributeOptionLabelInterface::LABEL], + ]; + foreach ($expectedStoreLabels as $store => $label) { + $this->assertNotNull($this->getAttributeOption($testAttributeCode, $label, $store)); + } + } + + /** + * Test to update option with already exist exception + * + * Test to except case when the two options has a same label + * + * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php + */ + public function testUpdateWithAlreadyExistsException() + { + $this->expectExceptionMessage("Admin store attribute option label '%1' is already exists."); + $testAttributeCode = 'select_attribute'; + + $newOptionData = [ + AttributeOptionInterface::LABEL => 'New Option', + AttributeOptionInterface::VALUE => 'new_option_value', + ]; + $newOptionId = $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_POST, + 'add', + [ + 'attributeCode' => $testAttributeCode, + 'option' => $newOptionData, + ] + ); + + $editOptionData = [ + AttributeOptionInterface::LABEL => 'Fixture Option', + AttributeOptionInterface::VALUE => $newOptionId, + ]; + $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_PUT, + 'update', + [ + 'attributeCode' => $testAttributeCode, + 'optionId' => $newOptionId, + 'option' => $editOptionData, + ], + $newOptionId + ); + } + + /** + * Test to update option with not exist exception + * + * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php + */ + public function testUpdateWithNotExistsException() + { + $this->expectExceptionMessage("The '%1' attribute doesn't include an option id '%2'."); + $testAttributeCode = 'select_attribute'; + + $newOptionData = [ + AttributeOptionInterface::LABEL => 'New Option', + AttributeOptionInterface::VALUE => 'new_option_value' + ]; + $newOptionId = (int)$this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_POST, + 'add', + [ + 'attributeCode' => $testAttributeCode, + 'option' => $newOptionData, + ] + ); + + $newOptionId++; + $editOptionData = [ + AttributeOptionInterface::LABEL => 'New Option Changed', + AttributeOptionInterface::VALUE => $newOptionId + ]; + $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_PUT, + 'update', + [ + 'attributeCode' => $testAttributeCode, + 'optionId' => $newOptionId, + 'option' => $editOptionData, + ], + $newOptionId + ); + } + + /** + * Perform Web API call to the system under test + * + * @param string $attributeCode + * @param string $httpMethod + * @param string $soapMethod + * @param array $arguments + * @param null $storeCode + * @param null $optionId + * @return array|bool|float|int|string + */ + private function webApiCallAttributeOptions( + string $attributeCode, + string $httpMethod, + string $soapMethod, + array $arguments = [], + $optionId = null, + $storeCode = null + ) { + $resourcePath = self::RESOURCE_PATH . "/{$attributeCode}/options"; + if ($optionId) { + $resourcePath .= '/' . $optionId; + } + $serviceName = $soapMethod === 'update' ? self::SERVICE_NAME_UPDATE : self::SERVICE_NAME; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => $resourcePath, + 'httpMethod' => $httpMethod, + ], + 'soap' => [ + 'service' => $serviceName, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => $serviceName . $soapMethod, + ], + ]; + + return $this->_webApiCall($serviceInfo, $arguments, null, $storeCode); + } + + /** + * @param string $attributeCode + * @param string $optionLabel + * @param string|null $storeCode + * @return array|null + */ + private function getAttributeOption( + string $attributeCode, + string $optionLabel, + ?string $storeCode = null + ): ?array { + $attributeOptions = $this->getAttributeOptions($attributeCode, $storeCode); + $option = null; + /** @var array $attributeOption */ + foreach ($attributeOptions as $attributeOption) { + if ($attributeOption['label'] === $optionLabel) { + $option = $attributeOption; + break; + } + } + + return $option; + } + + /** + * @param string $testAttributeCode + * @param string|null $storeCode + * @return array|bool|float|int|string + */ + private function getAttributeOptions(string $testAttributeCode, ?string $storeCode = null) + { + return $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_GET, + 'getItems', + ['attributeCode' => $testAttributeCode], + null, + $storeCode + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php index ef374dc1873cf..90fe075f91e30 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php @@ -68,6 +68,35 @@ public function testGet() $this->assertEquals($product->getSpecialPrice(), $response[0]['price']); } + /** + * Test get method when special price is 0. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testGetZeroValue() + { + $specialPrice = 0; + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU, true); + $product->setData('special_price', $specialPrice); + $productRepository->save($product); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/special-price-information', + 'httpMethod' => Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Get', + ], + ]; + $response = $this->_webApiCall($serviceInfo, ['skus' => [self::SIMPLE_PRODUCT_SKU]]); + $this->assertNotEmpty($response); + $this->assertEquals($specialPrice, $response[0]['price']); + } + /** * Test update method. * diff --git a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php index 4c024008e6853..069944c8c35a9 100644 --- a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php @@ -6,6 +6,7 @@ namespace Magento\ConfigurableProduct\Api; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Entity\Attribute; use Magento\Eav\Model\Config; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection; @@ -13,10 +14,12 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Webapi\Rest\Request; use Magento\TestFramework\Helper\Bootstrap; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\TestFramework\TestCase\WebapiAbstract; /** * Class ProductRepositoryTest for testing ConfigurableProduct integration + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductRepositoryTest extends WebapiAbstract { @@ -28,17 +31,22 @@ class ProductRepositoryTest extends WebapiAbstract /** * @var Config */ - protected $eavConfig; + private $eavConfig; /** * @var ObjectManagerInterface */ - protected $objectManager; + private $objectManager; /** * @var Attribute */ - protected $configurableAttribute; + private $configurableAttribute; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; /** * @inheritdoc @@ -47,6 +55,7 @@ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->eavConfig = $this->objectManager->get(Config::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); } /** @@ -164,6 +173,65 @@ public function testCreateConfigurableProduct() $this->assertEquals([$productId1, $productId2], $resultConfigurableProductLinks); } + /** + * Create configurable with simple which has zero attribute value + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_attribute_with_source_model.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @return void + */ + public function testCreateConfigurableProductWithZeroOptionValue(): void + { + $attributeCode = 'test_configurable_with_sm'; + $attributeValue = 0; + + $product = $this->productRepository->get('simple'); + $product->setCustomAttribute($attributeCode, $attributeValue); + $this->productRepository->save($product); + + $configurableAttribute = $this->eavConfig->getAttribute('catalog_product', $attributeCode); + + $productData = [ + 'sku' => self::CONFIGURABLE_PRODUCT_SKU, + 'name' => self::CONFIGURABLE_PRODUCT_SKU, + 'type_id' => Configurable::TYPE_CODE, + 'attribute_set_id' => 4, + 'extension_attributes' => [ + 'configurable_product_options' => [ + [ + 'attribute_id' => $configurableAttribute->getId(), + 'label' => 'Test configurable with source model', + 'values' => [ + ['value_index' => '0'], + ], + ], + ], + 'configurable_product_links' => [$product->getId()], + ], + ]; + + $response = $this->createProduct($productData); + + $this->assertArrayHasKey(ProductInterface::SKU, $response); + $this->assertEquals(self::CONFIGURABLE_PRODUCT_SKU, $response[ProductInterface::SKU]); + + $this->assertArrayHasKey(ProductInterface::TYPE_ID, $response); + $this->assertEquals('configurable', $response[ProductInterface::TYPE_ID]); + + $this->assertArrayHasKey(ProductInterface::EXTENSION_ATTRIBUTES_KEY, $response); + $this->assertArrayHasKey( + 'configurable_product_options', + $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY] + ); + $configurableProductOption = + current($response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]['configurable_product_options']); + + $this->assertArrayHasKey('attribute_id', $configurableProductOption); + $this->assertEquals($configurableAttribute->getId(), $configurableProductOption['attribute_id']); + $this->assertArrayHasKey('values', $configurableProductOption); + $this->assertEquals($attributeValue, $configurableProductOption['values'][0]['value_index']); + } + /** * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php index a00af2d6eb076..e1fb9e29105b9 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php @@ -6,23 +6,26 @@ namespace Magento\Customer\Api; -use Magento\Customer\Api\Data\CustomerInterface as Customer; use Magento\Customer\Api\Data\AddressInterface as Address; +use Magento\Customer\Api\Data\CustomerInterface as Customer; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SortOrder; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Customer as CustomerHelper; use Magento\TestFramework\TestCase\WebapiAbstract; -use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; -use Magento\Framework\Exception\NoSuchEntityException; /** - * Test class for Magento\Customer\Api\CustomerRepositoryInterface + * Test for \Magento\Customer\Api\CustomerRepositoryInterface. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -31,13 +34,8 @@ class CustomerRepositoryTest extends WebapiAbstract const SERVICE_VERSION = 'V1'; const SERVICE_NAME = 'customerCustomerRepositoryV1'; const RESOURCE_PATH = '/V1/customers'; - const RESOURCE_PATH_CUSTOMER_TOKEN = "/V1/integration/customer/token"; - /** - * Sample values for testing - */ - const ATTRIBUTE_CODE = 'attribute_code'; - const ATTRIBUTE_VALUE = 'attribute_value'; + private const STUB_INVALID_CUSTOMER_GROUP_ID = 777; /** * @var CustomerRepositoryInterface @@ -45,12 +43,12 @@ class CustomerRepositoryTest extends WebapiAbstract private $customerRepository; /** - * @var \Magento\Framework\Api\DataObjectHelper + * @var DataObjectHelper */ private $dataObjectHelper; /** - * @var \Magento\Customer\Api\Data\CustomerInterfaceFactory + * @var CustomerInterfaceFactory */ private $customerDataFactory; @@ -70,7 +68,7 @@ class CustomerRepositoryTest extends WebapiAbstract private $filterGroupBuilder; /** - * @var \Magento\Customer\Model\CustomerRegistry + * @var CustomerRegistry */ private $customerRegistry; @@ -131,7 +129,7 @@ protected function tearDown(): void $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $customerId, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -165,24 +163,23 @@ public function testInvalidCustomerUpdate() $customerTokenService = Bootstrap::getObjectManager()->create( \Magento\Integration\Api\CustomerTokenServiceInterface::class ); - $token = $customerTokenService->createCustomerAccessToken($firstCustomerData[Customer::EMAIL], 'test@123'); + $token = $customerTokenService->createCustomerAccessToken( + $firstCustomerData[Customer::EMAIL], + 'test@123' + ); //Create second customer and update lastname. $customerData = $this->_createCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); + $existingCustomerDataObject = $this->getCustomerData($customerData[Customer::ID]); $lastName = $existingCustomerDataObject->getLastname(); $customerData[Customer::LASTNAME] = $lastName . 'Updated'; $newCustomerDataObject = $this->customerDataFactory->create(); - $this->dataObjectHelper->populateWithArray( - $newCustomerDataObject, - $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class - ); + $this->dataObjectHelper->populateWithArray($newCustomerDataObject, $customerData, Customer::class); $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/{$customerData[Customer::ID]}", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, 'token' => $token, ], 'soap' => [ @@ -195,7 +192,7 @@ public function testInvalidCustomerUpdate() $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $requestData = ['customer' => $newCustomerDataObject]; $this->_webApiCall($serviceInfo, $requestData); @@ -209,7 +206,7 @@ public function testDeleteCustomer() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $customerData[Customer::ID], - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -228,16 +225,21 @@ public function testDeleteCustomer() //Verify if the customer is deleted $this->expectException(\Magento\Framework\Exception\NoSuchEntityException::class); $this->expectExceptionMessage(sprintf("No such entity with customerId = %s", $customerData[Customer::ID])); - $this->_getCustomerData($customerData[Customer::ID]); + $this->getCustomerData($customerData[Customer::ID]); } - public function testDeleteCustomerInvalidCustomerId() + /** + * Test delete customer with invalid id + * + * @return void + */ + public function testDeleteCustomerInvalidCustomerId(): void { $invalidId = -1; $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $invalidId, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -266,23 +268,25 @@ public function testDeleteCustomerInvalidCustomerId() } } - public function testUpdateCustomer() + /** + * Test customer update + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testUpdateCustomer(): void { - $customerData = $this->_createCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); - $lastName = $existingCustomerDataObject->getLastname(); - $customerData[Customer::LASTNAME] = $lastName . 'Updated'; - $newCustomerDataObject = $this->customerDataFactory->create(); - $this->dataObjectHelper->populateWithArray( - $newCustomerDataObject, - $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class - ); + $customerId = 1; + $updatedLastname = 'Updated lastname'; + $customer = $this->getCustomerData($customerId); + $customerData = $this->dataObjectProcessor->buildOutputDataArray($customer, Customer::class); + $customerData[Customer::LASTNAME] = $updatedLastname; $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . "/{$customerData[Customer::ID]}", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'resourcePath' => self::RESOURCE_PATH . '/' . $customerId, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -290,17 +294,18 @@ public function testUpdateCustomer() 'operation' => self::SERVICE_NAME . 'Save', ], ]; - $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( - $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class - ); - $requestData = ['customer' => $newCustomerDataObject]; + + $requestData['customer'] = TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP + ? $customerData + : [Customer::LASTNAME => $updatedLastname]; + $response = $this->_webApiCall($serviceInfo, $requestData); - $this->assertTrue($response !== null); + $this->assertNotNull($response); //Verify if the customer is updated - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); - $this->assertEquals($lastName . "Updated", $existingCustomerDataObject->getLastname()); + $existingCustomerDataObject = $this->getCustomerData($customerId); + $this->assertEquals($updatedLastname, $existingCustomerDataObject->getLastname()); + $this->assertEquals($customerData[Customer::FIRSTNAME], $existingCustomerDataObject->getFirstname()); } /** @@ -309,20 +314,20 @@ public function testUpdateCustomer() public function testUpdateCustomerNoWebsiteId() { $customerData = $this->customerHelper->createSampleCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); + $existingCustomerDataObject = $this->getCustomerData($customerData[Customer::ID]); $lastName = $existingCustomerDataObject->getLastname(); $customerData[Customer::LASTNAME] = $lastName . 'Updated'; $newCustomerDataObject = $this->customerDataFactory->create(); $this->dataObjectHelper->populateWithArray( $newCustomerDataObject, $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/{$customerData[Customer::ID]}", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -332,32 +337,28 @@ public function testUpdateCustomerNoWebsiteId() ]; $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); unset($newCustomerDataObject['website_id']); $requestData = ['customer' => $newCustomerDataObject]; - $expectedMessage = '"Associate to Website" is a required value.'; try { - $this->_webApiCall($serviceInfo, $requestData); - $this->fail("Expected exception."); + $response = $this->_webApiCall($serviceInfo, $requestData); + $this->assertEquals($customerData['website_id'], $response['website_id']); } catch (\SoapFault $e) { - $this->assertStringContainsString( - $expectedMessage, - $e->getMessage(), - "SoapFault does not contain expected message." - ); - } catch (\Exception $e) { - $errorObj = $this->customerHelper->processRestExceptionResult($e); - $this->assertEquals($expectedMessage, $errorObj['message'], 'Invalid message: "' . $e->getMessage() . '"'); - $this->assertEquals(HTTPExceptionCodes::HTTP_BAD_REQUEST, $e->getCode()); + $this->assertStringContainsString('"Associate to Website" is a required value.', $e->getMessage()); } } - public function testUpdateCustomerException() + /** + * Test customer exception update + * + * @return void + */ + public function testUpdateCustomerException(): void { $customerData = $this->_createCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); + $existingCustomerDataObject = $this->getCustomerData($customerData[Customer::ID]); $lastName = $existingCustomerDataObject->getLastname(); //Set non-existent id = -1 @@ -367,13 +368,13 @@ public function testUpdateCustomerException() $this->dataObjectHelper->populateWithArray( $newCustomerDataObject, $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/-1", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -383,7 +384,7 @@ public function testUpdateCustomerException() ]; $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $requestData = ['customer' => $newCustomerDataObject]; @@ -406,14 +407,99 @@ public function testUpdateCustomerException() } } + /** + * Test customer update with invalid customer group id + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testUpdateCustomerWithInvalidGroupId(): void + { + $customerId = 1; + $customerData = $this->dataObjectProcessor->buildOutputDataArray( + $this->getCustomerData($customerId), + Customer::class + ); + $customerData[Customer::GROUP_ID] = self::STUB_INVALID_CUSTOMER_GROUP_ID; + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . $customerId, + 'httpMethod' => Request::HTTP_METHOD_PUT, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + + $requestData['customer'] = $customerData; + $expectedMessage = 'The specified customer group id does not exist.'; + + try { + $this->_webApiCall($serviceInfo, $requestData); + $this->fail('Expected exception was not raised'); + } catch (\SoapFault $e) { + $this->assertStringContainsString($expectedMessage, $e->getMessage()); + } catch (\Exception $e) { + $errorObj = $this->processRestExceptionResult($e); + $this->assertEquals(HTTPExceptionCodes::HTTP_BAD_REQUEST, $e->getCode()); + $this->assertEquals($expectedMessage, $errorObj['message']); + } + } + + /** + * Test customer create with invalid customer group id + * + * @return void + */ + public function testCreateCustomerWithInvalidGroupId(): void + { + $customerData = $this->dataObjectProcessor->buildOutputDataArray( + $this->customerHelper->createSampleCustomerDataObject(), + Customer::class + ); + $customerData[Customer::GROUP_ID] = self::STUB_INVALID_CUSTOMER_GROUP_ID; + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + + $requestData = ['customer' => $customerData]; + $expectedMessage = 'The specified customer group id does not exist.'; + + try { + $this->_webApiCall($serviceInfo, $requestData); + $this->fail('Expected exception was not raised'); + } catch (\SoapFault $e) { + $this->assertStringContainsString($expectedMessage, $e->getMessage()); + } catch (\Exception $e) { + $errorObj = $this->processRestExceptionResult($e); + $this->assertEquals(HTTPExceptionCodes::HTTP_BAD_REQUEST, $e->getCode()); + $this->assertEquals($expectedMessage, $errorObj['message']); + } + } + /** * Test creating a customer with absent required address fields + * + * @return void */ - public function testCreateCustomerWithoutAddressRequiresException() + public function testCreateCustomerWithoutAddressRequiresException(): void { $customerDataArray = $this->dataObjectProcessor->buildOutputDataArray( $this->customerHelper->createSampleCustomerDataObject(), - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); foreach ($customerDataArray[Customer::KEY_ADDRESSES] as & $address) { @@ -423,7 +509,7 @@ public function testCreateCustomerWithoutAddressRequiresException() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -571,7 +657,7 @@ public function testSearchCustomersUsingGET() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search?' . $searchQueryString, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; $searchResults = $this->_webApiCall($serviceInfo); @@ -588,7 +674,7 @@ public function testSearchCustomersUsingGETEmptyFilter() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; try { @@ -640,7 +726,7 @@ public function testSearchCustomersMultipleFiltersWithSort() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -682,7 +768,7 @@ public function testSearchCustomersMultipleFiltersWithSortUsingGET() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search?' . $searchQueryString, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; $searchResults = $this->_webApiCall($serviceInfo); @@ -716,7 +802,7 @@ public function testSearchCustomersNonExistentMultipleFilters() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -755,7 +841,7 @@ public function testSearchCustomersNonExistentMultipleFiltersGET() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search?' . $searchQueryString, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; $searchResults = $this->_webApiCall($serviceInfo, $requestData); @@ -793,7 +879,7 @@ public function testSearchCustomersMultipleFilterGroups() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -886,11 +972,11 @@ public function testRevokeAllAccessTokensForCustomer() * Retrieve customer data by Id * * @param int $customerId - * @return \Magento\Customer\Api\Data\CustomerInterface + * @return Customer */ - protected function _getCustomerData($customerId) + private function getCustomerData($customerId): Customer { - $customerData = $this->customerRepository->getById($customerId); + $customerData = $this->customerRepository->getById($customerId); $this->customerRegistry->remove($customerId); return $customerData; } diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php new file mode 100644 index 0000000000000..9c7abcd6c8364 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php @@ -0,0 +1,213 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Api; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Registry; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Integration\Model\Oauth\Token as TokenModel; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Customer as CustomerHelper; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + */ +class CustomerSharingOptionsTest extends WebapiAbstract +{ + const RESOURCE_PATH = '/V1/customers/me'; + const REPO_SERVICE = 'customerCustomerRepositoryV1'; + const SERVICE_VERSION = 'V1'; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @var CustomerHelper + */ + private $customerHelper; + + /** + * @var TokenModel + */ + private $token; + + /** + * @var CustomerInterface + */ + private $customerData; + + /** + * @var CustomerTokenServiceInterface + */ + private $tokenService; + + /** + * Execute per test initialization. + */ + public function setUp(): void + { + $this->customerRegistry = Bootstrap::getObjectManager()->get( + \Magento\Customer\Model\CustomerRegistry::class + ); + + $this->customerRepository = Bootstrap::getObjectManager()->get( + CustomerRepositoryInterface::class, + ['customerRegistry' => $this->customerRegistry] + ); + + $this->customerHelper = new CustomerHelper(); + $this->customerData = $this->customerHelper->createSampleCustomer(); + $this->tokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + + // get token + $this->resetTokenForCustomerSampleData(); + } + + /** + * Ensure that fixture customer and his addresses are deleted. + */ + public function tearDown(): void + { + $this->customerRepository = null; + + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + parent::tearDown(); + } + + /** + * @param string $storeCode + * @param bool $expectingException + * @dataProvider getCustomerDataWebsiteScopeDataProvider + * + * @magentoConfigFixture default_store customer/account_share/scope 1 + */ + public function testGetCustomerDataWebsiteScope(string $storeCode, bool $expectingException) + { + $this->_markTestAsRestOnly('SOAP is difficult to generate exception messages, inconsistencies in WSDL'); + $this->processGetCustomerData($storeCode, $expectingException); + } + + /** + * @param string $storeCode + * @param bool $expectingException + * @dataProvider getCustomerDataGlobalScopeDataProvider + * + * @magentoConfigFixture customer/account_share/scope 0 + */ + public function testGetCustomerDataGlobalScope(string $storeCode, bool $expectingException) + { + $this->processGetCustomerData($storeCode, $expectingException); + } + + /** + * @param string $storeCode + * @param bool $expectingException + */ + private function processGetCustomerData(string $storeCode, bool $expectingException) + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_GET, + 'token' => $this->token, + ], + 'soap' => [ + 'service' => self::REPO_SERVICE, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::REPO_SERVICE . 'GetSelf', + 'token' => $this->token + ] + ]; + $arguments = []; + if (TESTS_WEB_API_ADAPTER === 'soap') { + $arguments['customerId'] = 0; + } + + if ($expectingException) { + self::expectException(\Exception::class); + self::expectExceptionMessage("The consumer isn't authorized to access %resources."); + } + + $this->_webApiCall($serviceInfo, $arguments, null, $storeCode); + } + + /** + * Data provider for testGetCustomerDataWebsiteScope. + * + * @return array + */ + public function getCustomerDataWebsiteScopeDataProvider(): array + { + return [ + 'Default Store View' => [ + 'store_code' => 'default', + 'exception' => false + ], + 'Custom Store View' => [ + 'store_code' => 'fixture_second_store', + 'exception' => true + ] + ]; + } + + /** + * Data provider for testGetCustomerDataGlobalScope. + * + * @return array + */ + public function getCustomerDataGlobalScopeDataProvider(): array + { + return [ + 'Default Store View' => [ + 'store_code' => 'default', + 'exception' => false + ], + 'Custom Store View' => [ + 'store_code' => 'fixture_second_store', + 'exception' => false + ] + ]; + } + + /** + * Sets the test's access token for the created customer sample data + */ + private function resetTokenForCustomerSampleData() + { + $this->resetTokenForCustomer($this->customerData[CustomerInterface::EMAIL], 'test@123'); + } + + /** + * Sets the test's access token for a particular username and password. + * + * @param string $username + * @param string $password + */ + private function resetTokenForCustomer($username, $password) + { + $this->token = $this->tokenService->createCustomerAccessToken($username, $password); + $this->customerRegistry->remove($this->customerRepository->get($username)->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php index 1361f10427fab..00bbb3f435cae 100644 --- a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php @@ -4,10 +4,13 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Downloadable\Api; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -15,22 +18,27 @@ */ class ProductRepositoryTest extends WebapiAbstract { - const SERVICE_NAME = 'catalogProductRepositoryV1'; - const SERVICE_VERSION = 'V1'; - const RESOURCE_PATH = '/V1/products'; - const PRODUCT_SKU = 'sku-test-product-downloadable'; + private const SERVICE_NAME = 'catalogProductRepositoryV1'; + private const SERVICE_VERSION = 'V1'; + private const RESOURCE_PATH = '/V1/products'; + private const PRODUCT_SKU = 'sku-test-product-downloadable'; + + private const PRODUCT_SAMPLES = 'downloadable_product_samples'; + private const PRODUCT_LINKS = 'downloadable_product_links'; /** * @var string */ - protected $testImagePath; + private $testImagePath; + /** + * @inheritdoc + */ protected function setUp(): void { - parent::setUp(); - $this->testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; + $objectManager = Bootstrap::getObjectManager(); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; /** @var DomainManagerInterface $domainManager */ $domainManager = $objectManager->get(DomainManagerInterface::class); @@ -45,7 +53,7 @@ protected function tearDown(): void $this->deleteProductBySku(self::PRODUCT_SKU); parent::tearDown(); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var DomainManagerInterface $domainManager */ $domainManager = $objectManager->get(DomainManagerInterface::class); @@ -296,6 +304,35 @@ public function testUpdateDownloadableProductLinks() $this->assertCount(2, $resultSamples); } + /** + * Update downloadable product extension attribute and check data + * + * @return void + */ + public function testUpdateDownloadableProductData(): void + { + $productResponce = $this->createDownloadableProduct(); + $stockItemData = $productResponce[ProductInterface::EXTENSION_ATTRIBUTES_KEY]['stock_item']; + + $stockItemData = TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP + ? $stockItemData['manage_stock'] = false + : ['stock_item' => ['manage_stock' => false]]; + + $productData = [ + ProductInterface::SKU => self::PRODUCT_SKU, + ProductInterface::EXTENSION_ATTRIBUTES_KEY => $stockItemData, + ]; + + $response = $this->saveProduct($productData); + + $this->assertArrayHasKey(ProductInterface::EXTENSION_ATTRIBUTES_KEY, $response); + $this->assertArrayHasKey(self::PRODUCT_SAMPLES, $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]); + $this->assertArrayHasKey(self::PRODUCT_LINKS, $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]); + + $this->assertCount(2, $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY][self::PRODUCT_SAMPLES]); + $this->assertCount(2, $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY][self::PRODUCT_LINKS]); + } + /** * Update downloadable product, update two links and change file content * @SuppressWarnings(PHPMD.ExcessiveMethodLength) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php new file mode 100644 index 0000000000000..fc0fdcf71525f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php @@ -0,0 +1,351 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Bundle; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test adding bundled products to cart using the unified mutation mutation + */ +class AddBundleProductToCartSingleMutationTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var Quote + */ + private $quote; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quote = $objectManager->create(Quote::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_1.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddBundleProductToCart() + { + $sku = 'bundle-product'; + + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + + $product = $this->productRepository->get($sku); + + /** @var $typeInstance \Magento\Bundle\Model\Product\Type */ + $typeInstance = $product->getTypeInstance(); + $typeInstance->setStoreFilter($product->getStoreId(), $product); + /** @var $option \Magento\Bundle\Model\Option */ + $option = $typeInstance->getOptionsCollection($product)->getFirstItem(); + /** @var \Magento\Catalog\Model\Product $selection */ + $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem(); + $optionId = $option->getId(); + $selectionId = $selection->getSelectionId(); + + $bundleOptionIdV2 = $this->generateBundleOptionIdV2((int) $optionId, (int) $selectionId, 1); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: 1 + selected_options: [ + "{$bundleOptionIdV2}" + ] + } + ] + ) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('addProductsToCart', $response); + self::assertArrayHasKey('cart', $response['addProductsToCart']); + $cart = $response['addProductsToCart']['cart']; + $bundleItem = current($cart['items']); + self::assertEquals($sku, $bundleItem['product']['sku']); + $bundleItemOption = current($bundleItem['bundle_options']); + self::assertEquals($optionId, $bundleItemOption['id']); + self::assertEquals($option->getTitle(), $bundleItemOption['label']); + self::assertEquals($option->getType(), $bundleItemOption['type']); + $value = current($bundleItemOption['values']); + self::assertEquals($selection->getSelectionId(), $value['id']); + self::assertEquals((float) $selection->getSelectionPriceValue(), $value['price']); + self::assertEquals(1, $value['quantity']); + } + + /** + * @param int $optionId + * @param int $selectionId + * @param int $quantity + * @return string + */ + private function generateBundleOptionIdV2(int $optionId, int $selectionId, int $quantity): string + { + return base64_encode("bundle/$optionId/$selectionId/$quantity"); + } + + public function dataProviderTestUpdateBundleItemQuantity(): array + { + return [ + [2], + [0], + ]; + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_1.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedExceptionMessage Please select all required options + */ + public function testAddBundleToCartWithWrongBundleOptions() + { + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + + $bundleOptionIdV2 = $this->generateBundleOptionIdV2((int) 1, (int) 1, 1); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "bundle-product" + quantity: 1 + selected_options: [ + "{$bundleOptionIdV2}" + ] + } + ] + ) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + user_errors { + message + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertEquals( + "Please select all required options.", + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddBundleItemWithCustomOptionQuantity() + { + + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $response = $this->graphQlQuery($this->getProductQuery("bundle-product")); + $bundleItem = $response['products']['items'][0]; + $sku = $bundleItem['sku']; + $bundleOptions = $bundleItem['items']; + + $uId0 = $bundleOptions[0]['options'][0]['uid']; + $uId1 = $bundleOptions[1]['options'][0]['uid']; + $response = $this->graphQlMutation( + $this->getMutationsQuery($maskedQuoteId, $uId0, $uId1, $sku) + ); + $bundleOptions = $response['addProductsToCart']['cart']['items'][0]['bundle_options']; + $this->assertEquals(5, $bundleOptions[0]['values'][0]['quantity']); + $this->assertEquals(1, $bundleOptions[1]['values'][0]['quantity']); + } + + /** + * Returns GraphQL query for retrieving a product with customizable options + * + * @param string $sku + * @return string + */ + private function getProductQuery(string $sku): string + { + return <<<QUERY +{ + products(search: "{$sku}") { + items { + sku + ... on BundleProduct { + items { + sku + option_id + required + type + title + options { + uid + label + product { + sku + } + can_change_quantity + id + price + + quantity + } + } + } + } + } +} +QUERY; + } + + private function getMutationsQuery( + string $maskedQuoteId, + string $optionUid0, + string $optionUid1, + string $sku + ): string { + return <<<QUERY +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: 2 + selected_options: [ + "{$optionUid1}", "{$optionUid0}" + ], + entered_options: [{ + uid: "{$optionUid0}" + value: "5" + }, + { + uid: "{$optionUid1}" + value: "5" + }] + } + ] + ) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + user_errors { + message + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php index 0acd6bb333426..f705195050843 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php @@ -80,7 +80,7 @@ public function testAddBundleProductToCart() $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); $query = <<<QUERY -mutation { +mutation { addBundleProductsToCart(input:{ cart_id:"{$maskedQuoteId}" cart_items:[ @@ -223,7 +223,7 @@ public function testAddBundleToCartWithoutOptions() $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); $query = <<<QUERY -mutation { +mutation { addBundleProductsToCart(input:{ cart_id:"{$maskedQuoteId}" cart_items:[ @@ -268,6 +268,107 @@ public function testAddBundleToCartWithoutOptions() } } } +QUERY; + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_with_multiple_options_radio_select.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddBundleToCartWithRadioAndSelectErr() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Option type (select, radio) should have only one element.'); + + $sku = 'bundle-product'; + + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + + $product = $this->productRepository->get($sku); + + /** @var $typeInstance \Magento\Bundle\Model\Product\Type */ + $typeInstance = $product->getTypeInstance(); + $typeInstance->setStoreFilter($product->getStoreId(), $product); + /** @var $option \Magento\Bundle\Model\Option */ + $options = $typeInstance->getOptionsCollection($product); + + $selectionIds = []; + $optionIds = []; + foreach ($options as $option) { + $type = $option->getType(); + + /** @var \Magento\Catalog\Model\Product $selection */ + $selections = $typeInstance->getSelectionsCollection([$option->getId()], $product); + $optionIds[$type] = $option->getId(); + + foreach ($selections->getItems() as $selection) { + $selectionIds[$type][] = $selection->getSelectionId(); + } + } + + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + addBundleProductsToCart(input:{ + cart_id:"{$maskedQuoteId}" + cart_items:[ + { + data:{ + sku:"{$sku}" + quantity:1 + } + bundle_options:[ + { + id:{$optionIds['select']} + quantity:1 + value:[ + "{$selectionIds['select'][0]}" + "{$selectionIds['select'][1]}" + ] + }, + { + id:{$optionIds['radio']} + quantity:1 + value:[ + "{$selectionIds['radio'][0]}" + "{$selectionIds['radio'][1]}" + ] + } + ] + } + ] + }) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + } +} QUERY; $this->graphQlMutation($query); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMultipleOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMultipleOptionsTest.php new file mode 100644 index 0000000000000..77c4d5b84e72e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMultipleOptionsTest.php @@ -0,0 +1,322 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Bundle; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\CompareArraysRecursively; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Bundle product with multiple options test. + */ +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 + * + * @dataProvider getBundleProductDataProvider + * @throws \Exception + */ + public function testBundleProductWithMultipleOptions(array $bundleProductDataProvider): void + { + $productSku = 'bundle-product'; + $query + = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + sku + type_id + id + name + ... on BundleProduct { + dynamic_sku + dynamic_price + dynamic_weight + price_view + ship_bundle_items + items { + option_id + title + required + type + position + sku + options { + id + quantity + position + is_default + price + price_type + can_change_quantity + label + product { + id + name + sku + type_id + } + } + } + } + } +} +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertBundleProduct($response, $bundleProductDataProvider); + } + + /** + * Assert bundle product response. + * + * @param array $response + * @param array $bundleProductDataProvider + */ + private function assertBundleProduct(array $response, array $bundleProductDataProvider): void + { + $this->assertNotEmpty($response['products']['items'], 'Precondition failed: "items" must not be empty'); + $productItems = $response['products']['items']; + + foreach ($bundleProductDataProvider as $key => $data) { + $diff = $this->compareArraysRecursively->execute($data, $productItems[$key]); + self::assertEquals([], $diff, "Actual response doesn't equal to expected data"); + } + } + + /** + * Bundle product data provider. + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function getBundleProductDataProvider(): array + { + return [ + 'products' => [ + 'items' => [ + [ + 'sku' => 'bundle-product', + 'type_id' => 'bundle', + 'name' => 'Bundle Product', + 'dynamic_sku' => true, + 'dynamic_price' => false, + 'dynamic_weight' => true, + 'price_view' => 'AS_LOW_AS', + 'ship_bundle_items' => 'TOGETHER', + 'items' => [ + [ + 'title' => 'Option 1', + 'required' => true, + 'type' => 'select', + 'position' => 1, + 'sku' => 'bundle-product', + 'options' => [ + [ + 'quantity' => 1, + 'position' => 0, + 'is_default' => false, + 'price' => 0, + 'price_type' => 'FIXED', + 'can_change_quantity' => false, + 'label' => 'Simple Product1', + 'product' => [ + 'name' => 'Simple Product1', + 'sku' => 'simple1', + 'type_id' => 'simple', + ], + ], + [ + 'quantity' => 1, + 'position' => 0, + 'is_default' => false, + 'price' => 0, + 'price_type' => 'FIXED', + 'can_change_quantity' => false, + 'label' => 'Simple Product2', + 'product' => [ + 'name' => 'Simple Product2', + 'sku' => 'simple2', + 'type_id' => 'simple', + ], + ], + ], + ], + [ + 'title' => 'Option 2', + 'required' => true, + 'type' => 'radio', + 'position' => 2, + 'sku' => 'bundle-product', + 'options' => [ + [ + 'quantity' => 1, + 'position' => 0, + 'is_default' => false, + 'price' => 0, + 'price_type' => 'FIXED', + 'can_change_quantity' => false, + 'label' => 'Simple Product1', + 'product' => [ + 'name' => 'Simple Product1', + 'sku' => 'simple1', + 'type_id' => 'simple', + ], + ], + [ + + 'quantity' => 1, + 'position' => 0, + 'is_default' => false, + 'price' => 0, + 'price_type' => 'FIXED', + 'can_change_quantity' => false, + 'label' => 'Simple Product2', + 'product' => [ + 'name' => 'Simple Product2', + 'sku' => 'simple2', + 'type_id' => 'simple', + ], + ], + ], + ], + [ + 'title' => 'Option 3', + 'required' => true, + 'type' => 'checkbox', + 'position' => 3, + 'sku' => 'bundle-product', + 'options' => [ + [ + 'quantity' => 1, + 'position' => 0, + 'is_default' => false, + 'price' => 0, + 'price_type' => 'FIXED', + 'can_change_quantity' => false, + 'label' => 'Simple Product1', + 'product' => [ + 'name' => 'Simple Product1', + 'sku' => 'simple1', + 'type_id' => 'simple', + ], + ], + [ + 'quantity' => 1, + 'position' => 0, + 'is_default' => false, + 'price' => 0, + 'price_type' => 'FIXED', + 'can_change_quantity' => false, + 'label' => 'Simple Product2', + 'product' => [ + 'name' => 'Simple Product2', + 'sku' => 'simple2', + 'type_id' => 'simple', + ], + ], + ], + ], + [ + 'title' => 'Option 4', + 'required' => true, + 'type' => 'multi', + 'position' => 4, + 'sku' => 'bundle-product', + 'options' => [ + [ + 'quantity' => 1, + 'position' => 0, + 'is_default' => false, + 'price' => 0, + 'price_type' => 'FIXED', + 'can_change_quantity' => false, + 'label' => 'Simple Product1', + 'product' => [ + 'name' => 'Simple Product1', + 'sku' => 'simple1', + 'type_id' => 'simple', + ], + ], + [ + 'quantity' => 1, + 'position' => 0, + 'is_default' => false, + 'price' => 0, + 'price_type' => 'FIXED', + 'can_change_quantity' => false, + 'label' => 'Simple Product2', + 'product' => [ + 'name' => 'Simple Product2', + 'sku' => 'simple2', + 'type_id' => 'simple', + ], + ], + ], + ], + [ + 'title' => 'Option 5', + 'required' => false, + 'type' => 'multi', + 'position' => 5, + 'sku' => 'bundle-product', + 'options' => [ + [ + 'quantity' => 1, + 'position' => 0, + 'is_default' => false, + 'price' => 0, + 'price_type' => 'FIXED', + 'can_change_quantity' => false, + 'label' => 'Simple Product1', + 'product' => [ + 'name' => 'Simple Product1', + 'sku' => 'simple1', + 'type_id' => 'simple', + ], + ], + [ + 'quantity' => 1, + 'position' => 0, + 'is_default' => false, + 'price' => 0, + 'price_type' => 'FIXED', + 'can_change_quantity' => false, + 'label' => 'Simple Product2', + 'product' => [ + 'name' => 'Simple Product2', + 'sku' => 'simple2', + 'type_id' => 'simple', + ], + ], + ], + ], + ], + ], + ], + ], + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php index 4da588794b2a9..a3daf89631c17 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php @@ -189,7 +189,7 @@ public function testQueryChildCategoriesWithProducts() $expectedBaseCategoryProducts = [ ['sku' => 'simple', 'name' => 'Simple Product'], ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => '12345', 'name' => 'Simple Product Two'] + ['sku' => '12345', 'name' => 'Simple Product Two'], ]; $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); //Check base category children @@ -648,15 +648,6 @@ public function filterMultipleCategoriesDataProvider(): array 'in', '["category-1-2", "movable"]', [ - [ - 'id' => '7', - 'name' => 'Movable', - 'url_key' => 'movable', - 'url_path' => 'movable', - 'children_count' => '0', - 'path' => '1/2/7', - 'position' => '3' - ], [ 'id' => '13', 'name' => 'Category 1.2', @@ -665,6 +656,15 @@ public function filterMultipleCategoriesDataProvider(): array 'children_count' => '0', 'path' => '1/2/3/13', 'position' => '2' + ], + [ + 'id' => '7', + 'name' => 'Movable', + 'url_key' => 'movable', + 'url_path' => 'movable', + 'children_count' => '0', + 'path' => '1/2/7', + 'position' => '3' ] ] ], diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php index c7fbcbd38c7e4..bbc84a82737bd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php @@ -155,7 +155,7 @@ public function testPaging() $lastPageQuery = sprintf($baseQuery, $page1Result['categories']['page_info']['total_pages']); $lastPageResult = $this->graphQlQuery($lastPageQuery); $this->assertCount(1, $lastPageResult['categories']['items']); - $this->assertEquals('Category 1.2', $lastPageResult['categories']['items'][0]['name']); + $this->assertEquals('Category 12', $lastPageResult['categories']['items'][0]['name']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryAnchorTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryAnchorTest.php new file mode 100644 index 0000000000000..66da162b4d46c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryAnchorTest.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test is categories anchor or not + * + * Preconditions: + * Fixture with anchor and not-anchored categories created + * Steps: + * Send Request: + * query{ + * category(id: %categoryId%){ + * id + * name + * is_anchor + * product_count + * products(pageSize: 10, currentPage: 1){ + * items{ + * name + * } + * } + * } + * Expected response: + * { + * "category": { + * "id": %category1Id%, + * "name": Category_Anchor, + * "is_anchor": 1, + * "product_count": 2, + * "products": { + * "items": [ + * { + * "name": "Product1", + * "name": "Product2" + * } + * ] + * } + * } + * } + */ +class CategoryAnchorTest extends GraphQlAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Verify that request returns correct values for given category + * + * @magentoApiDataFixture Magento/Catalog/_files/category_anchor.php + * @param string $query + * @param string $storeCode + * @param array $category + * @return void + * @throws \Exception + * @dataProvider categoryAnchorDataProvider + */ + public function testCategoryAnchor(string $query, string $storeCode, array $category): void + { + $response = $this->graphQlQuery($query, [], '', ['store' => $storeCode]); + + // check are there any items in the return data + self::assertNotNull($response['category'], 'category must not be null'); + + // check entire response + $this->assertResponseFields($response, $category); + } + + /** + * Data provider for anchored category and product inside + * + * @return array[][] + */ + public function categoryAnchorDataProvider(): array + { + return [ + [ + 'query' => $this->getQuery(22), + 'store' => 'default', + 'data' => [ + 'category' => [ + 'id' => 22, + 'name' => 'Category_Anchor', + 'is_anchor' => 1, + 'product_count' => 2, + 'products' => [ + 'items' => [ + ['name' => 'Product1'], + ['name' => 'Product2'], + ], + ], + ], + ], + ], + [ + 'query' => $this->getQuery(11), + 'store' => 'default', + 'data' => [ + 'category' => [ + 'id' => 11, + 'name' => 'Category_Default', + 'is_anchor' => 0, + 'product_count' => 1, + 'products' => [ + 'items' => [ + ['name' => 'Product1'], + ], + ], + ], + ], + ], + ]; + } + + /** + * Return GraphQL query string by categoryId + * + * @param int $categoryId + * @return string + */ + private function getQuery(int $categoryId): string + { + return <<<QUERY +{ + category(id: {$categoryId}){ + id + name + is_anchor + product_count + products(pageSize: 10, currentPage: 1, sort: {name: ASC}){ + items{ + name + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryEnabledTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryEnabledTest.php new file mode 100644 index 0000000000000..1c52aa905ec6e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryEnabledTest.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test is categories enabled for specific storeView + * + * Preconditions: + * Fixture with enabled and disabled categories in two stores created + * Steps: + * Set Headers - Store = ukrainian + * Send Request: + * query{ + * category(id: %categoryId%){ + * id + * name + * } + * } + * Expected response: + * { + * "category": { + * "id": %categoryId%, + * "name": "Category_UA" + * } + * } + * + * @magentoApiDataFixture Magento/Catalog/_files/category_enabled_for_store.php + */ +class CategoryEnabledTest extends GraphQlAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Verify that category enabled for specific store view + * + * @param string $query + * @param string $storeCode + * @param array $category + * @return void + * @throws \Exception + * @dataProvider categoryEnabledDataProvider + */ + public function testCategoryEnabledForSpecificStoreView(string $query, string $storeCode, array $category): void + { + $response = $this->graphQlQuery($query, [], '', ['store' => $storeCode]); + + // check are there any items in the return data + self::assertNotNull($response['category'], 'category must not be null'); + + // check entire response + $this->assertResponseFields($response, $category); + } + + /** + * Verify that category disabled for specific store view + * + * @param string $query + * @param string $storeCode + * @param array $category + * @return void + * @throws \Exception + * @dataProvider categoryDisabledDataProvider + */ + public function testCategoryDisabledForSpecificStoreView(string $query, string $storeCode, array $category): void + { + $this->markTestSkipped( + 'GraphQL response currently return Exception instead of data structure - MC-20132' + ); + $response = $this->graphQlQuery($query, [], '', ['store' => $storeCode]); + + // check are there any items in the return data + self::assertNotNull($response['category'], 'category must not be null'); + + // check entire response + $this->assertResponseFields($response, $category); + } + + /** + * Data provider for enabled category + * + * @return array + */ + public function categoryEnabledDataProvider(): array + { + return [ + [ + 'query' => $this->getQuery(44), + 'store' => 'default', + 'data' => [ + 'category' => [ + 'id' => 44, + 'name' => 'Category_UA', + ], + ] + ], + ]; + } + + /** + * Data provider for disabled category + * + * @return array[][] + */ + public function categoryDisabledDataProvider(): array + { + return [ + [ + 'query' => $this->getQuery(33), + 'store' => 'english', + 'data' => [ + 'category' => null, + ], + ], + ]; + } + + /** + * Return GraphQL query string by categoryId + * + * @param int $categoryId + * @return string + */ + private function getQuery(int $categoryId): string + { + return <<<QUERY +{ + category(id: {$categoryId}){ + id + name + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php index 00eb235cb4dc3..43612575a7dcb 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -215,7 +215,7 @@ public function testQueryChildCategoriesWithProducts() $this->assertEquals('Its a description of Test Category 1.2', $secondChildCategory['description']); $firstChildCategoryExpectedProducts = [ ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => 'simple', 'name' => 'Simple Product'] ]; $this->assertCategoryProducts($secondChildCategory, $firstChildCategoryExpectedProducts); $firstChildCategoryChildren = []; @@ -629,15 +629,6 @@ public function filterMultipleCategoriesDataProvider(): array 'in', '["category-1-2", "movable"]', [ - [ - 'id' => '7', - 'name' => 'Movable', - 'url_key' => 'movable', - 'url_path' => 'movable', - 'children_count' => '0', - 'path' => '1/2/7', - 'position' => '3' - ], [ 'id' => '13', 'name' => 'Category 1.2', @@ -646,6 +637,15 @@ public function filterMultipleCategoriesDataProvider(): array 'children_count' => '0', 'path' => '1/2/3/13', 'position' => '2' + ], + [ + 'id' => '7', + 'name' => 'Movable', + 'url_key' => 'movable', + 'url_path' => 'movable', + 'children_count' => '0', + 'path' => '1/2/7', + 'position' => '3' ] ] ], @@ -714,4 +714,60 @@ private function assertCategoryChildren(array $category, array $expectedChildren $this->assertResponseFields($category['children'][$i], $expectedChild); } } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryInlineFragment() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {eq: "6"}}){ + ... on CategoryTree { + id + name + url_key + url_path + children_count + path + position + } + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryNamedFragment() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {eq: "6"}}){ + ...Cat + } +} + +fragment Cat on CategoryTree { + id + name + url_key + url_path + children_count + path + position +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategorySpecificFieldsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategorySpecificFieldsTest.php new file mode 100644 index 0000000000000..830585bed88b5 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategorySpecificFieldsTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test is category fields + * + * Preconditions: + * Fixture with category in two stores created + * Steps: + * Set Headers - Store = default + * Send Request: + * query{ + * category(id: %categoryId%){ + * id + * include_in_menu + * name + * image + * description + * display_mode + * available_sort_by + * default_sort_by + * url_key + * meta_title + * meta_keywords + * meta_description + * } + * } + * Expected response: + * { + * "category": { + * "id": 9, + * "include_in_menu": 0, + * "name": "Category_en", + * "image": NULL, + * "description": "<p>Category_en Description</p>", + * "display_mode": "PRODUCTS_AND_PAGE", + * "available_sort_by": [ + * "name", + * "price" + * ], + * "default_sort_by": "price", + * "url_key": "category-en", + * "meta_title": "Category_en Meta Title", + * "meta_keywords": "Category_en Meta Keywords", + * "meta_description": "Category_en Meta Description" + * } + * } + */ +class CategorySpecificFieldsTest extends GraphQlAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Verify that search returns correct values for given price filter + * + * @magentoApiDataFixture Magento/Catalog/_files/category_specific_fields.php + * @param int $categoryId + * @param array $categoryFields + * @return void + * @throws \Exception + * @dataProvider categoryFieldsDataProvider + */ + public function testSpecificCategoryFields(int $categoryId, array $categoryFields): void + { + $query = <<<QUERY +{ + category(id: {$categoryId}){ + id + include_in_menu + name + description + display_mode + available_sort_by + default_sort_by + url_key + meta_title + meta_keywords + meta_description + } +} +QUERY; + $response = $this->graphQlQuery($query); + + // check are there any items in the return data + self::assertNotNull($response['category'], 'category must not be null'); + + // check entire response + $this->assertResponseFields($response['category'], $categoryFields); + } + + /** + * Data provider for enabled category + * + * @return array[][] + */ + public function categoryFieldsDataProvider(): array + { + return [ + [ + 'category_id' => 10, + 'category_fields' => [ + 'id' => 10, + 'include_in_menu' => 0, + 'name' => 'Category_en', + 'description' => 'Category_en Description', + 'display_mode' => 'PRODUCTS_AND_PAGE', + 'available_sort_by' => [ + 'name', + 'price', + ], + 'default_sort_by' => 'price', + 'url_key' => 'category-en', + 'meta_title' => 'Category_en Meta Title', + 'meta_keywords' => 'Category_en Meta Keywords', + 'meta_description' => 'Category_en Meta Description', + ], + ], + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/Options/Uid/CustomizableOptionsUidTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/Options/Uid/CustomizableOptionsUidTest.php new file mode 100644 index 0000000000000..c5a44d5ff68b3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/Options/Uid/CustomizableOptionsUidTest.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog\Options\Uid; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for product custom options uid + */ +class CustomizableOptionsUidTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_full_option_set.php + */ + public function testQueryUidForCustomizableOptions() + { + $productSku = 'simple'; + $query = $this->getQuery($productSku); + $response = $this->graphQlQuery($query); + $responseProduct = $response['products']['items'][0]; + self::assertNotEmpty($responseProduct['options']); + + foreach ($responseProduct['options'] as $option) { + if (isset($option['entered_option'])) { + $enteredOption = $option['entered_option']; + $uid = $this->getUidForEnteredValue($option['option_id']); + + self::assertEquals($uid, $enteredOption['uid']); + } elseif (isset($option['selected_option'])) { + $this->assertNotEmpty($option['selected_option']); + + foreach ($option['selected_option'] as $selectedOption) { + $uid = $this->getUidForSelectedValue($option['option_id'], $selectedOption['option_type_id']); + self::assertEquals($uid, $selectedOption['uid']); + } + } + } + } + + /** + * Get uid for entered option + * + * @param int $optionId + * + * @return string + */ + private function getUidForEnteredValue(int $optionId): string + { + return base64_encode('custom-option/' . $optionId); + } + + /** + * Get uid for selected option + * + * @param int $optionId + * @param int $optionValueId + * + * @return string + */ + private function getUidForSelectedValue(int $optionId, int $optionValueId): string + { + return base64_encode('custom-option/' . $optionId . '/' . $optionValueId); + } + + /** + * Get query + * + * @param string $sku + * + * @return string + */ + private function getQuery(string $sku): string + { + return <<<QUERY +query { + products(filter: { sku: { eq: "$sku" } }) { + items { + sku + + ... on CustomizableProductInterface { + options { + option_id + title + + ... on CustomizableRadioOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableDropDownOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableMultipleOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableCheckboxOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableAreaOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableFieldOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableFileOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableDateOption { + option_id + entered_option: value { + uid + } + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreOptionsTest.php new file mode 100644 index 0000000000000..97c6c41ad6397 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreOptionsTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Exception; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductAttributeStoreOptionsTest extends GraphQlAbstract +{ + /** + * Test that custom attribute option labels are returned respecting store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php + * @throws LocalizedException + */ + public function testAttributeStoreLabels(): void + { + $this->attributeLabelTest('Option Default Store'); + $this->attributeLabelTest('Option Test Store', ['Store' => 'test']); + } + + /** + * @param $expectedLabel + * @param array $headers + * @throws LocalizedException + * @throws Exception + */ + private function attributeLabelTest($expectedLabel, array $headers = []): void + { + /** @var Config $eavConfig */ + $eavConfig = Bootstrap::getObjectManager()->get(Config::class); + $attributeCode = 'test_configurable'; + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $optionValues = []; + + foreach ($options as $option) { + $optionValues[] = [ + 'value' => $option->getValue(), + ]; + } + + $expectedOptions = [ + [ + 'label' => $expectedLabel, + 'value' => $optionValues[0]['value'] + ] + ]; + + $query = <<<QUERY +{ + products(search:"Simple", + pageSize: 3 + currentPage: 1 + ) + { + aggregations + { + attribute_code + options + { + label + value + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $headers); + $this->assertNotEmpty($response['products']['aggregations']); + $actualAttributes = $response['products']['aggregations']; + $actualAttributeOptions = []; + + foreach ($actualAttributes as $actualAttribute) { + if ($actualAttribute['attribute_code'] === $attributeCode) { + $actualAttributeOptions = $actualAttribute['options']; + } + } + + $this->assertNotEmpty($actualAttributeOptions); + + foreach ($actualAttributeOptions as $key => $actualAttributeOption) { + if ($actualAttributeOption['value'] === $expectedOptions[$key]['value']) { + $this->assertEquals($actualAttributeOption['label'], $expectedOptions[$key]['label']); + } + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php index a34d5e21704af..b4b57b3817d3d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php @@ -52,7 +52,7 @@ public function testAttributeTypeResolver() attribute_type entity_type input_type - } + } } } QUERY; @@ -125,7 +125,7 @@ public function testComplexAttributeTypeResolver() attribute_type entity_type input_type - } + } } } QUERY; @@ -199,8 +199,8 @@ public function testUnDefinedAttributeType() { attribute_code attribute_type - entity_type - } + entity_type + } } } QUERY; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php new file mode 100644 index 0000000000000..b19b8d519e857 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test that product is not present in GQL after it was deleted + */ +class ProductDeleteTest extends GraphQlAbstract +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + protected function setUp(): void + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_all_fields.php + */ + public function testQuerySimpleProductAfterDelete() + { + $productSku = 'simple'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + attribute_set_id + } + } +} +QUERY; + // get customer ID token + /** @var \Magento\Integration\Api\CustomerTokenServiceInterface $customerTokenService */ + $customerTokenService = $this->objectManager->create( + \Magento\Integration\Api\CustomerTokenServiceInterface::class + ); + $customerToken = $customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); + + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertArrayHasKey('products', $response); + $this->assertArrayHasKey('items', $response['products']); + $this->assertCount(1, $response['products']['items']); + + // Delete the product and verify it is actually not accessible via the storefront anymore + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + + $registry = ObjectManager::getInstance()->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $productRepository->deleteById($productSku); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertArrayHasKey('products', $response); + $this->assertArrayHasKey('items', $response['products']); + $this->assertCount(0, $response['products']['items']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php index a75fb1e1e5ced..9dbd902f1714e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php @@ -17,7 +17,10 @@ class ProductSearchAggregationsTest extends GraphQlAbstract */ public function testAggregationBooleanAttribute() { - $this->reindex(); + $this->markTestSkipped( + 'MC-22184: Elasticsearch returns incorrect aggregation options for booleans' + . 'MC-36768: Custom attribute not appears in elasticsearch' + ); $skus= '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"'; $query = <<<QUERY @@ -61,21 +64,9 @@ function ($a) { $this->assertEquals('boolean_attribute', $booleanAggregation['attribute_code']); $this->assertContainsEquals(['label' => '1', 'value'=> '1', 'count' => '3'], $booleanAggregation['options']); - $this->markTestIncomplete('MC-22184: Elasticsearch returns incorrect aggregation options for booleans'); + $this->markTestSkipped('MC-22184: Elasticsearch returns incorrect aggregation options for booleans'); $this->assertEquals(2, $booleanAggregation['count']); $this->assertCount(2, $booleanAggregation['options']); $this->assertContainsEquals(['label' => '0', 'value'=> '0', 'count' => '2'], $booleanAggregation['options']); } - - /** - * Reindex - * - * @throws \Magento\Framework\Exception\LocalizedException - */ - private function reindex() - { - $appDir = dirname(Bootstrap::getInstance()->getAppTempDir()); - // phpcs:ignore Magento2.Security.InsecureFunction - exec("php -f {$appDir}/bin/magento indexer:reindex"); - } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchPriceFilterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchPriceFilterTest.php new file mode 100644 index 0000000000000..bafe972a2ecfb --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchPriceFilterTest.php @@ -0,0 +1,270 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Product filtering by condition "FROM..TO" for "Price" attribute + * + * Preconditions: + * Fixture simple products created + * Steps: + * Send request: + * query test { + * products(search: "Product", filter: {price: {from: "0.01" to: "9.99"}}, sort: {price: ASC}) { + * items { + * name + * } + * total_count + * } + * } + * Expected Response: + * { + * "data": { + * "products": { + * "items": [ + * { + * "name": "Product 2 $0.01" + * }, + * { + * "name": "Product 3 $5" + * }, + * { + * "name": "Product 4 $9.99" + * } + * ], + * "total_count": 3 + * } + * } + * } + */ +class ProductSearchPriceFilterTest extends GraphQlAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Verify that search returns correct values for given price filter + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_different_price.php + * @param string $priceFilter + * @param string $sort + * @param array $items + * @return void + * @dataProvider productSearchPriceDataProvider + * @throws \Exception + */ + public function testProductSearchPriceFilter($priceFilter, string $sort, array $items): void + { + // expected stuff + $totalCount = count($items); + $expectedFirstItemPriceValue = reset($items)['price']['minimalPrice']['amount']['value']; + $expectedLastItemPriceValue = end($items)['price']['minimalPrice']['amount']['value']; + $assertionMap = [ + 'products' => [ + 'items' => $items, + 'total_count' => $totalCount, + ], + ]; + + $query = <<<QUERY +{ + products(search: "Product", filter: {price: {{$priceFilter}}}, sort: {{$sort}}) { + items { + name + price { + minimalPrice { + amount { + value + } + } + } + } + total_count + } +} +QUERY; + + $response = $this->graphQlQuery($query); + + // check are there any items in the return data + self::assertNotNull( + $response['products']['items'], + 'product items must not be null' + ); + + // check for the total of items in return + self::assertCount( + $totalCount, + $response['products']['items'], + "there are should be $totalCount products in price range $priceFilter" + ); + + // prepare first and last item from response for assertions + $responseFirstItem = reset($response['products']['items']); + $responseLastItem = end($response['products']['items']); + // check are there price in the first item + self::assertArrayHasKey('price', $responseFirstItem, 'product item must have price'); + // check are there price in for the last item + self::assertArrayHasKey('price', $responseLastItem, 'product item must have price'); + + // prepare first and last item price value from response for assertions + $responseFirstItemPriceValue = $responseFirstItem['price']['minimalPrice']['amount']['value'] ?? null; + $responseLastItemPriceValue = $responseLastItem['price']['minimalPrice']['amount']['value'] ?? null; + // check are there price value in for the first item + self::assertNotNull($responseFirstItemPriceValue, 'first product item must have price value'); + // check are there price value in for the first item + self::assertNotNull($responseLastItemPriceValue, 'last product item must have price value'); + + // check price value for the first item in return + self::assertEquals( + $expectedFirstItemPriceValue, + $responseFirstItemPriceValue, + sprintf( + 'price for the first product must be %s as it sorted by price ASC', + $expectedFirstItemPriceValue + ) + ); + + // check price value for the last item in return + self::assertEquals( + $expectedLastItemPriceValue, + $responseLastItemPriceValue, + sprintf( + 'price for the first product must be %s as it sorted by price ASC', + $expectedLastItemPriceValue + ) + ); + + // check entire response + $this->assertResponseFields($response, $assertionMap); + } + + /** + * Data provider for product search price filter + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array[][] + */ + public function productSearchPriceDataProvider(): array + { + return [ + [ + 'price_filter' => 'from: "0.01" to: "9.99"', + 'sort' => 'price: ASC', + 'items' => [ + [ + 'name' => 'Product with price 0.01', + 'price' => [ + 'minimalPrice' => [ + 'amount' => [ + 'value' => 0.01, + ], + ], + ], + ], + [ + 'name' => 'Product with price 5', + 'price' => [ + 'minimalPrice' => [ + 'amount' => [ + 'value' => 5, + ], + ], + ], + ], + [ + 'name' => 'Product with price 9.99', + 'price' => [ + 'minimalPrice' => [ + 'amount' => [ + 'value' => 9.99, + ], + ], + ], + ], + ], + ], + [ + 'price_filter' => 'from: "5.01" to: "10"', + 'sort' => 'price: DESC', + 'items' => [ + [ + 'name' => 'Product with price 10', + 'price' => [ + 'minimalPrice' => [ + 'amount' => [ + 'value' => 10, + ], + ], + ], + ], + [ + 'name' => 'Product with price 9.99', + 'price' => [ + 'minimalPrice' => [ + 'amount' => [ + 'value' => 9.99, + ], + ], + ], + ], + ], + ], + [ + 'price_filter' => 'from: "5"', + 'sort' => 'price: DESC', + 'items' => [ + [ + 'name' => 'Product with price 10', + 'price' => [ + 'minimalPrice' => [ + 'amount' => [ + 'value' => 10, + ], + ], + ], + ], + [ + 'name' => 'Product with price 9.99', + 'price' => [ + 'minimalPrice' => [ + 'amount' => [ + 'value' => 9.99, + ], + ], + ], + ], + [ + 'name' => 'Product with price 5', + 'price' => [ + 'minimalPrice' => [ + 'amount' => [ + 'value' => 5, + ], + ], + ], + ], + ], + ], + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 1a95a3d6f4925..f755a1a1e0282 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -13,16 +13,16 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\CategoryLinkManagement; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Eav\Model\Config; -use Magento\TestFramework\ObjectManager; -use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\Catalog\Model\Product; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Framework\DataObject; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\CacheCleaner; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; /** * @SuppressWarnings(PHPMD.TooManyPublicMethods) @@ -73,7 +73,8 @@ public function testFilterForNonExistingCategory() */ public function testFilterLn() { - $this->reIndexAndCleanCache(); + $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' + . 'fixtures product not appears in elasticsearch'); $query = <<<QUERY { products ( @@ -154,6 +155,8 @@ private function compareFilterNames(array $a, array $b) */ public function testLayeredNavigationForConfigurableProducts() { + $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' + . 'fixtures product not appears in elasticsearch'); CacheCleaner::cleanAll(); $attributeCode = 'test_configurable'; @@ -166,7 +169,6 @@ public function testLayeredNavigationForConfigurableProducts() $firstOption = $options[0]->getValue(); $secondOption = $options[1]->getValue(); $query = $this->getQueryProductsWithArrayOfCustomAttributes($attributeCode, $firstOption, $secondOption); - $this->reIndexAndCleanCache(); $response = $this->graphQlQuery($query); $this->assertEquals(2, $response['products']['total_count']); @@ -179,18 +181,18 @@ public function testLayeredNavigationForConfigurableProducts() $response['products']['aggregations'][1], [ 'attribute_code' => $attribute->getAttributeCode(), - 'label'=> $attribute->getDefaultFrontendLabel(), - 'count'=> 2, + 'label' => $attribute->getDefaultFrontendLabel(), + 'count' => 2, 'options' => [ [ 'label' => 'Option 1', 'value' => $firstOption, - 'count' =>'2' + 'count' => '2' ], [ 'label' => 'Option 2', 'value' => $secondOption, - 'count' =>'2' + 'count' => '2' ] ], ] @@ -201,7 +203,7 @@ public function testLayeredNavigationForConfigurableProducts() * * @return string */ - private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $firstOption, $secondOption) : string + private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $firstOption, $secondOption): string { return <<<QUERY { @@ -258,6 +260,8 @@ private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $fi */ public function testFilterProductsByDropDownCustomAttribute() { + $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' + . 'fixtures product not appears in elasticsearch'); CacheCleaner::cleanAll(); $attributeCode = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attributeCode); @@ -313,9 +317,8 @@ public function testFilterProductsByDropDownCustomAttribute() $product1 = $productRepository->get('simple'); $product2 = $productRepository->get('12345'); $product3 = $productRepository->get('simple-4'); - $filteredProducts = [$product1, $product2, $product3 ]; + $filteredProducts = [$product3, $product2, $product1]; $countOfFilteredProducts = count($filteredProducts); - $this->reIndexAndCleanCache(); $response = $this->graphQlQuery($query); $this->assertEquals(3, $response['products']['total_count'], 'Number of products returned is incorrect'); $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is not empty'); @@ -327,8 +330,9 @@ public function testFilterProductsByDropDownCustomAttribute() //validate that correct products are returned $this->assertResponseFields( $productItemsInResponse[$itemIndex][0], - [ 'name' => $filteredProducts[$itemIndex]->getName(), - 'sku' => $filteredProducts[$itemIndex]->getSku() + [ + 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku() ] ); } @@ -341,34 +345,19 @@ public function testFilterProductsByDropDownCustomAttribute() $response['products']['aggregations'][2], [ 'attribute_code' => $attribute->getAttributeCode(), - 'count'=> 1, - 'label'=> $attribute->getDefaultFrontendLabel(), + 'count' => 1, + 'label' => $attribute->getDefaultFrontendLabel(), 'options' => [ [ 'label' => 'Option 3', - 'count' => 3, - 'value' => $optionValue - ], - ], + 'count' => 3, + 'value' => $optionValue + ], + ], ] ); } - /** - * @return void - * @throws \Magento\Framework\Exception\LocalizedException - */ - private function reIndexAndCleanCache() : void - { - $appDir = dirname(Bootstrap::getInstance()->getAppTempDir()); - $out = ''; - // phpcs:ignore Magento2.Security.InsecureFunction - exec("php -f {$appDir}/bin/magento indexer:reindex catalog_category_product", $out); - // phpcs:ignore Magento2.Security.InsecureFunction - exec("php -f {$appDir}/bin/magento indexer:reindex catalogsearch_fulltext", $out); - CacheCleaner::cleanAll(); - } - /** * Filter products using an array of multi select custom attributes * @@ -378,7 +367,6 @@ private function reIndexAndCleanCache() : void public function testFilterProductsByMultiSelectCustomAttributes() { $objectManager = Bootstrap::getObjectManager(); - $this->reIndexAndCleanCache(); $attributeCode = 'multiselect_attribute'; /** @var Config $eavConfig */ $eavConfig = $objectManager->get(Config::class); @@ -451,7 +439,7 @@ public function testFilterProductsByMultiSelectCustomAttributes() * @param string $attributeCode * @return string */ - private function getDefaultAttributeOptionValue(string $attributeCode) : string + private function getDefaultAttributeOptionValue(string $attributeCode): string { /** @var Config $eavConfig */ $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(Config::class); @@ -471,7 +459,8 @@ private function getDefaultAttributeOptionValue(string $attributeCode) : string */ public function testSearchAndFilterByCustomAttribute() { - $this->reIndexAndCleanCache(); + $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' + . 'fixtures product not appears in elasticsearch'); $attribute_code = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attribute_code); @@ -530,11 +519,13 @@ public function testSearchAndFilterByCustomAttribute() $this->assertCount(3, $response['products']['aggregations']); $expectedFilterLayers = [ - ['name' => 'Category', - 'request_var'=> 'cat' + [ + 'name' => 'Category', + 'request_var' => 'cat' ], - ['name' => 'Second Test Configurable', - 'request_var'=> 'second_test_configurable' + [ + 'name' => 'Second Test Configurable', + 'request_var' => 'second_test_configurable' ] ]; $layers = array_map(null, $expectedFilterLayers, $response['products']['filters']); @@ -559,22 +550,22 @@ public function testSearchAndFilterByCustomAttribute() $response['products']['aggregations'][0], [ 'attribute_code' => 'price', - 'count'=> 2, - 'label'=> 'Price', + 'count' => 2, + 'label' => 'Price', 'options' => [ [ 'count' => 2, 'label' => '10-20', 'value' => '10_20', - ], + ], [ 'count' => 1, - 'label' => '40-*', - 'value' => '40_*', + 'label' => '40-50', + 'value' => '40_50', ], - ], + ], ] ); // Validate the custom attribute layer of aggregations from the response @@ -582,8 +573,8 @@ public function testSearchAndFilterByCustomAttribute() $response['products']['aggregations'][2], [ 'attribute_code' => $attribute_code, - 'count'=> 1, - 'label'=> 'Second Test Configurable', + 'count' => 1, + 'label' => 'Second Test Configurable', 'options' => [ [ 'count' => 3, @@ -601,9 +592,9 @@ public function testSearchAndFilterByCustomAttribute() $this->assertResponseFields( $response['products']['aggregations'][1], [ - 'attribute_code' => 'category_id', - 'count'=> 7, - 'label'=> 'Category' + 'attribute_code' => 'category_id', + 'count' => 7, + 'label' => 'Category' ] ); } @@ -616,7 +607,8 @@ public function testSearchAndFilterByCustomAttribute() */ public function testFilterByCategoryIdAndCustomAttribute() { - $this->reIndexAndCleanCache(); + $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' + . 'fixtures product not appears in elasticsearch'); $categoryId = 13; $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); $query = <<<QUERY @@ -698,37 +690,37 @@ public function testFilterByCategoryIdAndCustomAttribute() $expectedCategoryInAggregations = [ [ - 'count' => 2, - 'label' => 'Category 1', - 'value'=> '3' + 'count' => 2, + 'label' => 'Category 1', + 'value' => '3' ], [ - 'count'=> 1, + 'count' => 1, 'label' => 'Category 1.1', - 'value'=> '4' + 'value' => '4' ], [ - 'count'=> 1, + 'count' => 1, 'label' => 'Movable Position 2', - 'value'=> '10' + 'value' => '10' ], [ - 'count'=> 1, + 'count' => 1, 'label' => 'Movable Position 3', - 'value'=> '11' + 'value' => '11' ], [ - 'count'=> 1, + 'count' => 1, 'label' => 'Category 12', - 'value'=> '12' + 'value' => '12' ], [ - 'count'=> 2, + 'count' => 2, 'label' => 'Category 1.2', - 'value'=> '13' + 'value' => '13' ], ]; // presort expected and actual results as different search engines have different orders @@ -833,7 +825,7 @@ public function testFilterBySingleProductUrlKey() [ 'name' => $product->getName(), 'sku' => $product->getSku(), - 'url_key'=> $product->getUrlKey() + 'url_key' => $product->getUrlKey() ] ); $this->assertEquals('Price', $response['products']['aggregations'][0]['label']); @@ -898,8 +890,8 @@ public function testFilterByMultipleProductUrlKeys() $product1 = $productRepository->get('simple'); $product2 = $productRepository->get('12345'); $product3 = $productRepository->get('simple-4'); - $filteredProducts = [$product1, $product2, $product3]; - $urlKey =[]; + $filteredProducts = [$product3, $product2, $product1]; + $urlKey = []; foreach ($filteredProducts as $product) { $urlKey[] = $product->getUrlKey(); } @@ -956,9 +948,10 @@ public function testFilterByMultipleProductUrlKeys() //validate that correct products are returned $this->assertResponseFields( $productItemsInResponse[$itemIndex][0], - [ 'name' => $filteredProducts[$itemIndex]->getName(), - 'sku' => $filteredProducts[$itemIndex]->getSku(), - 'url_key'=> $filteredProducts[$itemIndex]->getUrlKey() + [ + 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku(), + 'url_key' => $filteredProducts[$itemIndex]->getUrlKey() ] ); } @@ -1124,7 +1117,6 @@ public function testFilterWithinSpecificPriceRangeSortedByNameDesc() */ public function testSortByPosition() { - $this->reIndexAndCleanCache(); // Get category ID for filtering /** @var Collection $categoryCollection */ $categoryCollection = Bootstrap::getObjectManager()->get(Collection::class); @@ -1177,7 +1169,6 @@ public function testSortByPosition() $category->setPostedProducts($productPositions); $category->save(); - $this->reIndexAndCleanCache(); $queryDesc = <<<QUERY { @@ -1209,7 +1200,6 @@ public function testSortByPosition() public function testSearchWithFilterWithPageSizeEqualTotalCount() { - $this->reIndexAndCleanCache(); $query = <<<QUERY { @@ -1419,7 +1409,7 @@ public function testFilterProductsForExactMatchingName() ); $this->assertArrayHasKey('aggregations', $response['products']); $this->assertCount(2, $response['products']['aggregations']); - $expectedAggregations =[ + $expectedAggregations = [ [ 'attribute_code' => 'price', 'count' => 2, @@ -1431,8 +1421,8 @@ public function testFilterProductsForExactMatchingName() 'count' => 1, ], [ - 'label' => '20-*', - 'value' => '20_*', + 'label' => '20-30', + 'value' => '20_30', 'count' => 1, ] ] @@ -1532,7 +1522,7 @@ public function testFilterProductsBySingleCategoryId() QUERY; $response = $this->graphQlQuery($query); - $this->assertEquals(2, $response['products']['total_count'], 'Incorrect count of products returned'); + $this->assertEquals(2, $response['products']['total_count'], 'Incorrect count of products returned'); /** @var CategoryLinkManagement $productLinks */ $productLinks = ObjectManager::getInstance()->get(CategoryLinkManagement::class); /** @var CategoryRepositoryInterface $categoryRepository */ @@ -1549,7 +1539,7 @@ public function testFilterProductsBySingleCategoryId() $product = $productRepository->get($links[$itemIndex]->getSku()); $this->assertEquals($response['products']['items'][$itemIndex]['name'], $product->getName()); $this->assertEquals($response['products']['items'][$itemIndex]['type_id'], $product->getTypeId()); - $categoryIds = $product->getCategoryIds(); + $categoryIds = $product->getCategoryIds(); foreach ($categoryIds as $index => $value) { $categoryIds[$index] = (int)$value; } @@ -1587,8 +1577,7 @@ public function testFilterProductsBySingleCategoryId() */ public function testSearchAndSortByRelevance() { - $this->reIndexAndCleanCache(); - $search_term ="blue"; + $search_term = "blue"; $query = <<<QUERY { @@ -1640,7 +1629,7 @@ public function testSearchAndSortByRelevance() $this->assertNotEmpty($response['products']['filters'], 'Filters should have the Category layer'); $this->assertEquals('Colorful Category', $response['products']['filters'][0]['filter_items'][0]['label']); $this->assertCount(2, $response['products']['aggregations']); - $productsInResponse = ['Blue briefs','Navy Blue Striped Shoes','Grey shorts']; + $productsInResponse = ['Blue briefs', 'Navy Blue Striped Shoes', 'Grey shorts']; $count = count($response['products']['items']); for ($i = 0; $i < $count; $i++) { $this->assertEquals($productsInResponse[$i], $response['products']['items'][$i]['name']); @@ -1709,6 +1698,7 @@ public function testFilterByExactSkuAndSortByPriceDesc() $this->assertEquals(20, $response['products']['page_info']['page_size']); $this->assertEquals(1, $response['products']['page_info']['current_page']); } + /** * Fuzzy search filtered for price and sorted by price and name * @@ -1716,10 +1706,9 @@ public function testFilterByExactSkuAndSortByPriceDesc() */ public function testProductBasicFullTextSearchQuery() { - $this->reIndexAndCleanCache(); $textToSearch = 'blue'; $query - =<<<QUERY + = <<<QUERY { products( search: "{$textToSearch}" @@ -1808,10 +1797,9 @@ public function testProductBasicFullTextSearchQuery() */ public function testProductPartialNameFullTextSearchQuery() { - $this->reIndexAndCleanCache(); $textToSearch = 'Sim'; $query - =<<<QUERY + = <<<QUERY { products( search: "{$textToSearch}" @@ -1899,10 +1887,9 @@ public function testProductPartialNameFullTextSearchQuery() */ public function testProductPartialSkuFullTextSearchQuery() { - $this->reIndexAndCleanCache(); $textToSearch = 'prd'; $query - =<<<QUERY + = <<<QUERY { products( search: "{$textToSearch}" @@ -1990,14 +1977,13 @@ public function testProductPartialSkuFullTextSearchQuery() */ public function testProductPartialSkuHyphenatedFullTextSearchQuery() { - $this->reIndexAndCleanCache(); /** @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); $prod2 = $productRepository->get('prd2-sku2'); $textToSearch = 'sku2'; $query - =<<<QUERY + = <<<QUERY { products( search: "{$textToSearch}" @@ -2098,7 +2084,7 @@ public function testFilterWithinASpecificPriceRangeSortedByPriceDESC() } $query - =<<<QUERY + = <<<QUERY { products( filter: @@ -2335,7 +2321,7 @@ public function testQueryWithNoSearchOrFilterArgumentException() public function testFilterProductsThatAreOutOfStockWithConfigSettings() { $query - =<<<QUERY + = <<<QUERY { products( filter: @@ -2455,7 +2441,8 @@ private function assertProductItems(array $filteredProducts, array $actualRespon $this->assertNotEmpty($productItemsInResponse[$itemIndex]); $this->assertResponseFields( $productItemsInResponse[$itemIndex][0], - ['attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), + [ + 'attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), 'sku' => $filteredProducts[$itemIndex]->getSku(), 'name' => $filteredProducts[$itemIndex]->getName(), 'price' => [ @@ -2466,7 +2453,7 @@ private function assertProductItems(array $filteredProducts, array $actualRespon ] ] ], - 'type_id' =>$filteredProducts[$itemIndex]->getTypeId(), + 'type_id' => $filteredProducts[$itemIndex]->getTypeId(), 'weight' => $filteredProducts[$itemIndex]->getWeight() ] ); @@ -2481,7 +2468,8 @@ private function assertProductItemsWithPriceCheck(array $filteredProducts, array $this->assertNotEmpty($itemArray); $this->assertResponseFields( $productItemsInResponse[$itemIndex][0], - ['attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), + [ + 'attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), 'sku' => $filteredProducts[$itemIndex]->getSku(), 'name' => $filteredProducts[$itemIndex]->getName(), 'price' => [ @@ -2497,15 +2485,15 @@ private function assertProductItemsWithPriceCheck(array $filteredProducts, array 'currency' => 'USD' ] ], - 'regularPrice' => [ - 'amount' => [ - 'value' => $filteredProducts[$itemIndex]->getPrice(), - 'currency' => 'USD' - ] + 'regularPrice' => [ + 'amount' => [ + 'value' => $filteredProducts[$itemIndex]->getPrice(), + 'currency' => 'USD' ] + ] ], - 'type_id' =>$filteredProducts[$itemIndex]->getTypeId(), + 'type_id' => $filteredProducts[$itemIndex]->getTypeId(), 'weight' => $filteredProducts[$itemIndex]->getWeight() ] ); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index 99fdfb2cf1b00..9946e74a24994 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -653,8 +653,10 @@ public function testProductPrices() $secondProduct = $productRepository->get($secondProductSku, false, null, true); self::assertNotNull($response['products']['items'][0]['price'], "price must be not null"); self::assertCount(2, $response['products']['items']); - $this->assertBaseFields($firstProduct, $response['products']['items'][0]); - $this->assertBaseFields($secondProduct, $response['products']['items'][1]); + + // by default sort order is: "newest id first" + $this->assertBaseFields($secondProduct, $response['products']['items'][0]); + $this->assertBaseFields($firstProduct, $response['products']['items'][1]); } /** @@ -665,7 +667,8 @@ private function assertMediaGalleryEntries($product, $actualResponse) { $mediaGalleryEntries = $product->getMediaGalleryEntries(); $this->assertCount(1, $mediaGalleryEntries, "Precondition failed, incorrect number of media gallery entries."); - $this->assertIsArray([$actualResponse['media_gallery_entries']], + $this->assertIsArray( + [$actualResponse['media_gallery_entries']], "Media galleries field must be of an array type." ); $this->assertCount(1, $actualResponse['media_gallery_entries'], "There must be 1 record in media gallery."); @@ -701,10 +704,10 @@ private function assertMediaGalleryEntries($product, $actualResponse) */ private function assertCustomAttribute($actualResponse) { - $customAttribute = null; + $customAttribute = 'customAttributeValue'; $this->assertEquals($customAttribute, $actualResponse['attribute_code_custom']); } - + /** * @param ProductInterface $product * @param $actualResponse @@ -1047,6 +1050,8 @@ public function testProductWithNonAnchoredParentCategory() */ public function testProductInNonAnchoredSubCategories() { + $this->markTestSkipped('MC-30965: Product contains invalid categories'); + $query = <<<QUERY { products(filter: diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ReadCategoryAfterDeleteTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ReadCategoryAfterDeleteTest.php new file mode 100644 index 0000000000000..0ce52c45eb282 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ReadCategoryAfterDeleteTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test category read after children category was deleted + * + * Preconditions: + * Fixture with categories tree created + * Steps: + * - Delete child category + * - Get category tree + * - Verify that tree doesn't contain deleted category + */ +class ReadCategoryAfterDeleteTest extends GraphQlAbstract +{ + /** + * Verify that after delete children category data category tree returns correct values for given category + * + * @magentoApiDataFixture Magento/Catalog/_files/category_tree.php + * @dataProvider categoriesDeleteDataProvider() + * @param int $categoryToDelete + * @param array $expectedResult + * @return void + * @throws \Exception + */ + public function testCategoryDelete($categoryToDelete, $expectedResult): void + { + $this->deleteCategory($categoryToDelete); + + $query = $this->getQuery(400); + $response = $this->graphQlQuery($query, [], '', ['store' => 'default']); + $this->assertResponseFields($response, $expectedResult); + } + + /** + * Return GraphQL query string by categoryId + * + * @param int $categoryId + * @return string + */ + private function getQuery(int $categoryId): string + { + return <<<QUERY +{ + categoryList(filters: {ids: {in: ["$categoryId"]}}) { + id + name + children_count + children { + id + name + children_count + } + } +} +QUERY; + } + + /** + * @param int $categoryId + * @return void + */ + private function deleteCategory(int $categoryId): void + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $registry = $objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + $category = $objectManager->create(\Magento\Catalog\Model\Category::class); + $category->load($categoryId); + if ($category->getId()) { + $category->delete(); + } + + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * @return array + */ + public function categoriesDeleteDataProvider(): array + { + return [ + [ + 'category_to_delete' => 402, + 'expected_result' => [ + 'categoryList' => [ + [ + 'id' => 400, + 'name' => 'Category 1', + 'children_count' => 1, + 'children' => [ + [ + 'id' => 401, + 'name' => 'Category 1.1', + 'children_count' => 0, + ] + ] + ] + ] + ] + ] + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/PriceTiersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/PriceTiersTest.php new file mode 100644 index 0000000000000..a3f98c4cd81ba --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/PriceTiersTest.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogCustomer; + +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +class PriceTiersTest extends GraphQlAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var GetCustomerAuthenticationHeader + */ + private $getCustomerAuthenticationHeader; + + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->getCustomerAuthenticationHeader = $this->objectManager->get(GetCustomerAuthenticationHeader::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testAllGroups() + { + $productSku = 'simple'; + $query = $this->getProductSearchQuery($productSku); + + $response = $this->graphQlQuery($query); + + $itemTiers = $response['products']['items'][0]['price_tiers']; + $this->assertCount(5, $itemTiers); + $this->assertEquals(8, $this->getValueForQuantity(2, $itemTiers)); + $this->assertEquals(5, $this->getValueForQuantity(3, $itemTiers)); + $this->assertEquals(6, $this->getValueForQuantity(3.2, $itemTiers)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups.php + */ + public function testLoggedInCustomer() + { + $productSku = 'simple'; + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthenticationHeader->execute('customer@example.com', 'password') + ); + + $itemTiers = $response['products']['items'][0]['price_tiers']; + $this->assertCount(3, $itemTiers); + $this->assertEquals(9.25, $this->getValueForQuantity(2, $itemTiers)); + $this->assertEquals(8.25, $this->getValueForQuantity(3, $itemTiers)); + $this->assertEquals(7.25, $this->getValueForQuantity(5, $itemTiers)); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_store_with_second_currency.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups.php + */ + public function testSecondStoreViewWithCurrencyRate() + { + $storeViewCode = 'fixture_second_store'; + $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); + $rate = $storeRepository->get($storeViewCode)->getCurrentCurrencyRate(); + $productSku = 'simple'; + $query = $this->getProductSearchQuery($productSku); + $headers = array_merge( + $this->getCustomerAuthenticationHeader->execute('customer@example.com', 'password'), + $this->getHeaderStore($storeViewCode) + ); + + $response = $this->graphQlQuery( + $query, + [], + '', + $headers + ); + + $itemTiers = $response['products']['items'][0]['price_tiers']; + $this->assertCount(3, $itemTiers); + $this->assertEquals(round(9.25 * $rate, 2), $this->getValueForQuantity(2, $itemTiers)); + $this->assertEquals(round(8.25 * $rate, 2), $this->getValueForQuantity(3, $itemTiers)); + $this->assertEquals(round(7.25 * $rate, 2), $this->getValueForQuantity(5, $itemTiers)); + } + + /** + * Get the tier price value for the given product quantity + * + * @param float $quantity + * @param array $tiers + * @return float + */ + private function getValueForQuantity(float $quantity, array $tiers): float + { + $filteredResult = array_values(array_filter($tiers, function ($tier) use ($quantity) { + if ((float)$tier['quantity'] == $quantity) { + return $tier; + } + })); + + return (float)$filteredResult[0]['final_price']['value']; + } + + /** + * Get a query which user filter for product sku and returns price_tiers + * + * @param string $productSku + * @return string + */ + private function getProductSearchQuery(string $productSku): string + { + return <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + price_tiers { + final_price { + currency + value + } + discount { + amount_off + percent_off + } + quantity + } + } + } +} +QUERY; + } + + /** + * Get array that would be used in request header + * + * @param string $storeViewCode + * @return array + */ + private function getHeaderStore(string $storeViewCode): array + { + return ['Store' => $storeViewCode]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/SpecialPriceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/SpecialPriceTest.php new file mode 100644 index 0000000000000..931bb3f3c5d32 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/SpecialPriceTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogCustomer; + +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +class SpecialPriceTest extends GraphQlAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_special_price.php + */ + public function testSpecialPrice() + { + $productSku = 'simple'; + $query = $this->getProductSearchQuery($productSku); + + $response = $this->graphQlQuery($query); + + $specialPrice = (float)$response['products']['items'][0]['special_price']; + $this->assertEquals(5.99, $specialPrice); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_store_with_second_currency.php + * @magentoApiDataFixture Magento/Catalog/_files/product_special_price.php + */ + public function testSpecialPriceWithCurrencyRate() + { + $storeViewCode = 'fixture_second_store'; + $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); + $rate = $storeRepository->get($storeViewCode)->getCurrentCurrencyRate(); + $productSku = 'simple'; + $query = $this->getProductSearchQuery($productSku); + $headers = $this->getHeaderStore($storeViewCode); + + $response = $this->graphQlQuery( + $query, + [], + '', + $headers + ); + + $specialPrice = (float)$response['products']['items'][0]['special_price']; + $this->assertEquals(round(5.99 * $rate, 2), $specialPrice); + } + + /** + * Get a query which user filter for product sku and returns special_price + * + * @param string $productSku + * @return string + */ + private function getProductSearchQuery(string $productSku): string + { + return <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + special_price + } + } +} +QUERY; + } + + /** + * Get array that would be used in request header + * + * @param string $storeViewCode + * @return array + */ + private function getHeaderStore(string $storeViewCode): array + { + return ['Store' => $storeViewCode]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php new file mode 100644 index 0000000000000..a2b7b54fb875a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php @@ -0,0 +1,297 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add configurable product to cart testcases + */ +class AddConfigurableProductToCartSingleMutationTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddConfigurableProductToCart() + { + $product = $this->getConfigurableProductInfo(); + $quantity = 2; + $parentSku = $product['sku']; + $attributeId = (int) $product['configurable_options'][0]['attribute_id']; + $valueIndex = $product['configurable_options'][0]['values'][1]['value_index']; + + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesUIDQuery($attributeId, $valueIndex); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery( + $maskedQuoteId, + $product['sku'], + 2, + $selectedConfigurableOptionsQuery + ); + + $response = $this->graphQlMutation($query); + + $cartItem = current($response['addProductsToCart']['cart']['items']); + self::assertEquals($quantity, $cartItem['quantity']); + self::assertEquals($parentSku, $cartItem['product']['sku']); + self::assertArrayHasKey('configurable_options', $cartItem); + + $option = current($cartItem['configurable_options']); + self::assertEquals($attributeId, $option['id']); + self::assertEquals($valueIndex, $option['value_id']); + self::assertArrayHasKey('option_label', $option); + self::assertArrayHasKey('value_label', $option); + } + + /** + * Generates UID for super configurable product super attributes + * + * @param int $attributeId + * @param int $valueIndex + * @return string + */ + private function generateSuperAttributesUIDQuery(int $attributeId, int $valueIndex): string + { + return 'selected_options: ["' . base64_encode("configurable/$attributeId/$valueIndex") . '"]'; + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddConfigurableProductWithWrongSuperAttributes() + { + $product = $this->getConfigurableProductInfo(); + $quantity = 2; + $parentSku = $product['sku']; + + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesUIDQuery(0, 0); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + $quantity, + $selectedConfigurableOptionsQuery + ); + + $response = $this->graphQlMutation($query); + + self::assertEquals( + 'You need to choose options for your item.', + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddProductIfQuantityIsNotAvailable() + { + $product = $this->getConfigurableProductInfo(); + $parentSku = $product['sku']; + $attributeId = (int) $product['configurable_options'][0]['attribute_id']; + $valueIndex = $product['configurable_options'][0]['values'][1]['value_index']; + + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesUIDQuery($attributeId, $valueIndex); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + 2000, + $selectedConfigurableOptionsQuery + ); + + $response = $this->graphQlMutation($query); + + self::assertEquals( + 'The requested qty is not available', + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddNonExistentConfigurableProductParentToCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $parentSku = 'configurable_no_exist'; + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + 1, + '' + ); + + $response = $this->graphQlMutation($query); + + self::assertEquals( + 'Could not find a product with SKU "configurable_no_exist"', + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_zero_qty_first_child.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testOutOfStockVariationToCart() + { + $product = $this->getConfigurableProductInfo(); + $attributeId = (int) $product['configurable_options'][0]['attribute_id']; + $valueIndex = $product['configurable_options'][0]['values'][0]['value_index']; + $parentSku = $product['sku']; + + $configurableOptionsQuery = $this->generateSuperAttributesUIDQuery($attributeId, $valueIndex); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + 1, + $configurableOptionsQuery + ); + + $response = $this->graphQlMutation($query); + + $expectedErrorMessages = [ + 'There are no source items with the in stock status', + 'This product is out of stock.' + ]; + $this->assertContains( + $response['addProductsToCart']['user_errors'][0]['message'], + $expectedErrorMessages + ); + } + + /** + * @param string $maskedQuoteId + * @param string $parentSku + * @param int $quantity + * @param string $selectedOptionsQuery + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $parentSku, + int $quantity, + string $selectedOptionsQuery + ): string { + return <<<QUERY +mutation { + addProductsToCart( + cartId:"{$maskedQuoteId}" + cartItems: [ + { + sku: "{$parentSku}" + quantity: $quantity + {$selectedOptionsQuery} + } + ] + ) { + cart { + items { + id + quantity + product { + sku + } + ... on ConfigurableCartItem { + configurable_options { + id + option_label + value_id + value_label + } + } + } + }, + user_errors { + message + } + } +} +QUERY; + } + + /** + * Returns information about testable configurable product retrieved from GraphQl query + * + * @return array + * @throws Exception + */ + private function getConfigurableProductInfo(): array + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable')); + return current($searchResponse['products']['items']); + } + + /** + * Returns GraphQl query for fetching configurable product information + * + * @param string $term + * @return string + */ + private function getFetchProductQuery(string $term): string + { + return <<<QUERY +{ + products( + search:"{$term}" + pageSize:1 + ) { + items { + sku + ... on ConfigurableProduct { + configurable_options { + attribute_id + attribute_code + id + label + position + product_id + use_default + values { + default_label + label + store_label + use_default_value + value_index + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductMultipleStoreViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductMultipleStoreViewTest.php new file mode 100644 index 0000000000000..e8d789f6d35e4 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductMultipleStoreViewTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test configurable product queries work correctly with multiple websites + */ +class ConfigurableProductMultipleStoreViewTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_in_multiple_websites.php + */ + public function testConfigurableProductAssignedToOneWebsite() + { + $headerMapFirstStore['Store'] = 'default'; + $headerMapSecondStore['Store'] = 'fixture_second_store'; + $parentSku = 'configurable'; + $query = $this->getQuery($parentSku); + $responseForFirstWebsite = $this->graphQlQuery($query, [], '', $headerMapFirstStore); + $responseForSecondWebsite = $this->graphQlQuery($query, [], '', $headerMapSecondStore); + + $secondWebsiteVariants = $responseForSecondWebsite['products']['items'][0]['variants']; + self::assertEmpty($responseForFirstWebsite['products']['items']); + self::assertEquals(2, count($secondWebsiteVariants)); + self::assertContains('simple_10', $secondWebsiteVariants[0]['product']); + self::assertContains('Option 1', $secondWebsiteVariants[0]['attributes'][0]); + self::assertContains('simple_20', $secondWebsiteVariants[1]['product']); + self::assertContains('Option 2', $secondWebsiteVariants[1]['attributes'][0]); + } + + /** + * @param string $sku + * @return string + */ + private function getQuery(string $sku) + { + return <<<QUERY + { + products(filter: {sku: {eq: "{$sku}"}}) + { + items { + sku + ... on ConfigurableProduct { + variants { + product { + sku + stock_status + } + attributes { + code + label + value_index + } + } + } + } + } + } +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductOptionsTest.php new file mode 100644 index 0000000000000..aac82f07b6b8b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductOptionsTest.php @@ -0,0 +1,243 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test products query for configurable product options + */ +class ConfigurableProductOptionsTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes.php + * @dataProvider expectedResultDataProvider + * @param $expectedOptions + * @throws \Exception + */ + public function testQueryConfigurableProductLinks($expectedOptions) + { + $configurableProduct = 'configurable'; + $query = $this->getQuery($configurableProduct); + + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('products', $response); + $this->assertArrayHasKey('items', $response['products']); + $this->assertConfigurableProductOptions($response['products']['items'], $expectedOptions); + } + + /** + * @param $actualResponse + * @param $expectedOptions + */ + private function assertConfigurableProductOptions($actualResponse, $expectedOptions) + { + $this->assertCount(2, $actualResponse); + + foreach ($actualResponse as $responseProduct) { + $this->assertNotEmpty( + $responseProduct['configurable_options'], + "Precondition failed: 'configurable_options' must not be empty" + ); + $expectedProductOptions = $expectedOptions[$responseProduct['sku']]; + foreach ($expectedProductOptions['configurable_options'] as $optionIndex => $expectedProductOption) { + $responseProductOption = $responseProduct['configurable_options'][$optionIndex]; + $this->assertEquals( + $expectedProductOption['use_default'], + $responseProductOption['use_default'] + ); + $this->assertEquals( + $expectedProductOption['label'], + $responseProductOption['label'] + ); + $this->assertEquals( + $expectedProductOption['position'], + $responseProductOption['position'] + ); + $this->assertEquals( + $expectedProductOption['attribute_code'], + $responseProductOption['attribute_code'] + ); + $optionValuesCount = 2; + self::assertCount( + $optionValuesCount, + $responseProductOption['values'], + 'Product option values count in response is different with real option values' + ); + foreach ($expectedProductOption['values'] as $key => $value) { + $responseProductOptionValue = $responseProductOption['values'][$key]; + $this->assertEquals( + $value['label'], + $responseProductOptionValue['label'] + ); + $this->assertEquals( + $value['store_label'], + $responseProductOptionValue['store_label'] + ); + $this->assertEquals( + $value['default_label'], + $responseProductOptionValue['default_label'] + ); + $this->assertEquals( + $value['use_default_value'], + $responseProductOptionValue['use_default_value'] + ); + } + } + } + } + + /** + * @param string $configurableProduct + * @return string + */ + private function getQuery($configurableProduct) + { + return <<<QUERY +{ + products(filter: {name: {match: "$configurableProduct"}}) { + items { + id + sku + ... on ConfigurableProduct { + configurable_options { + id + attribute_id + label + position + use_default + attribute_code + values { + value_index + label + store_label + default_label + use_default_value + } + product_id + } + } + } + } +} +QUERY; + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function expectedResultDataProvider() + { + return [ + [ + [ + 'configurable_12345' => + [ + 'sku' => 'configurable_12345', + 'configurable_options' => + [ + [ + 'label' => 'Test Configurable First', + 'position' => 0, + 'use_default' => false, + 'attribute_code' => 'test_configurable_first', + 'values' => + [ + [ + 'label' => 'First Option 3', + 'store_label' => 'First Option 3', + 'default_label' => 'First Option 3', + 'use_default_value' => true, + ], + [ + 'label' => 'First Option 4', + 'store_label' => 'First Option 4', + 'default_label' => 'First Option 4', + 'use_default_value' => true, + ], + ], + ], + [ + 'label' => 'Test Configurable Second', + 'position' => 1, + 'use_default' => false, + 'attribute_code' => 'test_configurable_second', + 'values' => + [ + [ + 'label' => 'Second Option 3', + 'store_label' => 'Second Option 3', + 'default_label' => 'Second Option 3', + 'use_default_value' => true, + ], + [ + 'label' => 'Second Option 4', + 'store_label' => 'Second Option 4', + 'default_label' => 'Second Option 4', + 'use_default_value' => true, + ], + ], + ], + ], + ], + 'configurable' => + [ + 'sku' => 'configurable', + 'configurable_options' => + [ + [ + 'label' => 'Test Configurable First', + 'position' => 0, + 'use_default' => false, + 'attribute_code' => 'test_configurable_first', + 'values' => + [ + [ + 'label' => 'First Option 1', + 'store_label' => 'First Option 1', + 'default_label' => 'First Option 1', + 'use_default_value' => true, + ], + [ + 'label' => 'First Option 2', + 'store_label' => 'First Option 2', + 'default_label' => 'First Option 2', + 'use_default_value' => true, + ], + ], + ], + [ + 'label' => 'Test Configurable Second', + 'position' => 1, + 'use_default' => false, + 'attribute_code' => 'test_configurable_second', + 'values' => + [ + [ + 'label' => 'Second Option 1', + 'store_label' => 'Second Option 1', + 'default_label' => 'Second Option 1', + 'use_default_value' => true, + ], + [ + 'label' => 'Second Option 2', + 'store_label' => 'Second Option 2', + 'default_label' => 'Second Option 2', + 'use_default_value' => true, + ], + ], + ], + ], + ] + ] + ] + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/Options/Uid/CustomizableValueUidTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/Options/Uid/CustomizableValueUidTest.php new file mode 100644 index 0000000000000..070d917b85330 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/Options/Uid/CustomizableValueUidTest.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct\Options\Uid; + +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\ResourceModel\Entity\Attribute; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for downloadable product links uid + */ +class CustomizableValueUidTest extends GraphQlAbstract +{ + /** + * @var Attribute + */ + private $eavAttribute; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->eavAttribute = $objectManager->get(Attribute::class); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_one_simple.php + */ + public function testQueryUidForConfigurableSuperAttributes() + { + $productSku = 'configurable'; + $query = $this->getQuery($productSku); + $response = $this->graphQlQuery($query); + $responseProduct = $response['products']['items'][0]; + self::assertNotEmpty($responseProduct['variants']); + + foreach ($responseProduct['variants'] as $variant) { + self::assertNotEmpty($variant['attributes']); + + foreach ($variant['attributes'] as $attribute) { + $attributeId = (int) $this->eavAttribute->getIdByCode(Product::ENTITY, $attribute['code']); + $uid = $this->getUidByOptionIds($attributeId, $attribute['value_index']); + self::assertEquals($uid, $attribute['uid']); + } + } + } + + /** + * Get Uid + * + * @param int $optionId + * @param int $optionValueId + * + * @return string + */ + private function getUidByOptionIds(int $optionId, int $optionValueId): string + { + return base64_encode('configurable/' . $optionId . '/' . $optionValueId); + } + + /** + * Get query + * + * @param string $sku + * + * @return string + */ + private function getQuery(string $sku): string + { + return <<<QUERY +query { + products(filter: { sku: { eq: "$sku" } }) { + items { + ... on ConfigurableProduct { + variants { + attributes { + uid + code + value_index + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php new file mode 100644 index 0000000000000..b8f59b34fae0c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl; + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\GraphQl\Model\Cors\Configuration; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class CorsHeadersTest extends GraphQlAbstract +{ + /** + * @var Config $config + */ + private $resourceConfig; + /** + * @var ReinitableConfigInterface + */ + private $reinitConfig; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $objectManager = ObjectManager::getInstance(); + + $this->resourceConfig = $objectManager->get(Config::class); + $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0); + $this->reinitConfig->reinit(); + } + + public function testNoCorsHeadersWhenCorsIsDisabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0); + $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_MAX_AGE, '86400'); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertArrayNotHasKey('Access-Control-Max-Age', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + } + + public function testCorsHeadersWhenCorsIsEnabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1); + $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, 'http://magento.local'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400'); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + 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('http://magento.local', $headers['Access-Control-Allow-Origin']); + self::assertEquals('86400', $headers['Access-Control-Max-Age']); + } + + public function testEmptyCorsHeadersWhenCorsIsEnabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, ''); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertArrayNotHasKey('Access-Control-Max-Age', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + } + + private function getHeadersFromIntrospectionQuery(): array + { + $query + = <<<QUERY + query IntrospectionQuery { + __schema { + types { + name + } + } + } +QUERY; + + return $this->graphQlQueryWithResponseHeaders($query)['headers'] ?? []; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php index 3560a6ba48dd5..4f2b8f7566d31 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php @@ -123,7 +123,7 @@ public function testCreateCustomerIfInputDataIsEmpty() mutation { createCustomer( input: { - + } ) { customer { @@ -339,7 +339,9 @@ public function testCreateCustomerSubscribed() public function testCreateCustomerIfCustomerWithProvidedEmailAlreadyExists() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('A customer with the same email address already exists in an associated website.'); + $this->expectExceptionMessage( + 'A customer with the same email address already exists in an associated website.' + ); $existedEmail = 'customer@example.com'; $password = 'test123#'; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2Test.php new file mode 100644 index 0000000000000..10d17d5f7d1b3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2Test.php @@ -0,0 +1,390 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for create customer (V2) + */ +class CreateCustomerV2Test extends GraphQlAbstract +{ + /** + * @var Registry + */ + private $registry; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + protected function setUp(): void + { + parent::setUp(); + + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + } + + /** + * @throws \Exception + */ + public function testCreateCustomerAccountWithPassword() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertNull($response['createCustomerV2']['customer']['id']); + $this->assertEquals($newFirstname, $response['createCustomerV2']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomerV2']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomerV2']['customer']['email']); + $this->assertTrue($response['createCustomerV2']['customer']['is_subscribed']); + } + + /** + * @throws \Exception + */ + public function testCreateCustomerAccountWithoutPassword() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertEquals($newFirstname, $response['createCustomerV2']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomerV2']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomerV2']['customer']['email']); + $this->assertTrue($response['createCustomerV2']['customer']['is_subscribed']); + } + + /** + */ + public function testCreateCustomerIfInputDataIsEmpty() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('CustomerCreateInput.email of required type String! was not provided.'); + $this->expectExceptionMessage('CustomerCreateInput.firstname of required type String! was not provided.'); + $this->expectExceptionMessage('CustomerCreateInput.lastname of required type String! was not provided.'); + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + */ + public function testCreateCustomerIfEmailMissed() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Field CustomerCreateInput.email of required type String! was not provided'); + + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @dataProvider invalidEmailAddressDataProvider + * + * @param string $email + * @throws \Exception + */ + public function testCreateCustomerIfEmailIsNotValid(string $email) + { + $firstname = 'Richard'; + $lastname = 'Rowe'; + $password = 'test123#'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$firstname}" + lastname: "{$lastname}" + email: "{$email}" + password: "{$password}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->expectExceptionMessage('"' . $email . '" is not a valid email address.'); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function invalidEmailAddressDataProvider(): array + { + return [ + ['plainaddress'], + ['jØrgen@somedomain.com'], + ['#@%^%#$@#$@#.com'], + ['@example.com'], + ['Joe Smith <email@example.com>'], + ['email.example.com'], + ['email@example@example.com'], + ['email@example.com (Joe Smith)'], + ['email@example'], + ['“email”@example.com'], + ]; + } + + /** + */ + public function testCreateCustomerIfPassedAttributeDosNotExistsInCustomerInput() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Field "test123" is not defined by type CustomerCreateInput.'); + + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + test123: "123test123" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + */ + public function testCreateCustomerIfNameEmpty() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Required parameters are missing: First Name'); + + $newEmail = 'customer_created' . rand(1, 2000000) . '@example.com'; + $newFirstname = ''; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + email: "{$newEmail}" + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @magentoConfigFixture default_store newsletter/general/active 0 + */ + public function testCreateCustomerSubscribed() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + is_subscribed: true + } + ) { + customer { + email + is_subscribed + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + $this->assertFalse($response['createCustomerV2']['customer']['is_subscribed']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateCustomerIfCustomerWithProvidedEmailAlreadyExists() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'A customer with the same email address already exists in an associated website.' + ); + + $existedEmail = 'customer@example.com'; + $password = 'test123#'; + $firstname = 'John'; + $lastname = 'Smith'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + email: "{$existedEmail}" + password: "{$password}" + firstname: "{$firstname}" + lastname: "{$lastname}" + } + ) { + customer { + firstname + lastname + email + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + protected function tearDown(): void + { + $newEmail = 'new_customer@example.com'; + try { + $customer = $this->customerRepository->get($newEmail); + } catch (\Exception $exception) { + return; + } + + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $this->customerRepository->delete($customer); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerEmailTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerEmailTest.php new file mode 100644 index 0000000000000..ca21aa7d9c45b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerEmailTest.php @@ -0,0 +1,171 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Exception; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\CustomerGraphQl\Model\Customer\UpdateCustomerAccount; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for update customer's email + */ +class UpdateCustomerEmailTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + /** + * @var UpdateCustomerAccount + */ + private $updateCustomerAccount; + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * Setting up tests + */ + protected function setUp(): void + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + $this->updateCustomerAccount = Bootstrap::getObjectManager()->get(UpdateCustomerAccount::class); + $this->storeRepository = Bootstrap::getObjectManager()->get(StoreRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerEmail(): void + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $newEmail = 'newcustomer@example.com'; + + $query = <<<QUERY +mutation { + updateCustomerEmail( + email: "{$newEmail}" + password: "{$currentPassword}" + ) { + customer { + email + } + } +} +QUERY; + + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertEquals($newEmail, $response['updateCustomerEmail']['customer']['email']); + +/* $this->updateCustomerAccount->execute( + $this->customerRepository->get($newEmail), + ['email' => $currentEmail, 'password' => $currentPassword], + $this->storeRepository->getById(1) + );*/ + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerEmailIfPasswordIsWrong(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid login or password.'); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $newEmail = 'newcustomer@example.com'; + $wrongPassword = 'wrongpassword'; + + $query = <<<QUERY +mutation { + updateCustomerEmail( + email: "{$newEmail}" + password: "{$wrongPassword}" + ) { + customer { + email + } + } +} +QUERY; + + $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + */ + public function testUpdateEmailIfEmailAlreadyExists() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage( + 'A customer with the same email address already exists in an associated website.' + ); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $existedEmail = 'customer_two@example.com'; + + $query = <<<QUERY +mutation { + updateCustomerEmail( + email: "{$existedEmail}" + password: "{$currentPassword}" + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * Get customer authorization 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/Customer/UpdateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php index 6e90e85782bb2..8d6bae35de49b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -7,8 +7,9 @@ namespace Magento\GraphQl\Customer; +use Exception; use Magento\Customer\Model\CustomerAuthUpdate; -use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Exception\AuthenticationException; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -113,7 +114,7 @@ public function testUpdateCustomer() */ public function testUpdateCustomerIfInputDataIsEmpty() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('"input" value should be specified'); $currentEmail = 'customer@example.com'; @@ -139,7 +140,7 @@ public function testUpdateCustomerIfInputDataIsEmpty() */ public function testUpdateCustomerIfUserIsNotAuthorized() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('The current customer isn\'t authorized.'); $newFirstname = 'Richard'; @@ -165,7 +166,7 @@ public function testUpdateCustomerIfUserIsNotAuthorized() */ public function testUpdateCustomerIfAccountIsLocked() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('The account is locked.'); $this->lockCustomer->execute(1); @@ -195,7 +196,7 @@ public function testUpdateCustomerIfAccountIsLocked() */ public function testUpdateEmailIfPasswordIsMissed() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('Provide the current "password" to change "email".'); $currentEmail = 'customer@example.com'; @@ -223,7 +224,7 @@ public function testUpdateEmailIfPasswordIsMissed() */ public function testUpdateEmailIfPasswordIsInvalid() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('Invalid login or password.'); $currentEmail = 'customer@example.com'; @@ -253,8 +254,10 @@ public function testUpdateEmailIfPasswordIsInvalid() */ public function testUpdateEmailIfEmailAlreadyExists() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('A customer with the same email address already exists in an associated website.'); + $this->expectException(Exception::class); + $this->expectExceptionMessage( + 'A customer with the same email address already exists in an associated website.' + ); $currentEmail = 'customer@example.com'; $currentPassword = 'password'; @@ -281,12 +284,42 @@ public function testUpdateEmailIfEmailAlreadyExists() $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateEmailIfEmailIsInvalid() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $invalidEmail = 'customer.example.com'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + email: "{$invalidEmail}" + password: "{$currentPassword}" + } + ) { + customer { + email + } + } +} +QUERY; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('"' . $invalidEmail . '" is not a valid email address.'); + + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer.php */ public function testEmptyCustomerName() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('Required parameters are missing: First Name'); $currentEmail = 'customer@example.com'; @@ -310,10 +343,89 @@ public function testEmptyCustomerName() $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testEmptyCustomerLastName() + { + $query = <<<QUERY +mutation { + updateCustomer( + input: { + lastname: "" + } + ) { + customer { + lastname + } + } +} +QUERY; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Required parameters are missing: Last Name'); + + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerWithIncorrectGender() + { + $gender = 5; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('"' . $gender . '" is not a valid gender value.'); + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + gender: {$gender} + } + ) { + customer { + gender + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerIfDobIsInvalid() + { + $invalidDob = 'bla-bla-bla'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + date_of_birth: "{$invalidDob}" + } + ) { + customer { + date_of_birth + } + } +} +QUERY; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid date'); + + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + /** * @param string $email * @param string $password * @return array + * @throws AuthenticationException */ private function getCustomerAuthHeaders(string $email, string $password): array { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerV2Test.php new file mode 100644 index 0000000000000..8b3d1a565add4 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerV2Test.php @@ -0,0 +1,273 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for new update customer endpoint + */ +class UpdateCustomerV2Test extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var LockCustomer + */ + private $lockCustomer; + + protected function setUp(): void + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->lockCustomer = Bootstrap::getObjectManager()->get(LockCustomer::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomer(): void + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $newPrefix = 'Dr'; + $newFirstname = 'Richard'; + $newMiddlename = 'Riley'; + $newLastname = 'Rowe'; + $newSuffix = 'III'; + $newDob = '3/11/1972'; + $newTaxVat = 'GQL1234567'; + $newGender = 2; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + prefix: "{$newPrefix}" + firstname: "{$newFirstname}" + middlename: "{$newMiddlename}" + lastname: "{$newLastname}" + suffix: "{$newSuffix}" + date_of_birth: "{$newDob}" + taxvat: "{$newTaxVat}" + gender: {$newGender} + } + ) { + customer { + prefix + firstname + middlename + lastname + suffix + date_of_birth + taxvat + email + gender + } + } +} +QUERY; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertEquals($newPrefix, $response['updateCustomerV2']['customer']['prefix']); + $this->assertEquals($newFirstname, $response['updateCustomerV2']['customer']['firstname']); + $this->assertEquals($newMiddlename, $response['updateCustomerV2']['customer']['middlename']); + $this->assertEquals($newLastname, $response['updateCustomerV2']['customer']['lastname']); + $this->assertEquals($newSuffix, $response['updateCustomerV2']['customer']['suffix']); + $this->assertEquals($newDob, $response['updateCustomerV2']['customer']['date_of_birth']); + $this->assertEquals($newTaxVat, $response['updateCustomerV2']['customer']['taxvat']); + $this->assertEquals($newGender, $response['updateCustomerV2']['customer']['gender']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerIfInputDataIsEmpty(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('"input" value should be specified'); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + */ + public function testUpdateCustomerIfUserIsNotAuthorized(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('The current customer isn\'t authorized.'); + + $newFirstname = 'Richard'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + firstname: "{$newFirstname}" + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerIfAccountIsLocked(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('The account is locked.'); + + $this->lockCustomer->execute(1); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $newFirstname = 'Richard'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + firstname: "{$newFirstname}" + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testEmptyCustomerName(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Required parameters are missing: First Name'); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + firstname: "" + } + ) { + customer { + email + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testEmptyCustomerLastName(): void + { + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + lastname: "" + } + ) { + customer { + lastname + } + } +} +QUERY; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Required parameters are missing: Last Name'); + + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerIfDobIsInvalid(): void + { + $invalidDob = 'bla-bla-bla'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + date_of_birth: "{$invalidDob}" + } + ) { + customer { + date_of_birth + } + } +} +QUERY; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid date'); + + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + + /** + * @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/DownloadableProduct/Options/Uid/DownloadableLinksValueUidTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/Options/Uid/DownloadableLinksValueUidTest.php new file mode 100644 index 0000000000000..e2daadc5e743d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/Options/Uid/DownloadableLinksValueUidTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\DownloadableProduct\Options\Uid; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for downloadable product links uid + */ +class DownloadableLinksValueUidTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php + */ + public function testQueryUidForDownloadableLinks() + { + $productSku = 'downloadable-product'; + $query = $this->getQuery($productSku); + $response = $this->graphQlQuery($query); + $responseProduct = $response['products']['items'][0]; + + self::assertNotEmpty($responseProduct['downloadable_product_links']); + + foreach ($responseProduct['downloadable_product_links'] as $productLink) { + $uid = $this->getUidByLinkId((int) $productLink['id']); + self::assertEquals($uid, $productLink['uid']); + } + } + + /** + * Get uid by link id + * + * @param int $linkId + * + * @return string + */ + private function getUidByLinkId(int $linkId): string + { + return base64_encode('downloadable/' . $linkId); + } + + /** + * Get query + * + * @param string $sku + * + * @return string + */ + private function getQuery(string $sku): string + { + return <<<QUERY +query { + products(filter: { sku: { eq: "$sku" } }) { + items { + sku + + ... on DownloadableProduct { + downloadable_product_links { + id + uid + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GetCustomerAuthenticationHeader.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GetCustomerAuthenticationHeader.php new file mode 100644 index 0000000000000..8b51d37b50a27 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GetCustomerAuthenticationHeader.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl; + +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; + +/** + * Get authentication header for customer + */ +class GetCustomerAuthenticationHeader +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @param CustomerTokenServiceInterface $customerTokenService + */ + public function __construct(CustomerTokenServiceInterface $customerTokenService) + { + $this->customerTokenService = $customerTokenService; + } + + /** + * Get header to perform customer authenticated request + * + * @param string $email + * @param string $password + * @return string[] + * @throws AuthenticationException + */ + public function execute(string $email = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/GiftMessageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/GiftMessageTest.php new file mode 100644 index 0000000000000..8eaac6d46aa02 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/GiftMessageTest.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\GiftMessage\Cart; + +use Exception; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class GiftMessageTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_order 1 + * @magentoApiDataFixture Magento/GiftMessage/_files/quote_with_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageForCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('message_order_21'); + $response = $this->requestCartAndAssertResult($maskedQuoteId); + self::assertArrayHasKey('gift_message', $response['cart']); + self::assertSame('Mercutio', $response['cart']['gift_message']['to']); + self::assertSame('Romeo', $response['cart']['gift_message']['from']); + self::assertSame('I thought all for the best.', $response['cart']['gift_message']['message']); + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_order 0 + * @magentoApiDataFixture Magento/GiftMessage/_files/quote_with_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageForCartWithNotAllow() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('message_order_21'); + $response = $this->requestCartAndAssertResult($maskedQuoteId); + self::assertArrayHasKey('gift_message', $response['cart']); + self::assertNull($response['cart']['gift_message']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageForCartWithoutMessage() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $response = $this->requestCartAndAssertResult($maskedQuoteId); + self::assertArrayHasKey('gift_message', $response['cart']); + self::assertNull($response['cart']['gift_message']); + } + + /** + * Get Gift Message Assertion + * + * @param string $quoteId + * + * @return array + * @throws Exception + */ + private function requestCartAndAssertResult(string $quoteId) + { + $query = <<<QUERY +{ + cart(cart_id: "$quoteId") { + gift_message { + to + from + message + } + } +} +QUERY; + return $this->graphQlQuery($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php new file mode 100644 index 0000000000000..fa0909d556b3a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\GiftMessage\Cart\Item; + +use Exception; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class GiftMessageTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 0 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageCartForItemNotAllow() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_guest_order_with_gift_message'); + foreach ($this->requestCartResult($maskedQuoteId)['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertNull($item['gift_message']); + } + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageCartForItem() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_guest_order_with_gift_message'); + foreach ($this->requestCartResult($maskedQuoteId)['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertArrayHasKey('to', $item['gift_message']); + self::assertArrayHasKey('from', $item['gift_message']); + self::assertArrayHasKey('message', $item['gift_message']); + } + } + + /** + * @param string $quoteId + * + * @return array|bool|float|int|string + * @throws Exception + */ + private function requestCartResult(string $quoteId) + { + $query = <<<QUERY +{ + cart(cart_id: "$quoteId") { + items { + product { + name + } + ... on SimpleCartItem { + gift_message { + to + from + message + } + } + } + } +} +QUERY; + return $this->graphQlQuery($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Order/GiftMessageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Order/GiftMessageTest.php new file mode 100644 index 0000000000000..538456884df58 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Order/GiftMessageTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\GiftMessage\Order; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class GiftMessageTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp(): void + { + parent::setUp(); + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_order 1 + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GiftMessage/_files/customer/order_with_message.php + * @throws AuthenticationException + * @throws Exception + */ + public function testGiftMessageForOrder() + { + $query = $this->getCustomerOrdersQuery(); + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + foreach ($response['customerOrders']['items'] as $order) { + self::assertArrayHasKey('gift_message', $order); + self::assertArrayHasKey('to', $order['gift_message']); + self::assertArrayHasKey('from', $order['gift_message']); + self::assertArrayHasKey('message', $order['gift_message']); + } + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_order 0 + * @magentoConfigFixture default_store sales/gift_options/allow_items 0 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GiftMessage/_files/customer/order_with_message.php + */ + public function testGiftMessageNotAllowForOrder() + { + $query = $this->getCustomerOrdersQuery(); + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Can\'t load gift message for order'); + $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @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]; + } + + /** + * Get Customer Orders query + * + * @return string + */ + private function getCustomerOrdersQuery() + { + return <<<QUERY +query { + customerOrders { + items { + order_number + gift_message { + to + from + message + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php index e6db0b9e808ef..8cb0a6db972b4 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php @@ -7,12 +7,29 @@ namespace Magento\GraphQl\GroupedProduct; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; +/** + * Class to test GraphQl response with grouped products + */ class GroupedProductViewTest extends GraphQlAbstract { + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + } /** * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped.php @@ -20,17 +37,16 @@ class GroupedProductViewTest extends GraphQlAbstract public function testAllFieldsGroupedProduct() { $productSku = 'grouped-product'; - $query - = <<<QUERY + $query = <<<QUERY { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { id attribute_set_id created_at name sku - type_id + type_id ... on GroupedProduct { items{ qty @@ -39,9 +55,14 @@ public function testAllFieldsGroupedProduct() sku name type_id - url_key + url_key } } + product_links{ + linked_product_sku + position + link_type + } } } } @@ -49,47 +70,77 @@ public function testAllFieldsGroupedProduct() QUERY; $response = $this->graphQlQuery($query); - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $groupedProduct = $productRepository->get($productSku, false, null, true); + $groupedProduct = $this->productRepository->get($productSku, false, null, true); - $this->assertGroupedProductItems($groupedProduct, $response['products']['items'][0]); + $this->assertNotEmpty( + $response['products']['items'][0]['items'], + "Precondition failed: 'Grouped product items' must not be empty" + ); + $this->assertGroupedProductItems($groupedProduct, $response['products']['items'][0]['items']); + $this->assertNotEmpty( + $response['products']['items'][0]['product_links'], + "Precondition failed: 'Linked product items' must not be empty" + ); + $this->assertProductLinks($groupedProduct, $response['products']['items'][0]['product_links']); } - private function assertGroupedProductItems($product, $actualResponse) + /** + * @param ProductInterface $product + * @param array $items + */ + private function assertGroupedProductItems(ProductInterface $product, array $items): void { - $this->assertNotEmpty( - $actualResponse['items'], - "Precondition failed: 'grouped product items' must not be empty" - ); - $this->assertCount(2, $actualResponse['items']); + $this->assertCount(2, $items); $groupedProductLinks = $product->getProductLinks(); - foreach ($actualResponse['items'] as $itemIndex => $bundleItems) { - $this->assertNotEmpty($bundleItems); + foreach ($items as $itemIndex => $bundleItem) { + $this->assertNotEmpty($bundleItem); $associatedProductSku = $groupedProductLinks[$itemIndex]->getLinkedProductSku(); - - $productsRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $associatedProduct */ - $associatedProduct = $productsRepository->get($associatedProductSku); + $associatedProduct = $this->productRepository->get($associatedProductSku); $this->assertEquals( $groupedProductLinks[$itemIndex]->getExtensionAttributes()->getQty(), - $actualResponse['items'][$itemIndex]['qty'] + $bundleItem['qty'] ); $this->assertEquals( $groupedProductLinks[$itemIndex]->getPosition(), - $actualResponse['items'][$itemIndex]['position'] + $bundleItem['position'] ); $this->assertResponseFields( - $actualResponse['items'][$itemIndex]['product'], + $bundleItem['product'], [ - 'sku' => $associatedProductSku, - 'type_id' => $groupedProductLinks[$itemIndex]->getLinkedProductType(), - 'url_key'=> $associatedProduct->getUrlKey(), - 'name' => $associatedProduct->getName() + 'sku' => $associatedProductSku, + 'type_id' => $groupedProductLinks[$itemIndex]->getLinkedProductType(), + 'url_key'=> $associatedProduct->getUrlKey(), + 'name' => $associatedProduct->getName() ] ); } } + + /** + * @param ProductInterface $product + * @param array $links + * @return void + */ + private function assertProductLinks(ProductInterface $product, array $links): void + { + $this->assertCount(2, $links); + $productLinks = $product->getProductLinks(); + foreach ($links as $itemIndex => $linkedItem) { + $this->assertNotEmpty($linkedItem); + $this->assertEquals( + $productLinks[$itemIndex]->getPosition(), + $linkedItem['position'] + ); + $this->assertEquals( + $productLinks[$itemIndex]->getLinkedProductSku(), + $linkedItem['linked_product_sku'] + ); + $this->assertEquals( + $productLinks[$itemIndex]->getLinkType(), + $linkedItem['link_type'] + ); + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php new file mode 100644 index 0000000000000..9eea2396c24ce --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\GroupedProduct; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductViewTest extends GraphQlAbstract +{ + + /** + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_in_multiple_websites.php + */ + public function testGroupedProductAssignedToOneWebsite() + { + $headerMapFirstStore['Store'] = 'default'; + $headerMapSecondStore['Store'] = 'fixture_second_store'; + $productSku = 'grouped-product'; + $query = $this->getQuery($productSku); + $responseForFirstWebsite = $this->graphQlQuery($query, [], '', $headerMapFirstStore); + $responseForSecondWebsite = $this->graphQlQuery($query, [], '', $headerMapSecondStore); + self::assertEmpty($responseForFirstWebsite['products']['items']); + $groupedProductLinks = [ + [ + 'qty' => 1, + 'position' => 1, + 'product' => [ + 'sku' => 'simple', + 'name' => 'Simple Product', + 'type_id' => 'simple', + 'url_key' => 'simple-product' + ] + ], + [ + 'qty' => 2, + 'position' => 2, + 'product' => [ + 'sku' => 'virtual-product', + 'name' => 'Virtual Product', + 'type_id' => 'virtual', + 'url_key' => 'virtual-product' + ] + ] + ]; + $this->assertGroupedProductItems($groupedProductLinks, $responseForSecondWebsite['products']['items'][0]); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_items_in_multiple_websites.php + */ + public function testGroupedProductItemsAssignedToDifferentWebsites() + { + $headerMapFirstStore['Store'] = 'default'; + $headerMapSecondStore['Store'] = 'fixture_second_store'; + $productSku = 'grouped-product'; + $query = $this->getQuery($productSku); + $responseForFirstWebsite = $this->graphQlQuery($query, [], '', $headerMapFirstStore); + $responseForSecondWebsite = $this->graphQlQuery($query, [], '', $headerMapSecondStore); + $firstWebsiteGroupedProductLinks = [ + [ + 'qty' => 1, + 'position' => 1, + 'product' => [ + 'sku' => 'simple', + 'name' => 'Simple Product', + 'type_id' => 'simple', + 'url_key' => 'simple-product' + ] + ] + ]; + $secondWebsiteGroupedProductLinks = [ + [ + 'qty' => 2, + 'position' => 2, + 'product' => [ + 'sku' => 'virtual-product', + 'name' => 'Virtual Product', + 'type_id' => 'virtual', + 'url_key' => 'virtual-product' + ] + ] + ]; + + $this->assertGroupedProductItems( + $firstWebsiteGroupedProductLinks, + $responseForFirstWebsite['products']['items'][0] + ); + $this->assertGroupedProductItems( + $secondWebsiteGroupedProductLinks, + $responseForSecondWebsite['products']['items'][0] + ); + } + + /** + * @param string $sku + * @return string + */ + private function getQuery(string $sku): string + { + return <<<QUERY +{ + products(filter: {sku: {eq: "{$sku}"}}) { + items { + id + attribute_set_id + created_at + name + sku + type_id + ... on GroupedProduct { + items{ + qty + position + product{ + sku + name + type_id + url_key + } + } + } + } + } +} +QUERY; + } + + /** + * @param array $groupedProductLinks + * @param $actualResponse + */ + private function assertGroupedProductItems(array $groupedProductLinks, $actualResponse) + { + self::assertNotEmpty( + $actualResponse['items'], + "Precondition failed: 'grouped product items' must not be empty" + ); + self::assertCount(count($groupedProductLinks), $actualResponse['items']); + foreach ($actualResponse['items'] as $itemIndex => $bundleItems) { + self::assertEquals($groupedProductLinks[$itemIndex], $bundleItems); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Customer/SubscribeEmailToNewsletterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Customer/SubscribeEmailToNewsletterTest.php new file mode 100644 index 0000000000000..ec0e49cc55153 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Customer/SubscribeEmailToNewsletterTest.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Newsletter\Customer; + +use Exception; +use Magento\Customer\Model\CustomerAuthUpdate; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Newsletter\Model\ResourceModel\Subscriber as SubscriberResourceModel; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test newsletter email subscription for customer + */ +class SubscribeEmailToNewsletterTest extends GraphQlAbstract +{ + /** + * @var CustomerAuthUpdate + */ + private $customerAuthUpdate; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var SubscriberResourceModel + */ + private $subscriberResource; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerAuthUpdate = Bootstrap::getObjectManager()->get(CustomerAuthUpdate::class); + $this->customerRegistry = Bootstrap::getObjectManager()->get(CustomerRegistry::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->subscriberResource = $objectManager->get(SubscriberResourceModel::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testAddRegisteredCustomerEmailIntoNewsletterSubscription() + { + $query = $this->getQuery('customer@example.com'); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('subscribeEmailToNewsletter', $response); + self::assertNotEmpty($response['subscribeEmailToNewsletter']); + self::assertEquals('SUBSCRIBED', $response['subscribeEmailToNewsletter']['status']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testAddLockedCustomerEmailIntoNewsletterSubscription() + { + /* lock customer */ + $customerSecure = $this->customerRegistry->retrieveSecureData(1); + $customerSecure->setLockExpires('2030-12-31 00:00:00'); + $this->customerAuthUpdate->saveAuth(1); + + $query = $this->getQuery('customer@example.com'); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('subscribeEmailToNewsletter', $response); + self::assertNotEmpty($response['subscribeEmailToNewsletter']); + self::assertEquals('SUBSCRIBED', $response['subscribeEmailToNewsletter']['status']); + } + + /** + * @magentoConfigFixture default_store newsletter/subscription/confirm 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testSubscribeRegisteredCustomerEmailWithEnabledConfirmation() + { + $query = $this->getQuery('customer@example.com'); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('subscribeEmailToNewsletter', $response); + self::assertNotEmpty($response['subscribeEmailToNewsletter']); + self::assertEquals('NOT_ACTIVE', $response['subscribeEmailToNewsletter']['status']); + } + + /** + * @magentoConfigFixture default_store customer/create_account/confirm 1 + * @magentoApiDataFixture Magento/Customer/_files/unconfirmed_customer.php + * @expectedException Exception + * @expectedExceptionMessage The account sign-in was incorrect or your account is disabled temporarily. + * Please wait and try again later + */ + public function testNewsletterSubscriptionWithUnconfirmedCustomer() + { + $headers = $this->getHeaderMap('unconfirmedcustomer@example.com', 'Qwert12345'); + $query = $this->getQuery('unconfirmedcustomer@example.com'); + + $this->graphQlMutation($query, [], '', $headers); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testNewsletterSubscriptionWithIncorrectEmailFormat() + { + $query = $this->getQuery('customer.example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Enter a valid email address.' . "\n"); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Newsletter/_files/subscribers.php + */ + public function testNewsletterSubscriptionWithAlreadySubscribedEmail() + { + $query = $this->getQuery('customer@example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('This email address is already subscribed.' . "\n"); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Newsletter/_files/three_subscribers.php + */ + public function testNewsletterSubscriptionWithAnotherCustomerEmail() + { + $query = $this->getQuery('customer2@search.example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Cannot create a newsletter subscription.' . "\n"); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer@search.example.com')); + } + + /** + * Returns a mutation query + * + * @param string $email + * @return string + */ + private function getQuery(string $email = ''): string + { + return <<<QUERY +mutation { + subscribeEmailToNewsletter( + email: "$email" + ) { + status + } +} +QUERY; + } + + /** + * Retrieve customer authorization headers + * + * @param string $username + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return [ + 'Authorization' => 'Bearer ' . $customerToken + ]; + } + + /** + * @inheritDoc + */ + public function tearDown(): void + { + $this->subscriberResource + ->getConnection() + ->delete( + $this->subscriberResource->getMainTable() + ); + + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Guest/SubscribeEmailToNewsletterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Guest/SubscribeEmailToNewsletterTest.php new file mode 100644 index 0000000000000..f0a933609c762 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Guest/SubscribeEmailToNewsletterTest.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Newsletter\Guest; + +use Exception; +use Magento\Newsletter\Model\ResourceModel\Subscriber as SubscriberResourceModel; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test newsletter email subscription for guest + */ +class SubscribeEmailToNewsletterTest extends GraphQlAbstract +{ + /** + * @var SubscriberResourceModel + */ + private $subscriberResource; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->subscriberResource = $objectManager->get(SubscriberResourceModel::class); + } + + public function testAddEmailIntoNewsletterSubscription() + { + $query = $this->getQuery('guest@example.com'); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('subscribeEmailToNewsletter', $response); + self::assertNotEmpty($response['subscribeEmailToNewsletter']); + self::assertEquals('SUBSCRIBED', $response['subscribeEmailToNewsletter']['status']); + } + + public function testNewsletterSubscriptionWithIncorrectEmailFormat() + { + $query = $this->getQuery('guest.example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Enter a valid email address.' . "\n"); + + $this->graphQlMutation($query); + } + + /** + * @magentoConfigFixture default_store newsletter/subscription/allow_guest_subscribe 0 + */ + public function testNewsletterSubscriptionWithDisallowedGuestSubscription() + { + $query = $this->getQuery('guest@example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage( + 'Guests can not subscribe to the newsletter. You must create an account to subscribe.' . "\n" + ); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Newsletter/_files/guest_subscriber.php + */ + public function testNewsletterSubscriptionWithAlreadySubscribedEmail() + { + $query = $this->getQuery('guest@example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('This email address is already subscribed.' . "\n"); + + $this->graphQlMutation($query); + } + + /** + * Returns a mutation query + * + * @param string $email + * @return string + */ + private function getQuery(string $email = ''): string + { + return <<<QUERY +mutation { + subscribeEmailToNewsletter( + email: "$email" + ) { + status + } +} +QUERY; + } + + /** + * @inheritDoc + */ + public function tearDown(): void + { + $this->subscriberResource + ->getConnection() + ->delete( + $this->subscriberResource->getMainTable() + ); + + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductToCartSingleMutationTest.php new file mode 100644 index 0000000000000..956316c1fa0fa --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductToCartSingleMutationTest.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManager\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test cases for adding downloadable product with custom options to cart using the single add to cart mutation. + */ +class AddDownloadableProductToCartSingleMutationTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var GetCustomOptionsWithUIDForQueryBySku + */ + private $getCustomOptionsWithIDV2ForQueryBySku; + + /** + * @var GetCartItemOptionsFromUID + */ + private $getCartItemOptionsFromUID; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $this->objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getCartItemOptionsFromUID = $this->objectManager->get(GetCartItemOptionsFromUID::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = + $this->objectManager->get(GetCustomOptionsWithUIDForQueryBySku::class); + } + + /** + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddDownloadableProductWithOptions() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $sku = 'downloadable-product-with-purchased-separately-links'; + $qty = 1; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $decodedItemOptions = $this->getCartItemOptionsFromUID->execute($itemOptions); + + /* The type field is only required for assertions, it should not be present in query */ + foreach ($itemOptions['entered_options'] as &$enteredOption) { + if (isset($enteredOption['type'])) { + unset($enteredOption['type']); + } + } + + /* Add downloadable product link data to the "selected_options" */ + $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); + + $productOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ); + + $query = $this->getQuery($maskedQuoteId, $qty, $sku, trim($productOptionsQuery, '{}')); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('items', $response['addProductsToCart']['cart']); + self::assertCount($qty, $response['addProductsToCart']['cart']); + self::assertEquals($linkId, $response['addProductsToCart']['cart']['items'][0]['links'][0]['id']); + + $customizableOptionsOutput = + $response['addProductsToCart']['cart']['items'][0]['customizable_options']; + + foreach ($customizableOptionsOutput as $customizableOptionOutput) { + $customizableOptionOutputValues = []; + foreach ($customizableOptionOutput['values'] as $customizableOptionOutputValue) { + $customizableOptionOutputValues[] = $customizableOptionOutputValue['value']; + } + if (count($customizableOptionOutputValues) === 1) { + $customizableOptionOutputValues = $customizableOptionOutputValues[0]; + } + + self::assertEquals( + $decodedItemOptions[$customizableOptionOutput['id']], + $customizableOptionOutputValues + ); + } + } + + /** + * Function returns array of all product's links + * + * @param string $sku + * @return array + */ + private function getProductsLinks(string $sku) : array + { + $result = []; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + + $product = $productRepository->get($sku, false, null, true); + + foreach ($product->getDownloadableLinks() as $linkObject) { + $result[$linkObject->getLinkId()] = [ + 'title' => $linkObject->getTitle(), + 'link_type' => null, //deprecated field + 'price' => $linkObject->getPrice(), + ]; + } + + return $result; + } + + /** + * Generates UID for downloadable links + * + * @param int $linkId + * @return string + */ + private function generateProductLinkSelectedOptions(int $linkId): string + { + return base64_encode("downloadable/$linkId"); + } + + /** + * Returns GraphQl query string + * + * @param string $maskedQuoteId + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * @return string + */ + private function getQuery( + string $maskedQuoteId, + int $qty, + string $sku, + string $customizableOptions + ): string { + return <<<MUTATION +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] + ) { + cart { + items { + quantity + ... on DownloadableCartItem { + links { + id + } + customizable_options { + label + id + values { + value + } + } + } + } + }, + user_errors { + message + } + } +} +MUTATION; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartEndToEndTest.php new file mode 100644 index 0000000000000..d8f7aedfdd583 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartEndToEndTest.php @@ -0,0 +1,260 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Get customizable options of simple product via the corresponding GraphQl query and add the product + * with customizable options to the shopping cart + */ +class AddSimpleProductToCartEndToEndTest extends GraphQlAbstract +{ + /** + * @var GetCustomOptionsWithUIDForQueryBySku + */ + private $getCustomOptionsWithIDV2ForQueryBySku; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetCartItemOptionsFromUID + */ + private $getCartItemOptionsFromUID; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getCartItemOptionsFromUID = $objectManager->get(GetCartItemOptionsFromUID::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = $objectManager->get( + GetCustomOptionsWithUIDForQueryBySku::class + ); + } + + /** + * Test adding a simple product to the shopping cart with all supported + * customizable options assigned + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithOptions() + { + $sku = 'simple'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $qty = 1; + + $productOptionsData = $this->getProductOptionsViaQuery($sku); + + $itemOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($productOptionsData['received_options']) + ); + + $query = $this->getAddToCartMutation($maskedQuoteId, $qty, $sku, trim($itemOptionsQuery, '{}')); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('customizable_options', $response['addProductsToCart']['cart']['items'][0]); + + foreach ($response['addProductsToCart']['cart']['items'][0]['customizable_options'] as $option) { + self::assertEquals($productOptionsData['expected_options'][$option['id']], $option['values'][0]['value']); + } + } + + /** + * Get product data with customizable options using GraphQl query + * + * @param string $sku + * @return array + * @throws \Exception + */ + private function getProductOptionsViaQuery(string $sku): array + { + $query = $this->getProductQuery($sku); + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('options', $response['products']['items'][0]); + + $expectedItemOptions = []; + $receivedItemOptions = [ + 'entered_options' => [], + 'selected_options' => [] + ]; + + foreach ($response['products']['items'][0]['options'] as $option) { + if (isset($option['entered_option'])) { + /* The date normalization is required since the attribute might value is formatted by the system */ + if ($option['title'] === 'date option') { + $value = '2012-12-12 00:00:00'; + $expectedItemOptions[$option['option_id']] = date('M d, Y', strtotime($value)); + } else { + $value = 'test'; + $expectedItemOptions[$option['option_id']] = $value; + } + $value = $option['title'] === 'date option' ? '2012-12-12 00:00:00' : 'test'; + + $receivedItemOptions['entered_options'][] = [ + 'uid' => $option['entered_option']['uid'], + 'value' => $value + ]; + + } elseif (isset($option['selected_option'])) { + $receivedItemOptions['selected_options'][] = reset($option['selected_option'])['uid']; + $expectedItemOptions[$option['option_id']] = reset($option['selected_option'])['option_type_id']; + } + } + + return [ + 'expected_options' => $expectedItemOptions, + 'received_options' => $receivedItemOptions + ]; + } + + /** + * Returns GraphQL query for retrieving a product with customizable options + * + * @param string $sku + * @return string + */ + private function getProductQuery(string $sku): string + { + return <<<QUERY +query { + products(search: "$sku") { + items { + sku + + ... on CustomizableProductInterface { + options { + option_id + title + + ... on CustomizableRadioOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableDropDownOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableMultipleOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableCheckboxOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableAreaOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableFieldOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableFileOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableDateOption { + option_id + entered_option: value { + uid + } + } + } + } + } + } +} +QUERY; + } + + /** + * Returns GraphQl mutation for adding item to cart + * + * @param string $maskedQuoteId + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * @return string + */ + private function getAddToCartMutation( + string $maskedQuoteId, + int $qty, + string $sku, + string $customizableOptions + ): string { + return <<<MUTATION +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] + ) { + cart { + items { + quantity + ... on SimpleCartItem { + customizable_options { + label + id + values { + value + } + } + } + } + }, + user_errors { + message + } + } +} +MUTATION; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php new file mode 100644 index 0000000000000..4e50f6ff3a2ca --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php @@ -0,0 +1,259 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add simple product with custom options to cart using the unified mutation for adding different product types + */ +class AddSimpleProductToCartSingleMutationTest extends GraphQlAbstract +{ + /** + * @var GetCustomOptionsWithUIDForQueryBySku + */ + private $getCustomOptionsWithIDV2ForQueryBySku; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetCartItemOptionsFromUID + */ + private $getCartItemOptionsFromUID; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getCartItemOptionsFromUID = $objectManager->get(GetCartItemOptionsFromUID::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = $objectManager->get( + GetCustomOptionsWithUIDForQueryBySku::class + ); + } + + /** + * Test adding a simple product to the shopping cart with all supported + * customizable options assigned + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithOptions() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $sku = 'simple'; + $qty = 1; + + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $decodedItemOptions = $this->getCartItemOptionsFromUID->execute($itemOptions); + + /* The type field is only required for assertions, it should not be present in query */ + foreach ($itemOptions['entered_options'] as &$enteredOption) { + if (isset($enteredOption['type'])) { + unset($enteredOption['type']); + } + } + + $productOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ); + + $query = $this->getAddToCartMutation($maskedQuoteId, $qty, $sku, trim($productOptionsQuery, '{}')); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('items', $response['addProductsToCart']['cart']); + self::assertCount($qty, $response['addProductsToCart']['cart']['items']); + $customizableOptionsOutput = + $response['addProductsToCart']['cart']['items'][0]['customizable_options']; + + foreach ($customizableOptionsOutput as $customizableOptionOutput) { + $customizableOptionOutputValues = []; + foreach ($customizableOptionOutput['values'] as $customizableOptionOutputValue) { + $customizableOptionOutputValues[] = $customizableOptionOutputValue['value']; + } + if (count($customizableOptionOutputValues) === 1) { + $customizableOptionOutputValues = $customizableOptionOutputValues[0]; + } + + self::assertEquals( + $decodedItemOptions[$customizableOptionOutput['id']], + $customizableOptionOutputValues + ); + } + } + + /** + * @param string $sku + * @param string $message + * + * @dataProvider wrongSkuDataProvider + * + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddProductWithWrongSku(string $sku, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getAddToCartMutation($maskedQuoteId, 1, $sku, ''); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('user_errors', $response['addProductsToCart']); + self::assertCount(1, $response['addProductsToCart']['user_errors']); + self::assertEquals( + $message, + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * The test covers the case when upon adding available_qty + 1 to the shopping cart, the cart is being + * cleared + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_without_custom_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddToCartWithQtyPlusOne() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $sku = 'simple-2'; + + $query = $this->getAddToCartMutation($maskedQuoteId, 100, $sku, ''); + $response = $this->graphQlMutation($query); + + self::assertEquals(100, $response['addProductsToCart']['cart']['total_quantity']); + + $query = $this->getAddToCartMutation($maskedQuoteId, 1, $sku, ''); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('user_errors', $response['addProductsToCart']); + self::assertEquals( + 'The requested qty is not available', + $response['addProductsToCart']['user_errors'][0]['message'] + ); + self::assertEquals(100, $response['addProductsToCart']['cart']['total_quantity']); + } + + /** + * @param int $quantity + * @param string $message + * + * @dataProvider wrongQuantityDataProvider + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_without_custom_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddProductWithWrongQuantity(int $quantity, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $sku = 'simple-2'; + + $query = $this->getAddToCartMutation($maskedQuoteId, $quantity, $sku, ''); + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('user_errors', $response['addProductsToCart']); + self::assertCount(1, $response['addProductsToCart']['user_errors']); + + self::assertEquals( + $message, + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * @return array + */ + public function wrongSkuDataProvider(): array + { + return [ + 'Non-existent SKU' => [ + 'non-existent', + 'Could not find a product with SKU "non-existent"' + ], + 'Empty SKU' => [ + '', + 'Could not find a product with SKU ""' + ] + ]; + } + + /** + * @return array + */ + public function wrongQuantityDataProvider(): array + { + return [ + 'More quantity than in stock' => [ + 101, + 'The requested qty is not available' + ], + 'Quantity equals zero' => [ + 0, + 'The product quantity should be greater than 0' + ] + ]; + } + + /** + * Returns GraphQl query string + * + * @param string $maskedQuoteId + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * @return string + */ + private function getAddToCartMutation( + string $maskedQuoteId, + int $qty, + string $sku, + string $customizableOptions + ): string { + return <<<MUTATION +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] + ) { + cart { + total_quantity + items { + quantity + ... on SimpleCartItem { + customizable_options { + label + id + values { + value + } + } + } + } + }, + user_errors { + message + } + } +} +MUTATION; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php index 21a8d6ae94312..ff8d4f4280c10 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php @@ -281,6 +281,50 @@ public function testSetDisabledPaymentOnCart() $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWitMissingCartId() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = ""; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"cart_id\" is missing" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithMissingPaymentMethod() + { + $methodCode = ""; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"code\" for \"payment_method\" is missing." + ); + $this->graphQlMutation($query); + } + /** * @param string $maskedQuoteId * @param string $methodCode diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php index 3e06b89c77fb7..08554cbd8fac1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php @@ -149,7 +149,7 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() public function testSetNewShippingAddressOnCartWithVirtualProduct() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The Cart includes virtual product(s) only, so a shipping address is not used.'); + $this->expectExceptionMessage('Shipping address is not allowed on cart: cart contains no items for shipment.'); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); @@ -1745,6 +1745,57 @@ public function testSetNewShippingAddressWithDefaultValueOfSaveInAddressBookAndP } } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetShippingAddressOnCartWithNullCustomerAddressId() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: null + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + label + code + } + region { + code + label + } + __typename + } + } + } +} +QUERY; + $this->expectExceptionMessage( + 'The shipping address must contain either "customer_address_id" or "address".' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + /** * Verify the all the whitelisted fields for a New Address Object * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartItemOptionsFromUID.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartItemOptionsFromUID.php new file mode 100644 index 0000000000000..44b44e0ccac05 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartItemOptionsFromUID.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +/** + * Extracts cart item options from UID + */ +class GetCartItemOptionsFromUID +{ + /** + * Gets an array of encoded item options with UID, extracts and decodes the values + * + * @param array $encodedCustomOptions + * @return array + */ + public function execute(array $encodedCustomOptions): array + { + $customOptions = []; + + foreach ($encodedCustomOptions['selected_options'] as $selectedOption) { + [$optionType, $optionId, $optionValueId] = explode('/', base64_decode($selectedOption)); + if ($optionType == 'custom-option') { + if (isset($customOptions[$optionId])) { + $customOptions[$optionId] = [$customOptions[$optionId], $optionValueId]; + } else { + $customOptions[$optionId] = $optionValueId; + } + } + } + + foreach ($encodedCustomOptions['entered_options'] as $enteredOption) { + /* The date normalization is required since the attribute might value is formatted by the system */ + if ($enteredOption['type'] === 'date') { + $enteredOption['value'] = date('M d, Y', strtotime($enteredOption['value'])); + } + [$optionType, $optionId] = explode('/', base64_decode($enteredOption['uid'])); + if ($optionType == 'custom-option') { + $customOptions[$optionId] = $enteredOption['value']; + } + } + + return $customOptions; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCustomOptionsWithUIDForQueryBySku.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCustomOptionsWithUIDForQueryBySku.php new file mode 100644 index 0000000000000..870617555e8b2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCustomOptionsWithUIDForQueryBySku.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; + +/** + * Generate an array with test values for customizable options with UID + */ +class GetCustomOptionsWithUIDForQueryBySku +{ + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $productCustomOptionRepository; + + /** + * @param ProductCustomOptionRepositoryInterface $productCustomOptionRepository + */ + public function __construct(ProductCustomOptionRepositoryInterface $productCustomOptionRepository) + { + $this->productCustomOptionRepository = $productCustomOptionRepository; + } + + /** + * Returns array of custom options for the product + * + * @param string $sku + * @return array + */ + public function execute(string $sku): array + { + $customOptions = $this->productCustomOptionRepository->getList($sku); + $selectedOptions = []; + $enteredOptions = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + + switch ($optionType) { + case 'field': + case 'area': + $enteredOptions[] = [ + 'type' => 'field', + 'uid' => $this->encodeEnteredOption((int) $customOption->getOptionId()), + 'value' => 'test' + ]; + break; + case 'date': + $enteredOptions[] = [ + 'type' => 'date', + 'uid' => $this->encodeEnteredOption((int) $customOption->getOptionId()), + 'value' => '2012-12-12 00:00:00' + ]; + break; + case 'drop_down': + $optionSelectValues = $customOption->getValues(); + $selectedOptions[] = $this->encodeSelectedOption( + (int) $customOption->getOptionId(), + (int) reset($optionSelectValues)->getOptionTypeId() + ); + break; + case 'multiple': + foreach ($customOption->getValues() as $optionValue) { + $selectedOptions[] = $this->encodeSelectedOption( + (int) $customOption->getOptionId(), + (int) $optionValue->getOptionTypeId() + ); + } + break; + } + } + + return [ + 'selected_options' => $selectedOptions, + 'entered_options' => $enteredOptions + ]; + } + + /** + * Returns UID of the selected custom option + * + * @param int $optionId + * @param int $optionValueId + * @return string + */ + private function encodeSelectedOption(int $optionId, int $optionValueId): string + { + return base64_encode("custom-option/$optionId/$optionValueId"); + } + + /** + * Returns UID of the entered custom option + * + * @param int $optionId + * @return string + */ + private function encodeEnteredOption(int $optionId): string + { + return base64_encode("custom-option/$optionId"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php index be183fe93815a..67012e75bf272 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php @@ -39,6 +39,11 @@ class CreateEmptyCartTest extends GraphQlAbstract */ private $quoteIdMaskFactory; + /** + * @var string + */ + private $maskedQuoteId; + protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); @@ -61,6 +66,7 @@ public function testCreateEmptyCart() self::assertNotNull($guestCart->getId()); self::assertNull($guestCart->getCustomer()->getId()); self::assertEquals('default', $guestCart->getStore()->getCode()); + self::assertEquals('1', $guestCart->getCustomerIsGuest()); } /** @@ -81,6 +87,7 @@ public function testCreateEmptyCartWithNotDefaultStore() self::assertNotNull($guestCart->getId()); self::assertNull($guestCart->getCustomer()->getId()); self::assertSame('fixture_second_store', $guestCart->getStore()->getCode()); + self::assertEquals('1', $guestCart->getCustomerIsGuest()); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php index dbc10700794fa..78691d8cbd889 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php @@ -218,6 +218,50 @@ public function testSetDisabledPaymentOnCart() $this->graphQlMutation($query); } + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWitMissingCartId() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = ""; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"cart_id\" is missing" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithMissingPaymentMethod() + { + $methodCode = ""; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"code\" for \"payment_method\" is missing." + ); + $this->graphQlMutation($query); + } + /** * @param string $maskedQuoteId * @param string $methodCode diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php index b4136d06bf67c..b7ddd085f932e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php @@ -97,7 +97,7 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() public function testSetNewShippingAddressOnCartWithVirtualProduct() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The Cart includes virtual product(s) only, so a shipping address is not used.'); + $this->expectExceptionMessage('Shipping address is not allowed on cart: cart contains no items for shipment.'); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php index a17bc1aa3821a..0a22f3ca9721c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php @@ -7,8 +7,9 @@ namespace Magento\GraphQl\Quote\Guest; -use Magento\Quote\Model\QuoteFactory; +use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; @@ -273,6 +274,81 @@ private function getCartQuery(string $maskedQuoteId) } } } +QUERY; + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 0 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws Exception + */ + public function testUpdateGiftMessageCartForItemNotAllow() + { + $query = $this->getUpdateGiftMessageQuery(); + foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) { + self::assertNull($item['gift_message']); + } + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws Exception + */ + public function testUpdateGiftMessageCartForItem() + { + $query = $this->getUpdateGiftMessageQuery(); + foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertSame('Alex', $item['gift_message']['to']); + self::assertSame('Mike', $item['gift_message']['from']); + self::assertSame('Best regards.', $item['gift_message']['message']); + } + } + + private function getUpdateGiftMessageQuery() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_guest_order_with_gift_message', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $itemId = (int)$quote->getItemByProduct($this->productRepository->get('simple'))->getId(); + + return <<<QUERY +mutation { + updateCartItems( + input: { + cart_id: "$maskedQuoteId", + cart_items: [ + { + cart_item_id: $itemId + quantity: 3 + gift_message: { + to: "Alex" + from: "Mike" + message: "Best regards." + } + } + ] + } + ) { + cart { + items { + id + product { + name + } + quantity + ... on SimpleCartItem { + gift_message { + to + from + message + } + } + } + } + } +} QUERY; } } 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/Review/CreateProductReviewsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/CreateProductReviewsTest.php new file mode 100644 index 0000000000000..f9df1dac5df34 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/CreateProductReviewsTest.php @@ -0,0 +1,209 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Review; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Registry; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Review\Model\ResourceModel\Review\Collection; +use Magento\Review\Model\ResourceModel\Review\CollectionFactory as ReviewCollectionFactory; +use Magento\Review\Model\Review; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for adding product reviews mutation + */ +class CreateProductReviewsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var ReviewCollectionFactory + */ + private $reviewCollectionFactory; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->customerRepository = $objectManager->get(CustomerRepositoryInterface::class); + $this->reviewCollectionFactory = $objectManager->get(ReviewCollectionFactory::class); + $this->registry = $objectManager->get(Registry::class); + } + + /** + * Test adding a product review as guest and logged in customer + * + * @param string $customerName + * @param bool $isGuest + * + * @magentoApiDataFixture Magento/Review/_files/set_position_and_add_store_to_all_ratings.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @dataProvider customerDataProvider + */ + public function testCustomerAddProductReviews(string $customerName, bool $isGuest) + { + $productSku = 'simple_product'; + $query = $this->getQuery($productSku, $customerName); + $headers = []; + + if (!$isGuest) { + $headers = $this->getHeaderMap(); + } + + $response = $this->graphQlMutation($query, [], '', $headers); + + $expectedResult = [ + 'nickname' => $customerName, + 'summary' => 'Summary Test', + 'text' => 'Text Test', + 'average_rating' => 66.67, + 'ratings_breakdown' => [ + [ + 'name' => 'Price', + 'value' => 3 + ], [ + 'name' => 'Quality', + 'value' => 2 + ], [ + 'name' => 'Value', + 'value' => 5 + ] + ] + ]; + self::assertArrayHasKey('createProductReview', $response); + self::assertArrayHasKey('review', $response['createProductReview']); + self::assertEquals($expectedResult, $response['createProductReview']['review']); + } + + /** + * @magentoConfigFixture default_store catalog/review/allow_guest 0 + */ + public function testAddProductReviewGuestIsNotAllowed() + { + $productSku = 'simple_product'; + $customerName = 'John Doe'; + $query = $this->getQuery($productSku, $customerName); + self::expectExceptionMessage('Guest customers aren\'t allowed to add product reviews.'); + $this->graphQlMutation($query); + } + + /** + * Removing the recently added product reviews + */ + public function tearDown(): void + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $productId = 1; + /** @var Collection $reviewsCollection */ + $reviewsCollection = $this->reviewCollectionFactory->create(); + $reviewsCollection->addEntityFilter(Review::ENTITY_PRODUCT_CODE, $productId); + /** @var Review $review */ + foreach ($reviewsCollection as $review) { + $review->delete(); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } + + /** + * @return array + */ + public function customerDataProvider(): array + { + return [ + 'Guest Customer' => ['John Doe', true], + 'Logged In Customer' => ['John', false], + ]; + } + + /** + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Get mutation query + * + * @param string $sku + * @param string $customerName + * + * @return string + */ + private function getQuery(string $sku, string $customerName): string + { + return <<<QUERY +mutation { + createProductReview( + input: { + sku: "$sku", + nickname: "$customerName", + summary: "Summary Test", + text: "Text Test", + ratings: [ + { + id: "Mw==", + value_id: "MTM=" + }, { + id: "MQ==", + value_id: "Mg==" + }, { + id: "Mg==", + value_id: "MTA=" + } + ] + } +) { + review { + nickname + summary + text + average_rating + ratings_breakdown { + name + value + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/GetProductReviewsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/GetProductReviewsTest.php new file mode 100644 index 0000000000000..f09a1827961f0 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/GetProductReviewsTest.php @@ -0,0 +1,284 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Review; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Registry; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Review\Model\ResourceModel\Review\Collection; +use Magento\Review\Model\ResourceModel\Review\CollectionFactory as ReviewCollectionFactory; +use Magento\Review\Model\Review; +use Magento\Review\Model\Review\SummaryFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for product reviews queries + */ +class GetProductReviewsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var ReviewCollectionFactory + */ + private $reviewCollectionFactory; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->reviewCollectionFactory = $objectManager->get(ReviewCollectionFactory::class); + $this->registry = $objectManager->get(Registry::class); + } + + /** + * @magentoApiDataFixture Magento/Review/_files/set_position_and_add_store_to_all_ratings.php + */ + public function testProductReviewRatingsMetadata() + { + $query + = <<<QUERY +{ + productReviewRatingsMetadata { + items { + id + name + values { + value_id + value + } + } + } +} +QUERY; + $expectedRatingItems = [ + [ + 'id' => 'Mw==', + 'name' => 'Price', + 'values' => [ + [ + 'value_id' => 'MTE=', + 'value' => "1" + ],[ + 'value_id' => 'MTI=', + 'value' => "2" + ],[ + 'value_id' => 'MTM=', + 'value' => "3" + ],[ + 'value_id' => 'MTQ=', + 'value' => "4" + ],[ + 'value_id' => 'MTU=', + 'value' => "5" + ] + ] + ], [ + 'id' => 'MQ==', + 'name' => 'Quality', + 'values' => [ + [ + 'value_id' => 'MQ==', + 'value' => "1" + ],[ + 'value_id' => 'Mg==', + 'value' => "2" + ],[ + 'value_id' => 'Mw==', + 'value' => "3" + ],[ + 'value_id' => 'NA==', + 'value' => "4" + ],[ + 'value_id' => 'NQ==', + 'value' => "5" + ] + ] + ], [ + 'id' => 'Mg==', + 'name' => 'Value', + 'values' => [ + [ + 'value_id' => 'Ng==', + 'value' => "1" + ],[ + 'value_id' => 'Nw==', + 'value' => "2" + ],[ + 'value_id' => 'OA==', + 'value' => "3" + ],[ + 'value_id' => 'OQ==', + 'value' => "4" + ],[ + 'value_id' => 'MTA=', + 'value' => "5" + ] + ] + ] + ]; + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('productReviewRatingsMetadata', $response); + self::assertArrayHasKey('items', $response['productReviewRatingsMetadata']); + self::assertNotEmpty($response['productReviewRatingsMetadata']['items']); + self::assertEquals($expectedRatingItems, $response['productReviewRatingsMetadata']['items']); + } + + /** + * @magentoApiDataFixture Magento/Review/_files/different_reviews.php + */ + public function testProductReviewRatings() + { + $productSku = 'simple'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $summaryFactory = ObjectManager::getInstance()->get(SummaryFactory::class); + $storeId = ObjectManager::getInstance()->get(StoreManagerInterface::class)->getStore()->getId(); + $summary = $summaryFactory->create()->setStoreId($storeId)->load($product->getId()); + $query + = <<<QUERY +{ + products(filter: { + sku: { + eq: "$productSku" + } + }) { + items { + rating_summary + review_count + reviews { + items { + nickname + summary + text + average_rating + product { + sku + name + } + ratings_breakdown { + name + value + } + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertNotEmpty($response['products']['items']); + + $items = $response['products']['items']; + self::assertEquals($summary->getData('rating_summary'), $items[0]['rating_summary']); + self::assertEquals($summary->getData('reviews_count'), $items[0]['review_count']); + self::assertArrayHasKey('items', $items[0]['reviews']); + self::assertNotEmpty($items[0]['reviews']['items']); + } + + /** + * @magentoApiDataFixture Magento/Review/_files/customer_review_with_rating.php + */ + public function testCustomerReviewsAddedToProduct() + { + $query = <<<QUERY +{ + customer { + reviews { + items { + nickname + summary + text + average_rating + ratings_breakdown { + name + value + } + } + } + } +} +QUERY; + $expectedFirstItem = [ + 'nickname' => 'Nickname', + 'summary' => 'Review Summary', + 'text' => 'Review text', + 'average_rating' => 40, + 'ratings_breakdown' => [ + [ + 'name' => 'Quality', + 'value' => 2 + ],[ + 'name' => 'Value', + 'value' => 2 + ] + ] + ]; + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('customer', $response); + self::assertArrayHasKey('reviews', $response['customer']); + self::assertArrayHasKey('items', $response['customer']['reviews']); + self::assertNotEmpty($response['customer']['reviews']['items']); + self::assertEquals($expectedFirstItem, $response['customer']['reviews']['items'][0]); + } + + /** + * Removing the recently added product reviews + */ + public function tearDown(): void + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $productId = 1; + /** @var Collection $reviewsCollection */ + $reviewsCollection = $this->reviewCollectionFactory->create(); + $reviewsCollection->addEntityFilter(Review::ENTITY_PRODUCT_CODE, $productId); + /** @var Review $review */ + foreach ($reviewsCollection as $review) { + $review->delete(); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } + + /** + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CreditmemoTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CreditmemoTest.php new file mode 100644 index 0000000000000..cca2b5a66407c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CreditmemoTest.php @@ -0,0 +1,650 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\GraphQl\Sales\Fixtures\CustomerPlaceOrder; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\CreditmemoFactory; +use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; +use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection; +use Magento\Sales\Model\Service\CreditmemoService; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for credit memo functionality + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CreditmemoTest extends GraphQlAbstract +{ + /** + * @var GetCustomerAuthenticationHeader + */ + private $customerAuthenticationHeader; + + /** @var CreditmemoFactory */ + private $creditMemoFactory; + + /** @var Order */ + private $order; + + /** @var OrderCollection */ + private $orderCollection; + + /** @var CreditmemoService */ + private $creditMemoService; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var OrderRepositoryInterface */ + private $orderRepository; + + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** + * Set up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerAuthenticationHeader = $objectManager->get( + GetCustomerAuthenticationHeader::class + ); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->creditMemoFactory = $objectManager->get(CreditmemoFactory::class); + $this->order = $objectManager->create(Order::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->orderCollection = $objectManager->get(OrderCollection::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->creditMemoService = $objectManager->get(CreditmemoService::class); + } + + protected function tearDown(): void + { + $this->cleanUpCreditMemos(); + $this->deleteOrder(); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customer_creditmemo_with_two_items.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreditMemoForLoggedInCustomerQuery(): void + { + $response = $this->getCustomerOrderWithCreditMemoQuery(); + + $expectedCreditMemoData = [ + [ + 'comments' => [ + ['message' => 'some_comment'], + ['message' => 'some_other_comment'] + ], + 'items' => [ + [ + 'product_name' => 'Simple Related Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10 + ], + 'discounts' => [], + 'quantity_refunded' => 1 + ], + [ + 'product_name' => 'Simple Product With Related Product', + 'product_sku' => 'simple_with_cross', + 'product_sale_price' => [ + 'value' => 10 + ], + 'discounts' => [], + 'quantity_refunded' => 1 + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 20 + ], + 'grand_total' => [ + 'value' => 20, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 10, + 'currency' => 'EUR' + ], + 'total_shipping' => [ + 'value' => 0 + ], + 'total_tax' => [ + 'value' => 0 + ], + 'shipping_handling' => [ + 'amount_including_tax' => [ + 'value' => 0 + ], + 'amount_excluding_tax' => [ + 'value' => 0 + ], + 'total_amount' => [ + 'value' => 0 + ], + 'taxes' => [], + 'discounts' => [], + ], + 'adjustment' => [ + 'value' => 1.23 + ] + ] + ] + ]; + + $firstOrderItem = current($response['customer']['orders']['items'] ?? []); + $this->assertArrayHasKey('credit_memos', $firstOrderItem); + $creditMemos = $firstOrderItem['credit_memos']; + $this->assertResponseFields($creditMemos, $expectedCreditMemoData); + } + + /** + * Test customer refund details from order for bundle product with a partial refund + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreditMemoForBundledProductsWithPartialRefund() + { + //Place order with bundled product + /** @var CustomerPlaceOrder $bundleProductOrderFixture */ + $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class); + $placeOrderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => 'bundle-product-two-dropdown-options', 'quantity' => 2] + ); + $orderNumber = $placeOrderResponse['placeOrder']['order']['order_number']; + $this->prepareInvoice($orderNumber, 2); + + $order = $this->order->loadByIncrementId($orderNumber); + /** @var Order\Item $orderItem */ + $orderItem = current($order->getAllItems()); + $orderItem->setQtyRefunded(1); + $order->addItem($orderItem); + $order->save(); + // Create a credit memo + $creditMemo = $this->creditMemoFactory->createByOrder($order, $order->getData()); + $creditMemo->setOrder($order); + $creditMemo->setState(1); + $creditMemo->setSubtotal(15); + $creditMemo->setBaseSubTotal(15); + $creditMemo->setShippingAmount(10); + $creditMemo->setBaseGrandTotal(23); + $creditMemo->setGrandTotal(23); + $creditMemo->setAdjustment(-2.00); + $creditMemo->addComment("Test comment for partial refund", false, true); + $creditMemo->save(); + + $this->creditMemoService->refund($creditMemo, true); + $response = $this->getCustomerOrderWithCreditMemoQuery(); + $expectedInvoicesData = [ + [ + 'items' => [ + [ + 'product_name' => 'Bundle Product With Two dropdown options', + 'product_sku' => 'bundle-product-two-dropdown-options-simple1-simple2', + 'product_sale_price' => [ + 'value' => 15 + ], + 'discounts' => [], + 'bundle_options' => [ + [ + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_name' => 'Simple Product1', + 'product_sku' => 'simple1', + 'quantity' => 1, + 'price' => ['value' => 1, 'currency' => 'USD'] + ] + ] + ], + [ + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_name' => 'Simple Product2', + 'product_sku' => 'simple2', + 'quantity' => 2, + 'price' => ['value' => 2, 'currency' => 'USD'] + ] + ] + ] + ], + 'quantity_invoiced' => 2 + ], + + ] + ] + ]; + $expectedCreditMemoData = [ + [ + 'comments' => [ + ['message' => 'Test comment for partial refund'] + ], + 'items' => [ + [ + 'product_name' => 'Bundle Product With Two dropdown options', + 'product_sku' => 'bundle-product-two-dropdown-options-simple1-simple2', + 'product_sale_price' => [ + 'value' => 15 + ], + 'discounts' => [], + 'bundle_options' => [ + [ + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_name' => 'Simple Product1', + 'product_sku' => 'simple1', + 'quantity' => 1, + 'price' => ['value' => 1, 'currency' => 'USD'] + ] + ] + ], + [ + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_name' => 'Simple Product2', + 'product_sku' => 'simple2', + 'quantity' => 2, + 'price' => ['value' => 2, 'currency' => 'USD'] + ] + ] + ] + ], + 'quantity_refunded' => 1 + ], + + ], + 'total' => [ + 'subtotal' => [ + 'value' => 15 + ], + 'grand_total' => [ + 'value' => 23, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 23, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 10 + ], + 'total_tax' => [ + 'value' => 0 + ], + 'shipping_handling' => [ + 'amount_including_tax' => [ + 'value' => 10 + ], + 'amount_excluding_tax' => [ + 'value' => 10 + ], + 'total_amount' => [ + 'value' => 10 + ], + 'taxes' => [], + 'discounts' => [], + ], + 'adjustment' => [ + 'value' => 2 + ] + ] + ] + ]; + $firstOrderItem = current($response['customer']['orders']['items'] ?? []); + + $this->assertArrayHasKey('invoices', $firstOrderItem); + $invoices = $firstOrderItem['invoices']; + $this->assertResponseFields($invoices, $expectedInvoicesData); + + $this->assertArrayHasKey('credit_memos', $firstOrderItem); + $creditMemos = $firstOrderItem['credit_memos']; + $this->assertResponseFields($creditMemos, $expectedCreditMemoData); + } + + /** + * Test customer order with credit memo details for bundle products with taxes and discounts + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreditMemoForBundleProductWithTaxesAndDiscounts() + { + //Place order with bundled product + /** @var CustomerPlaceOrder $bundleProductOrderFixture */ + $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class); + $placeOrderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => 'bundle-product-two-dropdown-options', 'quantity' => 2] + ); + $orderNumber = $placeOrderResponse['placeOrder']['order']['order_number']; + $this->prepareInvoice($orderNumber, 2); + $order = $this->order->loadByIncrementId($orderNumber); + /** @var Order\Item $orderItem */ + $orderItem = current($order->getAllItems()); + $orderItem->setQtyRefunded(1); + $order->addItem($orderItem); + $order->save(); + + $creditMemo = $this->creditMemoFactory->createByOrder($order, $order->getData()); + $creditMemo->setOrder($order); + $creditMemo->setState(1); + $creditMemo->setSubtotal(15); + $creditMemo->setBaseSubTotal(15); + $creditMemo->setShippingAmount(10); + $creditMemo->setTaxAmount(1.69); + $creditMemo->setBaseGrandTotal(24.19); + $creditMemo->setGrandTotal(24.19); + $creditMemo->setAdjustment(0.00); + $creditMemo->setDiscountAmount(-2.5); + $creditMemo->setDiscountDescription('Discount Label for 10% off'); + $creditMemo->addComment("Test comment for refund with taxes and discount", false, true); + $creditMemo->save(); + + $this->creditMemoService->refund($creditMemo, true); + $response = $this->getCustomerOrderWithCreditMemoQuery(); + $expectedCreditMemoData = [ + [ + 'comments' => [ + ['message' => 'Test comment for refund with taxes and discount'] + ], + 'items' => [ + [ + 'product_name' => 'Bundle Product With Two dropdown options', + 'product_sku' => 'bundle-product-two-dropdown-options-simple1-simple2', + 'product_sale_price' => [ + 'value' => 15 + ], + 'discounts' => [ + [ + 'amount' => [ + 'value' => 3, + 'currency' => "USD" + ], + 'label' => 'Discount Label for 10% off' + ] + ], + 'bundle_options' => [ + [ + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_name' => 'Simple Product1', + 'product_sku' => 'simple1', + 'quantity' => 1, + 'price' => ['value' => 1, 'currency' => 'USD'] + ] + ] + ], + [ + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_name' => 'Simple Product2', + 'product_sku' => 'simple2', + 'quantity' => 2, + 'price' => ['value' => 2, 'currency' => 'USD'] + ] + ] + ] + ], + 'quantity_refunded' => 1 + ], + + ], + 'total' => [ + 'subtotal' => [ + 'value' => 15 + ], + 'grand_total' => [ + 'value' => 24.19, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 24.19, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 10 + ], + 'total_tax' => [ + 'value'=> 1.69 + ], + 'shipping_handling' => [ + 'amount_including_tax' => [ + 'value' => 10.75 + ], + 'amount_excluding_tax' => [ + 'value' => 10 + ], + 'total_amount' => [ + 'value' => 10 + ], + 'taxes'=> [ + 0 => [ + 'amount' => ['value' => 0.67], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ], + 'discounts' => [ + [ + 'amount'=> ['value'=> 1] + ] + ], + ], + 'adjustment' => [ + 'value' => 0 + ] + ] + ] + ]; + $firstOrderItem = current($response['customer']['orders']['items'] ?? []); + $this->assertArrayHasKey('credit_memos', $firstOrderItem); + + $creditMemos = $firstOrderItem['credit_memos']; + $this->assertResponseFields($creditMemos, $expectedCreditMemoData); + } + + /** + * Prepare invoice for the order + * + * @param string $orderNumber + * @param int|null $qty + */ + private function prepareInvoice(string $orderNumber, int $qty = null) + { + /** @var \Magento\Sales\Model\Order $order */ + $order = Bootstrap::getObjectManager() + ->create(\Magento\Sales\Model\Order::class)->loadByIncrementId($orderNumber); + $orderItem = current($order->getItems()); + $orderService = Bootstrap::getObjectManager()->create( + \Magento\Sales\Api\InvoiceManagementInterface::class + ); + $invoice = $orderService->prepareInvoice($order, [$orderItem->getId() => $qty]); + $invoice->register(); + $order = $invoice->getOrder(); + $order->setIsInProcess(true); + $transactionSave = Bootstrap::getObjectManager() + ->create(\Magento\Framework\DB\Transaction::class); + $transactionSave->addObject($invoice)->addObject($order)->save(); + } + + /** + * @return void + */ + private function deleteOrder(): void + { + /** @var \Magento\Framework\Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(OrderCollection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * @return void + */ + private function cleanUpCreditMemos(): void + { + /** @var \Magento\Framework\Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $creditmemoRepository = Bootstrap::getObjectManager()->get(CreditmemoRepositoryInterface::class); + $creditmemoCollection = Bootstrap::getObjectManager()->create(Collection::class); + foreach ($creditmemoCollection as $creditmemo) { + $creditmemoRepository->delete($creditmemo); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Get CustomerOrder with credit memo details + * + * @return array + * @throws AuthenticationException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function getCustomerOrderWithCreditMemoQuery(): array + { + $query = + <<<QUERY +query { + customer { + orders { + items { + invoices { + items { + product_name + product_sku + product_sale_price { + value + } + ... on BundleInvoiceItem { + bundle_options { + label + values { + product_sku + product_name + quantity + price { + value + currency + } + } + } + } + discounts { amount{value currency} label } + quantity_invoiced + discounts { amount{value currency} label } + } + } + credit_memos { + comments { + message + } + items { + product_name + product_sku + product_sale_price { + value + } + ... on BundleCreditMemoItem { + bundle_options { + label + values { + product_sku + product_name + quantity + price { + value + currency + } + } + } + } + discounts { amount{value currency} label } + quantity_refunded + } + total { + subtotal { + value + } + base_grand_total { + value + currency + } + grand_total { + value + currency + } + total_shipping { + value + } + total_tax { + value + } + shipping_handling { + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value} + taxes {amount{value} title rate} + discounts {amount{value}} + } + adjustment { + value + } + } + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CustomerOrders/OrderShipmentsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CustomerOrders/OrderShipmentsTest.php new file mode 100644 index 0000000000000..c9f507b1f94e8 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CustomerOrders/OrderShipmentsTest.php @@ -0,0 +1,328 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales\CustomerOrders; + +use Magento\Framework\DB\Transaction; +use Magento\Framework\Registry; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\GraphQl\Sales\Fixtures\CustomerPlaceOrder; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\ShipmentFactory; +use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class OrderShipmentsTest extends GraphQlAbstract +{ + /** + * @var GetCustomerAuthenticationHeader + */ + private $getCustomerAuthHeader; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + protected function setUp(): void + { + $this->getCustomerAuthHeader = Bootstrap::getObjectManager()->get(GetCustomerAuthenticationHeader::class); + $this->orderRepository = Bootstrap::getObjectManager()->get(OrderRepositoryInterface::class); + } + + protected function tearDown(): void + { + $this->cleanupOrders(); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment.php + */ + public function testGetOrderShipment() + { + $query = $this->getQuery('100000555'); + $authHeader = $this->getCustomerAuthHeader->execute('customer_uk_address@test.com', 'password'); + $orderModel = $this->fetchOrderModel('100000555'); + + $result = $this->graphQlQuery($query, [], '', $authHeader); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['customer']['orders']['items']); + + $order = $result['customer']['orders']['items'][0]; + $this->assertEquals('Flat Rate', $order['carrier']); + $this->assertEquals('Flat Rate - Fixed', $order['shipping_method']); + $this->assertArrayHasKey('shipments', $order); + /** @var Shipment $orderShipmentModel */ + $orderShipmentModel = $orderModel->getShipmentsCollection()->getFirstItem(); + $shipment = $order['shipments'][0]; + $this->assertEquals(base64_encode($orderShipmentModel->getIncrementId()), $shipment['id']); + $this->assertEquals($orderShipmentModel->getIncrementId(), $shipment['number']); + //Check Tracking + $this->assertCount(1, $shipment['tracking']); + $tracking = $shipment['tracking'][0]; + $this->assertEquals('ups', $tracking['carrier']); + $this->assertEquals('United Parcel Service', $tracking['title']); + $this->assertEquals('1234567890', $tracking['number']); + //Check Items + $this->assertCount(2, $shipment['items']); + foreach ($orderShipmentModel->getItems() as $expectedItem) { + $sku = $expectedItem->getSku(); + $findItem = array_filter($shipment['items'], function ($item) use ($sku) { + return $item['product_sku'] === $sku; + }); + $this->assertCount(1, $findItem); + $actualItem = reset($findItem); + $expectedEncodedId = base64_encode($expectedItem->getEntityId()); + $this->assertEquals($expectedEncodedId, $actualItem['id']); + $this->assertEquals($expectedItem->getSku(), $actualItem['product_sku']); + $this->assertEquals($expectedItem->getName(), $actualItem['product_name']); + $this->assertEquals($expectedItem->getPrice(), $actualItem['product_sale_price']['value']); + $this->assertEquals('USD', $actualItem['product_sale_price']['currency']); + $this->assertEquals('1', $actualItem['quantity_shipped']); + //Check correct order_item + $this->assertNotEmpty($actualItem['order_item']); + $this->assertEquals($expectedItem->getSku(), $actualItem['order_item']['product_sku']); + } + //Check comments + $this->assertCount(1, $shipment['comments']); + $this->assertEquals('This comment is visible to the customer', $shipment['comments'][0]['message']); + $this->assertNotEmpty($shipment['comments'][0]['timestamp']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments.php + */ + public function testGetOrderShipmentsMultiple() + { + $query = $this->getQuery('100000555'); + $authHeader = $this->getCustomerAuthHeader->execute('customer_uk_address@test.com', 'password'); + + $result = $this->graphQlQuery($query, [], '', $authHeader); + $this->assertArrayNotHasKey('errors', $result); + $order = $result['customer']['orders']['items'][0]; + $shipments = $order['shipments']; + $this->assertCount(2, $shipments); + $this->assertEquals('0000000098', $shipments[0]['number']); + $this->assertCount(1, $shipments[0]['items']); + $this->assertEquals('0000000099', $shipments[1]['number']); + $this->assertCount(1, $shipments[1]['items']); + } + + /** + * @magentoConfigFixture default_store carriers/ups/active 1 + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping.php + */ + public function testOrderShipmentWithUpsCarrier() + { + $query = $this->getQuery('100000001'); + $authHeader = $this->getCustomerAuthHeader->execute('customer@example.com', 'password'); + + $result = $this->graphQlQuery($query, [], '', $authHeader); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertEquals('UPS Next Day Air', $result['customer']['orders']['items'][0]['shipping_method']); + $this->assertEquals('United Parcel Service', $result['customer']['orders']['items'][0]['carrier']); + $shipments = $result['customer']['orders']['items'][0]['shipments']; + $expectedTracking = [ + 'title' => 'United Parcel Service', + 'carrier' => 'ups', + 'number' => '987654321' + ]; + $this->assertEquals($expectedTracking, $shipments[0]['tracking'][0]); + } + + /** + * @magentoConfigFixture default_store carriers/ups/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + */ + public function testOrderShipmentWithBundleProduct() + { + //Place order with bundled product + /** @var CustomerPlaceOrder $bundleProductOrderFixture */ + $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class); + $placeOrderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => 'bundle-product-two-dropdown-options'] + ); + $orderNumber = $placeOrderResponse['placeOrder']['order']['order_number']; + $this->shipOrder($orderNumber); + + $result = $this->graphQlQuery( + $this->getQuery(), + [], + '', + $this->getCustomerAuthHeader->execute('customer@example.com', 'password') + ); + $this->assertArrayNotHasKey('errors', $result); + + $shipments = $result['customer']['orders']['items'][0]['shipments']; + $shipmentBundleItem = $shipments[0]['items'][0]; + + $shipmentItemAssertionMap = [ + 'order_item' => [ + 'product_sku' => 'bundle-product-two-dropdown-options-simple1-simple2' + ], + 'product_name' => 'Bundle Product With Two dropdown options', + 'product_sku' => 'bundle-product-two-dropdown-options-simple1-simple2', + 'product_sale_price' => [ + 'value' => 15, + 'currency' => 'USD' + ], + 'bundle_options' => [ + [ + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_name' => 'Simple Product1', + 'product_sku' => 'simple1', + 'quantity' => 1, + 'price' => ['value' => 1] + ] + ] + ], + [ + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_name' => 'Simple Product2', + 'product_sku' => 'simple2', + 'quantity' => 2, + 'price' => ['value' => 2] + ] + ] + ] + ] + ]; + + $this->assertResponseFields($shipmentBundleItem, $shipmentItemAssertionMap); + } + + /** + * Get query that fetch orders and shipment information + * + * @param string|null $orderId + * @return string + */ + private function getQuery(string $orderId = null) + { + $filter = $orderId ? "(filter:{number:{eq:\"$orderId\"}})" : ""; + return <<<QUERY +{ + customer { + orders {$filter}{ + items { + number + status + items { + product_sku + } + carrier + shipping_method + shipments { + id + number + tracking { + title + carrier + number + } + items { + id + order_item { + product_sku + } + product_name + product_sku + product_sale_price { + value + currency + } + ... on BundleShipmentItem { + bundle_options { + label + values { + product_name + product_sku + quantity + price { + value + } + } + } + } + quantity_shipped + } + comments { + timestamp + message + } + } + } + } + } +} +QUERY; + } + + /** + * Get model instance for order by number + * + * @param string $orderNumber + * @return Order + */ + private function fetchOrderModel(string $orderNumber): Order + { + /** @var Order $order */ + $order = Bootstrap::getObjectManager()->get(Order::class); + $order->loadByIncrementId($orderNumber); + return $order; + } + + /** + * Create shipment for order + * + * @param string $orderNumber + */ + private function shipOrder(string $orderNumber): void + { + $order = $this->fetchOrderModel($orderNumber); + $order->setIsInProcess(true); + /** @var Transaction $transaction */ + $transaction = Bootstrap::getObjectManager()->create(Transaction::class); + + $items = []; + foreach ($order->getItems() as $orderItem) { + $items[$orderItem->getId()] = $orderItem->getQtyOrdered(); + } + + $shipment = Bootstrap::getObjectManager()->get(ShipmentFactory::class)->create($order, $items); + $shipment->register(); + $transaction->addObject($shipment)->addObject($order)->save(); + } + + /** + * Clean up orders + */ + private function cleanupOrders() + { + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(OrderCollection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrder.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrder.php new file mode 100644 index 0000000000000..0386d414b8682 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrder.php @@ -0,0 +1,367 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales\Fixtures; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\TestCase\GraphQl\Client; + +class CustomerPlaceOrder +{ + /** + * @var Client + */ + private $gqlClient; + + /** + * @var CustomerTokenServiceInterface + */ + private $tokenService; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var string + */ + private $authHeader; + + /** + * @var string + */ + private $cartId; + + /** + * @var array + */ + private $customerLogin; + + /** + * @param Client $gqlClient + * @param CustomerTokenServiceInterface $tokenService + * @param ProductRepositoryInterface $productRepository + */ + public function __construct( + Client $gqlClient, + CustomerTokenServiceInterface $tokenService, + ProductRepositoryInterface $productRepository + ) { + $this->gqlClient = $gqlClient; + $this->tokenService = $tokenService; + $this->productRepository = $productRepository; + } + + /** + * Place order for a bundled product + * + * @param array $customerLogin + * @param array $productData + * @return array + */ + public function placeOrderWithBundleProduct(array $customerLogin, array $productData): array + { + $this->customerLogin = $customerLogin; + $this->createCustomerCart(); + $this->addBundleProduct($productData); + $this->setBillingAddress(); + $shippingMethod = $this->setShippingAddress(); + $paymentMethod = $this->setShippingMethod($shippingMethod); + $this->setPaymentMethod($paymentMethod); + return $this->doPlaceOrder(); + } + + /** + * Make GraphQl POST request + * + * @param string $query + * @param array $additionalHeaders + * @return array + */ + private function makeRequest(string $query, array $additionalHeaders = []): array + { + $headers = array_merge([$this->getAuthHeader()], $additionalHeaders); + return $this->gqlClient->post($query, [], '', $headers); + } + + /** + * Get header for authenticated requests + * + * @return string + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function getAuthHeader(): string + { + if (empty($this->authHeader)) { + $customerToken = $this->tokenService + ->createCustomerAccessToken($this->customerLogin['email'], $this->customerLogin['password']); + $this->authHeader = "Authorization: Bearer {$customerToken}"; + } + return $this->authHeader; + } + + /** + * Get cart id + * + * @return string + */ + private function getCartId(): string + { + if (empty($this->cartId)) { + $this->cartId = $this->createCustomerCart(); + } + return $this->cartId; + } + + /** + * Create empty cart for the customer + * + * @return array + */ + private function createCustomerCart(): string + { + //Create empty cart + $createEmptyCart = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $result = $this->makeRequest($createEmptyCart); + return $result['createEmptyCart']; + } + + /** + * Add a bundle product to the cart + * + * @param array $productData + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function addBundleProduct(array $productData) + { + $productSku = $productData['sku']; + $qty = $productData['quantity'] ?? 1; + /** @var Product $bundleProduct */ + $bundleProduct = $this->productRepository->get($productSku); + /** @var \Magento\Bundle\Model\Product\Type $typeInstance */ + $typeInstance = $bundleProduct->getTypeInstance(); + $optionId1 = (int)$typeInstance->getOptionsCollection($bundleProduct)->getFirstItem()->getId(); + $optionId2 = (int)$typeInstance->getOptionsCollection($bundleProduct)->getLastItem()->getId(); + $selectionId1 = (int)$typeInstance->getSelectionsCollection([$optionId1], $bundleProduct) + ->getFirstItem() + ->getSelectionId(); + $selectionId2 = (int)$typeInstance->getSelectionsCollection([$optionId2], $bundleProduct) + ->getLastItem() + ->getSelectionId(); + + $addProduct = <<<QUERY +mutation { + addBundleProductsToCart(input:{ + cart_id:"{$this->getCartId()}" + cart_items:[ + { + data:{ + sku:"{$productSku}" + quantity:{$qty} + } + bundle_options:[ + { + id:{$optionId1} + quantity:1 + value:["{$selectionId1}"] + } + { + id:$optionId2 + quantity:2 + value:["{$selectionId2}"] + } + ] + } + ] + }) { + cart { + items {quantity product {sku}} + } + } +} +QUERY; + return $this->makeRequest($addProduct); + } + + /** + * Set the billing address on the cart + * + * @return array + */ + private function setBillingAddress(): array + { + $setBillingAddress = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$this->getCartId()}" + billing_address: { + address: { + firstname: "John" + lastname: "Smith" + company: "Test company" + street: ["test street 1", "test street 2"] + city: "Texas City" + postcode: "78717" + telephone: "5123456677" + region: "TX" + country_code: "US" + } + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + return $this->makeRequest($setBillingAddress); + } + + /** + * Set the shipping address on the cart and return an available shipping method + * + * @return array + */ + private function setShippingAddress(): array + { + $setShippingAddress = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "{$this->getCartId()}" + shipping_addresses: [ + { + address: { + firstname: "test shipFirst" + lastname: "test shipLast" + company: "test company" + street: ["test street 1", "test street 2"] + city: "Montgomery" + region: "AL" + postcode: "36013" + country_code: "US" + telephone: "3347665522" + } + } + ] + } + ) { + cart { + shipping_addresses { + available_shipping_methods { + carrier_code + method_code + amount {value} + } + } + } + } +} +QUERY; + $result = $this->makeRequest($setShippingAddress); + $shippingMethod = $result['setShippingAddressesOnCart'] + ['cart']['shipping_addresses'][0]['available_shipping_methods'][0]; + return $shippingMethod; + } + + /** + * Set the shipping method on the cart and return an available payment method + * + * @param array $shippingMethod + * @return array + */ + private function setShippingMethod(array $shippingMethod): array + { + $setShippingMethod = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$this->getCartId()}", + shipping_methods: [ + { + carrier_code: "{$shippingMethod['carrier_code']}" + method_code: "{$shippingMethod['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $result = $this->makeRequest($setShippingMethod); + $paymentMethod = $result['setShippingMethodsOnCart']['cart']['available_payment_methods'][0]; + return $paymentMethod; + } + + /** + * Set the payment method on the cart + * + * @param array $paymentMethod + * @return array + */ + private function setPaymentMethod(array $paymentMethod): array + { + $setPaymentMethod = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$this->getCartId()}" + payment_method: { + code: "{$paymentMethod['code']}" + } + } + ) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + return $this->makeRequest($setPaymentMethod); + } + + /** + * Place the order + * + * @return array + */ + private function doPlaceOrder(): array + { + $placeOrder = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$this->getCartId()}" + } + ) { + order { + order_number + } + } +} +QUERY; + return $this->makeRequest($placeOrder); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php new file mode 100644 index 0000000000000..8b18d4bd07d1b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php @@ -0,0 +1,880 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Framework\Registry; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\GraphQl\GetCustomerAuthenticationHeader; + +/** + * Tests the Invoice query + */ +class InvoiceTest extends GraphQlAbstract +{ + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + /** @var OrderRepositoryInterface */ + private $orderRepository; + + protected function setUp(): void + { + $this->customerAuthenticationHeader + = Bootstrap::getObjectManager()->get(GetCustomerAuthenticationHeader::class); + $this->orderRepository = Bootstrap::getObjectManager()->get(OrderRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSingleInvoiceForLoggedInCustomerQuery() + { + $response = $this->getCustomerInvoicesBasedOnOrderNumber('100000001'); + $expectedOrdersData = [ + 'status' => 'Processing', + 'grand_total' => 100.00 + ]; + $expectedInvoiceData = [ + [ + 'items' => [ + [ + 'product_name' => 'Simple Related Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 1, + 'discounts' => [] + ], + [ + 'product_name' => 'Simple Product With Related Product', + 'product_sku' => 'simple_with_cross', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 1, + 'discounts' => [] + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 100, + 'currency' => 'USD' + ], + 'grand_total' => [ + 'value' => 100, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'shipping_handling' => [ + 'total_amount' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'amount_including_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'amount_excluding_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'taxes' => [], + 'discounts' => [] + ], + 'taxes' => [], + 'discounts' => [], + 'base_grand_total' => [ + 'value' => 100, + 'currency' => 'EUR' + ], + 'total_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + ] + ] + ]; + $this->assertOrdersData($response, $expectedOrdersData); + $invoices = $response[0]['invoices']; + $this->assertResponseFields($invoices, $expectedInvoiceData); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testMultipleInvoiceForLoggedInCustomerQuery() + { + $response = $this->getCustomerInvoicesBasedOnOrderNumber('100000002'); + $expectedOrdersData = [ + 'status' => 'Processing', + 'grand_total' => 60.00 + ]; + $expectedInvoiceData = [ + [ + 'items' => [ + [ + 'product_name' => 'Simple Related Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 3, + 'discounts'=> [] + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 30, + 'currency' => 'USD' + ], + 'grand_total' => [ + 'value' => 50, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 20, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 50, + 'currency' => 'EUR' + ], + 'total_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'shipping_handling' => [ + 'total_amount' => [ + 'value' => 20, + 'currency' => 'USD' + ], + 'amount_including_tax' => [ + 'value' => 25, + 'currency' => 'USD' + ], + 'amount_excluding_tax' => [ + 'value' => 20, + 'currency' => 'USD' + ], + 'taxes' => [], + 'discounts' => [], + ], + 'taxes' => [], + 'discounts' => [], + ] + ], + [ + 'items' => [ + [ + 'product_name' => 'Simple Product With Related Product', + 'product_sku' => 'simple_with_cross', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 1, + 'discounts' => [] + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'grand_total' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 0, + 'currency' => 'EUR' + ], + 'total_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'shipping_handling' => [ + 'total_amount' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'amount_including_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'amount_excluding_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'taxes' => [], + 'discounts' => [], + ], + 'taxes' => [], + 'discounts' => [], + ] + ] + ]; + $this->assertOrdersData($response, $expectedOrdersData); + $invoices = $response[0]['invoices']; + $this->assertResponseFields($invoices, $expectedInvoiceData); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customers_with_invoices.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testMultipleCustomersWithInvoicesQuery() + { + $query = + <<<QUERY +{ + customer + { + orders { + items { + status + total { + grand_total { + value + currency + } + } + invoices { + items{ + product_name + product_sku + product_sale_price { + value + currency + } + quantity_invoiced + } + total { + subtotal { + value + currency + } + grand_total { + value + currency + } + total_shipping { + value + currency + } + } + } +} +} +} +} +QUERY; + + $currentEmail = 'customer@search.example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $expectedOrdersData = [ + 'status' => 'Processing', + 'grand_total' => 100.00 + ]; + + $expectedInvoiceData = [ + [ + 'items' => [ + [ + 'product_name' => 'Simple Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 1 + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 100, + 'currency' => 'USD' + ], + 'grand_total' => [ + 'value' => 100, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 0, + 'currency' => 'USD' + ] + ] + ] + ]; + $this->assertOrdersData($response['customer']['orders']['items'], $expectedOrdersData); + $invoices = $response['customer']['orders']['items'][0]['invoices']; + $this->assertResponseFields($invoices, $expectedInvoiceData); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + */ + public function testInvoiceForCustomerWithTaxesAndDiscounts() + { + $quantity = 2; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $orderNumber = $this->placeOrder($cartId); + $this->prepareInvoice($orderNumber, 2); + $customerOrderResponse = $this->getCustomerInvoicesBasedOnOrderNumber($orderNumber); + $customerOrderItem = $customerOrderResponse[0]; + $invoice = $customerOrderItem['invoices'][0]; + $this->assertEquals(3, $invoice['total']['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $invoice['total']['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $invoice['total']['discounts'][0]['label'] + ); + $this->assertTotalsAndShippingWithTaxesAndDiscounts($customerOrderItem['invoices'][0]['total']); + $this->deleteOrder(); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + */ + public function testPartialInvoiceForCustomerWithTaxesAndDiscounts() + { + $quantity = 2; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $orderNumber = $this->placeOrder($cartId); + $this->prepareInvoice($orderNumber, 1); + $customerOrderResponse = $this->getCustomerInvoicesBasedOnOrderNumber($orderNumber); + $customerOrderItem = $customerOrderResponse[0]; + $invoice = $customerOrderItem['invoices'][0]; + $invoiceItem = $invoice['items'][0]; + $this->assertEquals(1, $invoiceItem['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $invoiceItem['discounts'][0]['amount']['currency']); + $this->assertEquals('Discount Label for 10% off', $invoiceItem['discounts'][0]['label']); + $this->assertEquals(2, $invoice['total']['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $invoice['total']['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $invoice['total']['discounts'][0]['label'] + ); + $this->assertTotalsAndShippingWithTaxesAndDiscountsForOneQty($customerOrderItem['invoices'][0]['total']); + $this->deleteOrder(); + } + + /** + * Prepare invoice for the order + * + * @param string $orderNumber + * @param int|null $qty + */ + private function prepareInvoice(string $orderNumber, int $qty = null) + { + /** @var \Magento\Sales\Model\Order $order */ + $order = Bootstrap::getObjectManager() + ->create(\Magento\Sales\Model\Order::class)->loadByIncrementId($orderNumber); + $orderItem = current($order->getItems()); + $orderService = Bootstrap::getObjectManager()->create( + \Magento\Sales\Api\InvoiceManagementInterface::class + ); + $invoice = $orderService->prepareInvoice($order, [$orderItem->getId() => $qty]); + $invoice->register(); + $order = $invoice->getOrder(); + $order->setIsInProcess(true); + $transactionSave = Bootstrap::getObjectManager() + ->create(\Magento\Framework\DB\Transaction::class); + $transactionSave->addObject($invoice)->addObject($order)->save(); + } + + /** + * Check order totals an shipping amounts with taxes + * + * @param array $customerOrderItemTotal + */ + private function assertTotalsAndShippingWithTaxesAndDiscounts(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(2.03, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 29.1, 'currency' =>'USD'], + 'grand_total' => ['value' => 29.1, 'currency' =>'USD'], + 'total_tax' => ['value' => 2.03, 'currency' =>'USD'], + 'subtotal' => ['value' => 20, 'currency' =>'USD'], + 'total_shipping' => ['value' => 10, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 10.75, 'currency' =>'USD'], + 'amount_excluding_tax' => ['value' => 10, 'currency' =>'USD'], + 'total_amount' => ['value' => 10, 'currency' =>'USD'], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 0.68], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ], + 'discounts'=> [ + 0 => ['amount'=>['value' => 1, 'currency'=> 'USD']] + ], + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Check order totals an shipping amounts with taxes + * + * @param array $customerOrderItemTotal + */ + private function assertTotalsAndShippingWithTaxesAndDiscountsForOneQty(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(1.36, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 19.43, 'currency' =>'USD'], + 'grand_total' => ['value' => 19.43, 'currency' =>'USD'], + 'total_tax' => ['value' => 1.36, 'currency' =>'USD'], + 'subtotal' => ['value' => 10, 'currency' =>'USD'], + 'total_shipping' => ['value' => 10, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 10.75, 'currency' =>'USD'], + 'amount_excluding_tax' => ['value' => 10, 'currency' =>'USD'], + 'total_amount' => ['value' => 10, 'currency' =>'USD'], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 0.68], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ], + 'discounts'=> [['amount'=>['value' => 1, 'currency'=> 'USD']] + ], + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Create an empty cart with GraphQl mutation + * + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['createEmptyCart']; + } + + /** + * Add product to cart with GraphQl query + * + * @param string $cartId + * @param float $qty + * @param string $sku + * @return void + */ + private function addProductToCart(string $cartId, float $qty, string $sku): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$cartId}" + cart_items: [ + { + data: { + quantity: {$qty} + sku: "{$sku}" + } + } + ] + } + ) { + cart {items{quantity product {sku}}}} +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Set billing address on cart with GraphQL mutation + * + * @param string $cartId + * @return void + */ + private function setBillingAddress(string $cartId): void + { + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$cartId}" + billing_address: { + address: { + firstname: "John" + lastname: "Smith" + company: "Test company" + street: ["test street 1", "test street 2"] + city: "Texas City" + postcode: "78717" + telephone: "5123456677" + region: "TX" + country_code: "US" + } + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Set shipping address on cart with GraphQl query + * + * @param string $cartId + * @return array + */ + private function setShippingAddress(string $cartId): array + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "test shipFirst" + lastname: "test shipLast" + company: "test company" + street: ["test street 1", "test street 2"] + city: "Montgomery" + region: "AL" + postcode: "36013" + country_code: "US" + telephone: "3347665522" + } + } + ] + } + ) { + cart { + shipping_addresses { + available_shipping_methods { + carrier_code + method_code + amount {value} + } + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $shippingAddress = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + $availableShippingMethod = current($shippingAddress['available_shipping_methods']); + return $availableShippingMethod; + } + + /** + * Set shipping method on cart with GraphQl mutation + * + * @param string $cartId + * @param array $method + * @return array + */ + private function setShippingMethod(string $cartId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + return $availablePaymentMethod; + } + + /** + * Set payment method on cart with GrpahQl mutation + * + * @param string $cartId + * @param array $method + * @return void + */ + private function setPaymentMethod(string $cartId, array $method): void + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$cartId}" + payment_method: { + code: "{$method['code']}" + } + } + ) { + cart {selected_payment_method {code}} + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Place order using GraphQl mutation + * + * @param string $cartId + * @return string + */ + private function placeOrder(string $cartId): string + { + $query = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$cartId}" + } + ) { + order { + order_number + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['placeOrder']['order']['order_number']; + } + + /** + * Get customer order query + * + * @param string $orderNumber + * @return array + */ + private function getCustomerInvoicesBasedOnOrderNumber($orderNumber): array + { + $query = + <<<QUERY +{ + customer { + email + orders(filter:{number:{eq:"{$orderNumber}"}}) { + total_count + items { + status + total { + grand_total{value currency} + } + invoices { + items{ + product_name product_sku product_sale_price{value currency}quantity_invoiced + discounts {amount{value currency} label} + } + total { + base_grand_total{value currency} + grand_total{value currency} + total_tax{value currency} + subtotal { value currency } + taxes {amount{value currency} title rate} + discounts {amount{value currency} label} + total_shipping{value currency} + shipping_handling + { + amount_including_tax{value currency} + amount_excluding_tax{value currency} + total_amount{value currency} + taxes {amount{value} title rate} + discounts {amount{value currency}} + } + } + } + } + } + } + } +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + return $response['customer']['orders']['items']; + } + + private function assertOrdersData($response, $expectedOrdersData): void + { + $actualData = $response[0]; + $this->assertEquals( + $expectedOrdersData['grand_total'], + $actualData['total']['grand_total']['value'], + "grand_total is different than the expected for order" + ); + $this->assertEquals( + $expectedOrdersData['status'], + $actualData['status'], + "status is different than the expected for order" + ); + } + + /** + * Clean up orders + * + * @return void + */ + private function deleteOrder(): void + { + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(Collection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php index 7bece410a06f8..0baee2797bf5d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php @@ -108,7 +108,7 @@ public function testSimpleProductOutOfStock() /** @var \Magento\Catalog\Api\ProductRepositoryInterface $repository */ $productRepository = Bootstrap::getObjectManager() ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $productSku = 'simple'; + $productSku = 'simple-2'; /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ $product = $productRepository->get($productSku); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php new file mode 100644 index 0000000000000..299bccc5a1277 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php @@ -0,0 +1,1394 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Registry; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Class RetrieveOrdersTest + */ +class RetrieveOrdersByOrderNumberTest extends GraphQlAbstract +{ + /** @var OrderRepositoryInterface */ + private $orderRepository; + + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + protected function setUp():void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->customerAuthenticationHeader = $objectManager->get(GetCustomerAuthenticationHeader::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetCustomerOrdersSimpleProductQuery() + { + $orderNumber = '100000002'; + $response = $this->getCustomerOrderQueryOnSimpleProducts($orderNumber); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertNotEmpty($response['customer']['orders']['items']); + $customerOrderItemsInResponse = $response['customer']['orders']['items'][0]; + $this->assertArrayHasKey('items', $customerOrderItemsInResponse); + $this->assertNotEmpty($customerOrderItemsInResponse['items']); + $this->assertNotEmpty($response["customer"]["orders"]["items"][0]["billing_address"]); + $this->assertNotEmpty($response["customer"]["orders"]["items"][0]["shipping_address"]); + $this->assertNotEmpty($response["customer"]["orders"]["items"][0]["payment_methods"]); + + $searchCriteria = $this->searchCriteriaBuilder->addFilter('increment_id', '100000002') + ->create(); + /** @var \Magento\Sales\Api\Data\OrderInterface[] $orders */ + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + foreach ($orders as $order) { + $orderNumber = $order->getIncrementId(); + $this->assertNotEmpty($customerOrderItemsInResponse['id']); + $this->assertEquals($orderNumber, $customerOrderItemsInResponse['number']); + $this->assertEquals('Processing', $customerOrderItemsInResponse['status']); + } + $expectedOrderItems = [ + 'quantity_ordered'=> 2, + 'product_sku'=> 'simple', + 'product_name'=> 'Simple Product', + 'product_sale_price'=> ['currency'=> 'USD', 'value'=> 10] + ]; + $actualOrderItemsFromResponse = $customerOrderItemsInResponse['items'][0]; + $this->assertEquals($expectedOrderItems, $actualOrderItemsFromResponse); + $actualOrderTotalFromResponse = $response['customer']['orders']['items'][0]['total']; + $expectedOrderTotal = [ + 'base_grand_total' => ['value'=> 120,'currency' =>'USD'], + 'grand_total' => ['value'=> 120,'currency' =>'USD'], + 'subtotal' => ['value'=> 120,'currency' =>'USD'] + ]; + $this->assertEquals($expectedOrderTotal, $actualOrderTotalFromResponse, 'Totals do not match'); + } + + /** + * Verify the customer order with tax, discount with shipping tax class set for calculation setting + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testCustomerOrdersSimpleProductWithTaxesAndDiscounts() + { + $quantity = 4; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + $billingAssertionMap = [ + 'firstname' => 'John', + 'lastname' => 'Smith', + 'city' => 'Texas City', + 'company' => 'Test company', + 'country_code' => 'US', + 'postcode' => '78717', + 'region' => 'Texas', + 'region_id' => '57', + 'street' => [ + 0 => 'test street 1', + 1 => 'test street 2', + ], + 'telephone' => '5123456677' + ]; + $this->assertResponseFields($customerOrderResponse[0]["billing_address"], $billingAssertionMap); + $shippingAssertionMap = [ + 'firstname' => 'test shipFirst', + 'lastname' => 'test shipLast', + 'city' => 'Montgomery', + 'company' => 'test company', + 'country_code' => 'US', + 'postcode' => '36013', + 'street' => [ + 0 => 'test street 1', + 1 => 'test street 2', + ], + 'region_id' => '1', + 'region' => 'Alabama', + 'telephone' => '3347665522' + ]; + $this->assertResponseFields($customerOrderResponse[0]["shipping_address"], $shippingAssertionMap); + $paymentMethodAssertionMap = [ + [ + 'name' => 'Check / Money order', + 'type' => 'checkmo', + 'additional_data' => [] + ] + ]; + $this->assertResponseFields($customerOrderResponse[0]["payment_methods"], $paymentMethodAssertionMap); + // Asserting discounts on order item level + $this->assertEquals(4, $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $customerOrderResponse[0]['items'][0]['discounts'][0]['label'] + ); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsWithTaxesAndDiscounts($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * @param array $customerOrderItemTotal + */ + private function assertTotalsWithTaxesAndDiscounts(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(4.05, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 58.05, 'currency' =>'USD'], + 'grand_total' => ['value' => 58.05, 'currency' =>'USD'], + 'subtotal' => ['value' => 40, 'currency' =>'USD'], + 'total_tax' => ['value' => 4.05, 'currency' =>'USD'], + 'total_shipping' => ['value' => 20, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 21.5], + 'amount_excluding_tax' => ['value' => 20], + 'total_amount' => ['value' => 20, 'currency' =>'USD'], + 'discounts' => [ + 0 => ['amount'=>['value'=> 2, 'currency' =>'USD']] + ], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 1.35], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ] + ], + 'discounts' => [ + 0 => ['amount' => [ 'value' => 6, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Verify the customer order with tax, discount with shipping tax class set for calculation setting + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testCustomerOrdersSimpleProductWithTaxesAndDiscountsWithTwoRules() + { + $quantity = 4; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + // Asserting discounts on order item level + $this->assertEquals(4, $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $customerOrderResponse[0]['items'][0]['discounts'][0]['label'] + ); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsWithTaxesAndDiscountsWithTwoRules($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * @param array $customerOrderItemTotal + */ + private function assertTotalsWithTaxesAndDiscountsWithTwoRules(array $customerOrderItemTotal): void + { + $this->assertCount(2, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(4.05, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + $secondTaxData = $customerOrderItemTotal['taxes'][1]; + $this->assertEquals('USD', $secondTaxData['amount']['currency']); + $this->assertEquals(2.97, $secondTaxData['amount']['value']); + $this->assertEquals('US-AL-*-Rate-1', $secondTaxData['title']); + $this->assertEquals(5.5, $secondTaxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 61.02, 'currency' =>'USD'], + 'grand_total' => ['value' => 61.02, 'currency' =>'USD'], + 'subtotal' => ['value' => 40, 'currency' =>'USD'], + 'total_tax' => ['value' => 7.02, 'currency' =>'USD'], + 'total_shipping' => ['value' => 20, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 22.6], + 'amount_excluding_tax' => ['value' => 20], + 'total_amount' => ['value' => 20, 'currency' =>'USD'], + 'discounts' => [ + 0 => ['amount'=>['value'=> 2, 'currency' =>'USD']] + ], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 1.35], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ], + 1 => [ + 'amount'=>['value' => 0.99], + 'title' => 'US-AL-*-Rate-1', + 'rate' => 5.5 + ] + ] + ], + 'discounts' => [ + 0 => ['amount' => [ 'value' => 6, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetMatchingCustomerOrders() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{match:"100"}}){ + total_count + page_info{ + total_pages + current_page + page_size + } + items + { + id + number + status + order_date + items{ + quantity_ordered + product_sku + product_name + product_type + product_sale_price{currency value} + product_url_key + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(6, $response['customer']['orders']['total_count']); + $this->assertCount(6, $response['customer']['orders']['items']); + $customerOrderItems = $response['customer']['orders']['items']; + $expectedOrderNumbers = ['100000002', '100000004', '100000005','100000006', '100000007', '100000008']; + $actualOrdersFromResponse = []; + foreach ($customerOrderItems as $order) { + array_push($actualOrdersFromResponse, $order['number']); + } + $this->assertEquals($expectedOrderNumbers, $actualOrdersFromResponse, 'Order numbers do not match'); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetMatchingOrdersForLowerQueryLength() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{match:"0"}}){ + total_count + page_info{ + total_pages + current_page + page_size + } + items + { + id + number + status + order_date + items{ + quantity_ordered + product_sku + product_name + } + } + } +} +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + //character length should not trigger an exception + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(6, $response['customer']['orders']['total_count']); + $this->assertCount($response['customer']['orders']['total_count'], $response['customer']['orders']['items']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testGetMultipleCustomerOrdersQueryWithDefaultPagination() + { + $orderNumbers = ['100000007', '100000008']; + $query = <<<QUERY +{ + customer + { + orders(filter:{number:{in:["{$orderNumbers[0]}","{$orderNumbers[1]}"]}}){ + total_count + page_info{ + total_pages + current_page + page_size + } + items + { + id + number + status + order_date + items{ + quantity_ordered + product_sku + product_name + product_type + product_sale_price{currency value} + } + total{ + base_grand_total {value currency} + grand_total {value currency} + subtotal {value currency} + total_shipping{value} + total_tax{value currency} + taxes {amount {currency value} title rate} + total_shipping{value} + shipping_handling{ + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value} + taxes {amount{value} title rate} + } + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayNotHasKey('errors', $response); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(2, $response['customer']['orders']['total_count']); + $this->assertArrayHasKey('page_info', $response['customer']['orders']); + $pageInfo = $response['customer']['orders']['page_info']; + $this->assertEquals(1, $pageInfo['current_page']); + $this->assertEquals(20, $pageInfo['page_size']); + $this->assertEquals(1, $pageInfo['total_pages']); + $this->assertNotEmpty($response['customer']['orders']['items']); + $customerOrderItemsInResponse = $response['customer']['orders']['items']; + $this->assertCount(2, $response['customer']['orders']['items']); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('increment_id', $orderNumbers, 'in') + ->create(); + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + $key = 0; + foreach ($orders as $order) { + $orderId = base64_encode($order->getEntityId()); + $orderNumber = $order->getIncrementId(); + $orderItemInResponse = $customerOrderItemsInResponse[$key]; + $this->assertNotEmpty($orderItemInResponse['id']); + $this->assertEquals($orderId, $orderItemInResponse['id']); + $this->assertEquals($orderNumber, $orderItemInResponse['number']); + $this->assertEquals('Processing', $orderItemInResponse['status']); + $this->assertEquals(5, $orderItemInResponse['total']['shipping_handling']['total_amount']['value']); + $this->assertEquals(5, $orderItemInResponse['total']['total_shipping']['value']); + $this->assertEquals(5, $orderItemInResponse['total']['total_tax']['value']); + $key++; + } + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Sales/_files/orders_with_customer.php + */ + public function testGetCustomerOrdersUnauthorizedCustomer() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{eq:"100000001"}}){ + total_count + items + { + id + number + status + order_date + } + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The current customer isn\'t authorized.'); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + * @magentoApiDataFixture Magento/Sales/_files/two_orders_for_two_diff_customers.php + */ + public function testGetCustomerOrdersWithWrongCustomer() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{eq:"100000001"}}){ + total_count + items + { + id + number + status + order_date + } + } + } +} +QUERY; + $currentEmail = 'customer_two@example.com'; + $currentPassword = 'password'; + $responseWithWrongCustomer = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertEquals(0, $responseWithWrongCustomer['customer']['orders']['total_count']); + $this->assertEmpty($responseWithWrongCustomer['customer']['orders']['items']); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $responseWithCorrectCustomer = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertEquals(1, $responseWithCorrectCustomer['customer']['orders']['total_count']); + $this->assertNotEmpty($responseWithCorrectCustomer['customer']['orders']['items']); + } + + /** + * @param String $orderNumber + * @throws AuthenticationException + * @dataProvider dataProviderIncorrectOrder + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetCustomerNonExistingOrderQuery(string $orderNumber) + { + $query = + <<<QUERY +{ + customer { + orders(filter: {number: {eq: "{$orderNumber}"}}) { + items { + number + items { + product_sku + } + total { + base_grand_total { + value + currency + } + grand_total { + value + currency + } + total_shipping { + value + } + shipping_handling { + amount_including_tax { + value + } + amount_excluding_tax { + value + } + total_amount { + value + } + taxes { + amount { + value + } + title + rate + } + } + subtotal { + value + currency + } + taxes { + amount { + value + currency + } + title + rate + } + discounts { + amount { + value + currency + } + label + } + } + } + page_info { + current_page + page_size + total_pages + } + total_count + } + } +} + +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayNotHasKey('errors', $response); + $this->assertArrayHasKey('customer', $response); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertCount(0, $response['customer']['orders']['items']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(0, $response['customer']['orders']['total_count']); + $this->assertArrayHasKey('page_info', $response['customer']['orders']); + $this->assertEquals( + ['current_page' => 1, 'page_size' => 20, 'total_pages' => 0], + $response['customer']['orders']['page_info'] + ); + } + + /** + * @return array + */ + public function dataProviderIncorrectOrder(): array + { + return [ + 'correctFormatNonExistingOrder' => [ + '200000009', + ], + 'alphaFormatNonExistingOrder' => [ + '200AA00B9', + ], + 'longerFormatNonExistingOrder' => [ + 'X0000-0033331', + ], + ]; + } + + /** + * @param String $orderNumber + * @param String $store + * @param int $expectedCount + * @throws AuthenticationException + * @dataProvider dataProviderMultiStores + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php + */ + public function testGetCustomerOrdersTwoStoreViewQuery(string $orderNumber, string $store, int $expectedCount) + { + $query = + <<<QUERY +{ + customer { + orders(filter:{number:{eq:"{$orderNumber}"}}) { + page_info {current_page page_size total_pages} + total_count + items { + number + items{ product_sku } + total { + base_grand_total{value currency} + grand_total{value currency} + subtotal { value currency } + shipping_handling + { + total_amount{value currency} + } + } + } + } + } + } +QUERY; + + $headers = array_merge( + $this->customerAuthenticationHeader->execute('customer@example.com', 'password'), + ['Store' => $store] + ); + $response = $this->graphQlQuery($query, [], '', $headers); + $this->assertArrayHasKey('customer', $response); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertCount($expectedCount, $response['customer']['orders']['items']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals($expectedCount, (int)$response['customer']['orders']['total_count']); + $this->assertTotals($response, $expectedCount); + } + + /** + * @param array $response + * @param int $expectedCount + */ + private function assertTotals(array $response, int $expectedCount): void + { + $assertionMap = [ + 'base_grand_total' => ['value' => 100, 'currency' =>'USD'], + 'grand_total' => ['value' => 100, 'currency' =>'USD'], + 'subtotal' => ['value' => 110, 'currency' =>'USD'], + 'shipping_handling' => [ + 'total_amount' => ['value' => 10, 'currency' =>'USD'] + ] + ]; + if ($expectedCount === 0) { + $this->assertEmpty($response['customer']['orders']['items']); + } else { + $customerOrderItemTotal = $response['customer']['orders']['items'][0]['total']; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + } + + /** + * @return array + */ + public function dataProviderMultiStores(): array + { + return [ + 'firstStoreFirstOrder' => [ + '100000001', 'default', 1 + ], + 'secondStoreSecondOrder' => [ + '100000002', 'fixture_second_store', 1 + ], + 'firstStoreSecondOrder' => [ + '100000002', 'default', 0 + ], + 'secondStoreFirstOrder' => [ + '100000001', 'fixture_second_store', 0 + ], + ]; + } + + /** + * Verify that the customer order has the tax information on shipping and totals + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testCustomerOrderWithTaxesExcludedOnShipping() + { + $quantity = 2; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsAndShippingWithExcludedTaxSetting($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * Assert totals and shipping amounts with taxes excluded + * + * @param $customerOrderItemTotal + */ + private function assertTotalsAndShippingWithExcludedTaxSetting($customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(2.25, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'total_tax' => ['value' => 2.25, 'currency' =>'USD'], + 'subtotal' => ['value' => 20, 'currency' =>'USD'], + 'discounts' => [], + 'total_shipping' => ['value' => 10, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 10.75], + 'amount_excluding_tax' => ['value' => 10], + 'total_amount' => ['value' => 10, 'currency' =>'USD'], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 0.75], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ], + 'discounts' =>[] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Verify that the customer order has the tax information on shipping and totals + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php + */ + public function testCustomerOrderWithTaxesIncludedOnShippingAndTotals() + { + $quantity = 2; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsAndShippingWithTaxes($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * Check order totals an shipping amounts with taxes + * + * @param array $customerOrderItemTotal + */ + private function assertTotalsAndShippingWithTaxes(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(2.25, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + unset($customerOrderItemTotal['shipping_handling']['discounts']); + $assertionMap = [ + 'base_grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'total_tax' => ['value' => 2.25, 'currency' =>'USD'], + 'subtotal' => ['value' => 20, 'currency' =>'USD'], + 'total_shipping' => ['value' => 10, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 10.75], + 'amount_excluding_tax' => ['value' => 10], + 'total_amount' => ['value' => 10, 'currency' =>'USD'], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 0.75], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Create an empty cart with GraphQl mutation + * + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['createEmptyCart']; + } + + /** + * Add product to cart with GraphQl query + * + * @param string $cartId + * @param float $qty + * @param string $sku + * @return void + */ + private function addProductToCart(string $cartId, float $qty, string $sku): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$cartId}" + cart_items: [ + { + data: { + quantity: {$qty} + sku: "{$sku}" + } + } + ] + } + ) { + cart {items{quantity product {sku}}}} +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Set billing address on cart with GraphQL mutation + * + * @param string $cartId + * @return void + */ + private function setBillingAddress(string $cartId): void + { + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$cartId}" + billing_address: { + address: { + firstname: "John" + lastname: "Smith" + company: "Test company" + street: ["test street 1", "test street 2"] + city: "Texas City" + postcode: "78717" + telephone: "5123456677" + region: "TX" + country_code: "US" + } + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Set shipping address on cart with GraphQl query + * + * @param string $cartId + * @return array + */ + private function setShippingAddress(string $cartId): array + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "test shipFirst" + lastname: "test shipLast" + company: "test company" + street: ["test street 1", "test street 2"] + city: "Montgomery" + region: "AL" + postcode: "36013" + country_code: "US" + telephone: "3347665522" + } + } + ] + } + ) { + cart { + shipping_addresses { + available_shipping_methods { + carrier_code + method_code + amount {value} + } + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $shippingAddress = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + $availableShippingMethod = current($shippingAddress['available_shipping_methods']); + return $availableShippingMethod; + } + + /** + * Set shipping method on cart with GraphQl mutation + * + * @param string $cartId + * @param array $method + * @return array + */ + private function setShippingMethod(string $cartId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + return $availablePaymentMethod; + } + + /** + * Set payment method on cart with GrpahQl mutation + * + * @param string $cartId + * @param array $method + * @return void + */ + private function setPaymentMethod(string $cartId, array $method): void + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$cartId}" + payment_method: { + code: "{$method['code']}" + } + } + ) { + cart {selected_payment_method {code}} + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Place order using GraphQl mutation + * + * @param string $cartId + * @return string + */ + private function placeOrder(string $cartId): string + { + $query = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$cartId}" + } + ) { + order { + order_number + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['placeOrder']['order']['order_number']; + } + + /** + * Get customer order query + * + * @param string $orderNumber + * @return array + */ + private function getCustomerOrderQuery($orderNumber): array + { + $query = + <<<QUERY +{ + customer { + email + orders(filter:{number:{eq:"{$orderNumber}"}}) { + total_count + items { + id + number + order_date + status + payment_methods + { + name + type + additional_data + { + name + value + } + } + shipping_address { + ... address + } + billing_address { + ... address + } + items{product_name product_sku quantity_ordered discounts {amount{value currency} label}} + total { + base_grand_total{value currency} + grand_total{value currency} + total_tax{value currency} + subtotal { value currency } + taxes {amount{value currency} title rate} + discounts {amount{value currency} label} + total_shipping{value currency} + shipping_handling + { + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value currency} + taxes {amount{value} title rate} + discounts {amount{value currency}} + } + + } + } + } + } + } + + fragment address on OrderAddress { + firstname + lastname + city + company + country_code + fax + middlename + postcode + street + region + region_id + telephone + vat_id + } +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + return $response['customer']['orders']['items']; + } + + /** + * Get customer order query + * + * @param string $orderNumber + * @return array + */ + private function getCustomerOrderQueryOnSimpleProducts($orderNumber): array + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{eq:"{$orderNumber}"}}) { + total_count + items + { + id + number + status + order_date + payment_methods + { + name + type + additional_data + { + name + value + } + } + shipping_address { + ... address + } + billing_address { + ... address + } + items{ + quantity_ordered + product_sku + product_name + product_sale_price{currency value} + } + total { + base_grand_total { + value + currency + } + grand_total { + value + currency + } + subtotal { + value + currency + } + } + } + } + } +} + +fragment address on OrderAddress { + firstname + lastname + city + company + country_code + fax + middlename + postcode + street + region + region_id + telephone + vat_id + } +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + return $response; + } + + /** + * Clean up orders + * + * @return void + */ + private function deleteOrder(): void + { + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(Collection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php new file mode 100644 index 0000000000000..b4c9bd4962cc2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php @@ -0,0 +1,310 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\GraphQl\Sales\Fixtures\CustomerPlaceOrder; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for orders with bundle product + */ +class RetrieveOrdersWithBundleProductByOrderNumberTest extends GraphQlAbstract +{ + /** @var OrderRepositoryInterface */ + private $orderRepository; + + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + protected function setUp():void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->customerAuthenticationHeader = $objectManager->get(GetCustomerAuthenticationHeader::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + protected function tearDown(): void + { + $this->deleteOrder(); + } + + /** + * Test customer order details with bundle product with child items + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + */ + public function testGetCustomerOrderBundleProduct() + { + //Place order with bundled product + $qty = 1; + $bundleSku = 'bundle-product-two-dropdown-options'; + /** @var CustomerPlaceOrder $bundleProductOrderFixture */ + $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class); + $orderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => $bundleSku, 'quantity' => $qty] + ); + $orderNumber = $orderResponse['placeOrder']['order']['order_number']; + //End place order with bundled product + + $customerOrderResponse = $this->getCustomerOrderQueryBundleProduct($orderNumber); + $customerOrderItems = $customerOrderResponse[0]; + $this->assertEquals("Pending", $customerOrderItems['status']); + $bundledItemInTheOrder = $customerOrderItems['items'][0]; + $this->assertEquals( + 'bundle-product-two-dropdown-options-simple1-simple2', + $bundledItemInTheOrder['product_sku'] + ); + $priceOfBundledItemInOrder = $bundledItemInTheOrder['product_sale_price']['value']; + $this->assertEquals(15, $priceOfBundledItemInOrder); + $this->assertArrayHasKey('bundle_options', $bundledItemInTheOrder); + $bundleOptionsFromResponse = $bundledItemInTheOrder['bundle_options']; + $this->assertNotEmpty($bundleOptionsFromResponse); + $this->assertEquals(2, count($bundleOptionsFromResponse)); + $expectedBundleOptions = + [ + [ '__typename' => 'ItemSelectedBundleOption', + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_sku' => 'simple1', + 'product_name' => 'Simple Product1', + 'quantity'=> 1, + 'price' => [ + 'value' => 1, + 'currency' => 'USD' + ] + ] + ] + ], + [ '__typename' => 'ItemSelectedBundleOption', + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_sku' => 'simple2', + 'product_name' => 'Simple Product2', + 'quantity'=> 2, + 'price' => [ + 'value' => 2, + 'currency' => 'USD' + ] + ] + ] + ], + ]; + $this->assertEquals($expectedBundleOptions, $bundleOptionsFromResponse); + } + + /** + * Test customer order details with bundle products + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testGetCustomerOrderBundleProductWithTaxesAndDiscounts() + { + //Place order with bundled product + $qty = 4; + $bundleSku = 'bundle-product-two-dropdown-options'; + /** @var CustomerPlaceOrder $bundleProductOrderFixture */ + $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class); + $orderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => $bundleSku, 'quantity' => $qty] + ); + $orderNumber = $orderResponse['placeOrder']['order']['order_number']; + //End place order with bundled product + + $customerOrderResponse = $this->getCustomerOrderQueryBundleProduct($orderNumber); + $customerOrderItems = $customerOrderResponse[0]; + $this->assertEquals("Pending", $customerOrderItems['status']); + + $bundledItemInTheOrder = $customerOrderItems['items'][0]; + $this->assertEquals( + 'bundle-product-two-dropdown-options-simple1-simple2', + $bundledItemInTheOrder['product_sku'] + ); + $this->assertEquals(6, $bundledItemInTheOrder['discounts'][0]['amount']['value']); + $this->assertEquals( + 'Discount Label for 10% off', + $bundledItemInTheOrder["discounts"][0]['label'] + ); + $this->assertArrayHasKey('bundle_options', $bundledItemInTheOrder); + $childItemsInTheOrder = $bundledItemInTheOrder['bundle_options']; + $this->assertNotEmpty($childItemsInTheOrder); + $this->assertCount(2, $childItemsInTheOrder); + $this->assertEquals('Drop Down Option 1', $childItemsInTheOrder[0]['label']); + $this->assertEquals('Drop Down Option 2', $childItemsInTheOrder[1]['label']); + + $this->assertEquals('simple1', $childItemsInTheOrder[0]['values'][0]['product_sku']); + $this->assertEquals('simple2', $childItemsInTheOrder[1]['values'][0]['product_sku']); + $this->assertTotalsOnBundleProductWithTaxesAndDiscounts($customerOrderItems['total']); + } + + /** + * @param array $customerOrderItemTotal + */ + private function assertTotalsOnBundleProductWithTaxesAndDiscounts(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(5.4, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 77.4, 'currency' =>'USD'], + 'grand_total' => ['value' => 77.4, 'currency' =>'USD'], + 'subtotal' => ['value' => 60, 'currency' =>'USD'], + 'total_tax' => ['value' => 5.4, 'currency' =>'USD'], + 'total_shipping' => ['value' => 20, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 21.5], + 'amount_excluding_tax' => ['value' => 20], + 'total_amount' => ['value' => 20], + 'discounts' => [ + 0 => ['amount'=>['value'=> 2]] + ], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 1.35], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ] + ], + 'discounts' => [ + 0 => ['amount' => [ 'value' => 8, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Get customer order query for bundle order items + * + * @param $orderNumber + * @return mixed + * @throws AuthenticationException + */ + private function getCustomerOrderQueryBundleProduct($orderNumber) + { + $query = + <<<QUERY +{ + customer { + orders(filter:{number:{eq:"{$orderNumber}"}}) { + total_count + items { + id + number + order_date + status + items{ + __typename + product_sku + product_name + product_url_key + product_sale_price{value} + quantity_ordered + discounts{amount{value} label} + ... on BundleOrderItem{ + bundle_options{ + __typename + label + values { + product_sku + product_name + quantity + price { + value + currency + } + } + } + } + } + total { + base_grand_total{value currency} + grand_total{value currency} + subtotal {value currency } + total_tax{value currency} + taxes {amount{value currency} title rate} + total_shipping{value currency} + shipping_handling + { + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value} + discounts{amount{value}} + taxes {amount{value} title rate} + } + discounts {amount{value currency} label} + } + } + } + } + } +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $customerOrderItemsInResponse = $response['customer']['orders']['items']; + return $customerOrderItemsInResponse; + } + + /** + * @return void + */ + private function deleteOrder(): void + { + /** @var \Magento\Framework\Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(Collection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php index 337068710c31b..040215a241c47 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php @@ -122,7 +122,9 @@ public function testSendFriendDisableAsCustomer() public function testSendWithoutExistProduct() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The product that was requested doesn\'t exist. Verify the product and try again.'); + $this->expectExceptionMessage( + 'The product that was requested doesn\'t exist. Verify the product and try again.' + ); $productId = 2018; $recipients = '{ @@ -290,81 +292,124 @@ public function testSendProductWithoutVisibility() /** * @return array */ - public function sendFriendsErrorsDataProvider() + public function sendFriendsErrorsDataProvider(): array + { + return array_merge( + $this->getRecipientErrors(), + $this->getSenderErrors() + ); + } + + /** + * @return array + */ + private function getRecipientErrors(): array { return [ [ - 'product_id: 1 - sender: { - name: "Name" - email: "e@mail.com" - message: "Lorem Ipsum" - } - recipients: [ - { - name: "" - email:"recipient1@mail.com" - }, - { - name: "" - email:"recipient2@mail.com" - } - ]', 'Please provide Name for all of recipients.' + 'product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "" + email:"recipient1@mail.com" + }, + { + name: "" + email:"recipient2@mail.com" + } + ]', + 'Please provide Name for all of recipients.' ], [ 'product_id: 1 - sender: { - name: "Name" - email: "e@mail.com" - message: "Lorem Ipsum" - } - recipients: [ - { - name: "Recipient Name 1" - email:"" - }, - { - name: "Recipient Name 2" - email:"" - } - ]', 'Please provide Email for all of recipients.' + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"" + }, + { + name: "Recipient Name 2" + email:"" + } + ]', + 'Please provide Email for all of recipients.' + ], + ]; + } + + /** + * @return array + */ + private function getSenderErrors(): array + { + return [ + [ + 'product_id: 1 + sender: { + name: "" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ]', + 'Please provide Name of sender.' ], [ 'product_id: 1 - sender: { - name: "" - email: "e@mail.com" - message: "Lorem Ipsum" - } - recipients: [ - { - name: "Recipient Name 1" - email:"recipient1@mail.com" - }, - { - name: "Recipient Name 2" - email:"recipient2@mail.com" - } - ]', 'Please provide Name of sender.' + sender: { + name: "Name" + email: "" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ]', + 'Please provide Email of sender.' ], [ 'product_id: 1 - sender: { - name: "Name" - email: "e@mail.com" - message: "" - } - recipients: [ - { - name: "Recipient Name 1" - email:"recipient1@mail.com" - }, - { - name: "Recipient Name 2" - email:"recipient2@mail.com" - } - ]', 'Please provide Message.' - ] + sender: { + name: "Name" + email: "e@mail.com" + message: "" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ]', + 'Please provide Message.' + ], ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php new file mode 100644 index 0000000000000..d762462729234 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php @@ -0,0 +1,267 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Store; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\Data\StoreConfigInterface; +use Magento\Store\Api\StoreConfigManagerInterface; +use Magento\Store\Model\ResourceModel\Store as StoreResource; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the GraphQL endpoint's AvailableStores query + */ +class AvailableStoreConfigTest extends GraphQlAbstract +{ + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var StoreConfigManagerInterface + */ + private $storeConfigManager; + + /** + * @var StoreResource + */ + private $storeResource; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->storeConfigManager = $this->objectManager->get(StoreConfigManagerInterface::class); + $this->storeResource = $this->objectManager->get(StoreResource::class); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoApiDataFixture Magento/Store/_files/inactive_store.php + */ + public function testDefaultWebsiteAvailableStoreConfigs(): void + { + $storeConfigs = $this->storeConfigManager->getStoreConfigs(); + + $expectedAvailableStores = []; + $expectedAvailableStoreCodes = [ + 'default', + 'test' + ]; + + foreach ($storeConfigs as $storeConfig) { + if (in_array($storeConfig->getCode(), $expectedAvailableStoreCodes)) { + $expectedAvailableStores[] = $storeConfig; + } + } + + $query + = <<<QUERY +{ + availableStores { + id, + code, + website_id, + locale, + base_currency_code, + default_display_currency_code, + timezone, + weight_unit, + base_url, + base_link_url, + base_static_url, + base_media_url, + secure_base_url, + secure_base_link_url, + secure_base_static_url, + secure_base_media_url, + store_name + use_store_in_url + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('availableStores', $response); + foreach ($expectedAvailableStores as $key => $storeConfig) { + $this->validateStoreConfig($storeConfig, $response['availableStores'][$key]); + } + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + */ + public function testNonDefaultWebsiteAvailableStoreConfigs(): void + { + $storeConfigs = $this->storeConfigManager->getStoreConfigs(['fixture_second_store', 'fixture_third_store']); + + $query + = <<<QUERY +{ + availableStores { + id, + code, + website_id, + locale, + base_currency_code, + default_display_currency_code, + timezone, + weight_unit, + base_url, + base_link_url, + base_static_url, + base_media_url, + secure_base_url, + secure_base_link_url, + secure_base_static_url, + secure_base_media_url, + store_name + use_store_in_url + } +} +QUERY; + $headerMap = ['Store' => 'fixture_second_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + $this->assertArrayHasKey('availableStores', $response); + foreach ($storeConfigs as $key => $storeConfig) { + $this->validateStoreConfig($storeConfig, $response['availableStores'][$key]); + } + } + + /** + * Validate Store Config Data + * + * @param StoreConfigInterface $storeConfig + * @param array $responseConfig + */ + private function validateStoreConfig(StoreConfigInterface $storeConfig, array $responseConfig): void + { + $store = $this->objectManager->get(Store::class); + $this->storeResource->load($store, $storeConfig->getCode(), 'code'); + $this->assertEquals($storeConfig->getId(), $responseConfig['id']); + $this->assertEquals($storeConfig->getCode(), $responseConfig['code']); + $this->assertEquals($storeConfig->getLocale(), $responseConfig['locale']); + $this->assertEquals($storeConfig->getBaseCurrencyCode(), $responseConfig['base_currency_code']); + $this->assertEquals( + $storeConfig->getDefaultDisplayCurrencyCode(), + $responseConfig['default_display_currency_code'] + ); + $this->assertEquals($storeConfig->getTimezone(), $responseConfig['timezone']); + $this->assertEquals($storeConfig->getWeightUnit(), $responseConfig['weight_unit']); + $this->assertEquals($storeConfig->getBaseUrl(), $responseConfig['base_url']); + $this->assertEquals($storeConfig->getBaseLinkUrl(), $responseConfig['base_link_url']); + $this->assertEquals($storeConfig->getBaseStaticUrl(), $responseConfig['base_static_url']); + $this->assertEquals($storeConfig->getBaseMediaUrl(), $responseConfig['base_media_url']); + $this->assertEquals($storeConfig->getSecureBaseUrl(), $responseConfig['secure_base_url']); + $this->assertEquals($storeConfig->getSecureBaseLinkUrl(), $responseConfig['secure_base_link_url']); + $this->assertEquals($storeConfig->getSecureBaseStaticUrl(), $responseConfig['secure_base_static_url']); + $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $responseConfig['secure_base_media_url']); + $this->assertEquals($store->getName(), $responseConfig['store_name']); + $this->assertEquals($store->isUseStoreInUrl(), $responseConfig['use_store_in_url']); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_website_with_four_stores_divided_in_groups.php + * @magentoConfigFixture web/url/use_store 1 + */ + public function testAllStoreConfigsWithCodeInUrlEnabled(): void + { + $storeConfigs = $this->storeConfigManager->getStoreConfigs( + [ + 'fixture_second_store', + 'fixture_third_store', + 'fixture_fourth_store', + 'fixture_fifth_store' + ] + ); + + $query + = <<<QUERY +{ + availableStores(useCurrentGroup:false) { + id, + code, + website_id, + locale, + base_currency_code, + default_display_currency_code, + timezone, + weight_unit, + base_url, + base_link_url, + base_static_url, + base_media_url, + secure_base_url, + secure_base_link_url, + secure_base_static_url, + secure_base_media_url, + store_name + use_store_in_url + } +} +QUERY; + $headerMap = ['Store' => 'fixture_fifth_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + $this->assertArrayHasKey('availableStores', $response); + $this->assertCount(4, $response['availableStores']); + foreach ($response['availableStores'] as $key => $responseConfig) { + $this->validateStoreConfig($storeConfigs[$key], $responseConfig); + $this->assertEquals(true, $responseConfig['use_store_in_url']); + } + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_website_with_four_stores_divided_in_groups.php + */ + public function testCurrentGroupStoreConfigs(): void + { + $storeConfigs = $this->storeConfigManager->getStoreConfigs(['fixture_fourth_store', 'fixture_fifth_store']); + + $query + = <<<QUERY +{ + availableStores(useCurrentGroup:true) { + id, + code, + website_id, + locale, + base_currency_code, + default_display_currency_code, + timezone, + weight_unit, + base_url, + base_link_url, + base_static_url, + base_media_url, + secure_base_url, + secure_base_link_url, + secure_base_static_url, + secure_base_media_url, + store_name + use_store_in_url + } +} +QUERY; + $headerMap = ['Store' => 'fixture_fifth_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + $this->assertArrayHasKey('availableStores', $response); + $this->assertCount(2, $response['availableStores']); + foreach ($response['availableStores'] as $key => $responseConfig) { + $this->validateStoreConfig($storeConfigs[$key], $responseConfig); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php index 48619d1392309..cc8a60cf0937a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php @@ -7,10 +7,12 @@ namespace Magento\GraphQl\Store; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Api\Data\StoreConfigInterface; use Magento\Store\Api\StoreConfigManagerInterface; use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Api\StoreResolverInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -20,34 +22,37 @@ class StoreConfigResolverTest extends GraphQlAbstract { - /** @var ObjectManager */ + /** @var ObjectManager */ private $objectManager; + /** + * @inheritDoc + */ protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); } /** * @magentoApiDataFixture Magento/Store/_files/store.php - * @magentoConfigFixture default_store store/information/name Test Store + * @throws NoSuchEntityException */ - public function testGetStoreConfig() + public function testGetStoreConfig(): void { - /** @var StoreConfigManagerInterface $storeConfigsManager */ - $storeConfigsManager = $this->objectManager->get(StoreConfigManagerInterface::class); + /** @var StoreConfigManagerInterface $storeConfigManager */ + $storeConfigManager = $this->objectManager->get(StoreConfigManagerInterface::class); /** @var StoreResolverInterface $storeResolver */ $storeResolver = $this->objectManager->get(StoreResolverInterface::class); /** @var StoreRepositoryInterface $storeRepository */ $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); $storeId = $storeResolver->getCurrentStoreId(); $store = $storeRepository->getById($storeId); - /** @var StoreConfigInterface $storeConfig */ - $storeConfig = current($storeConfigsManager->getStoreConfigs([$store->getCode()])); + /** @var StoreConfigInterface $defaultStoreConfig */ + $defaultStoreConfig = current($storeConfigManager->getStoreConfigs([$store->getCode()])); $query = <<<QUERY { - storeConfig{ + storeConfig { id, code, website_id, @@ -70,27 +75,39 @@ public function testGetStoreConfig() QUERY; $response = $this->graphQlQuery($query); $this->assertArrayHasKey('storeConfig', $response); - $this->assertEquals($storeConfig->getId(), $response['storeConfig']['id']); - $this->assertEquals($storeConfig->getCode(), $response['storeConfig']['code']); - $this->assertEquals($storeConfig->getLocale(), $response['storeConfig']['locale']); - $this->assertEquals($storeConfig->getBaseCurrencyCode(), $response['storeConfig']['base_currency_code']); + $this->validateStoreConfig($defaultStoreConfig, $response['storeConfig'], $store->getName()); + } + + /** + * Validate Store Config Data + * + * @param StoreConfigInterface $storeConfig + * @param array $responseConfig + * @param string $storeName + */ + private function validateStoreConfig( + StoreConfigInterface $storeConfig, + array $responseConfig, + string $storeName + ): void { + $this->assertEquals($storeConfig->getId(), $responseConfig['id']); + $this->assertEquals($storeConfig->getCode(), $responseConfig['code']); + $this->assertEquals($storeConfig->getLocale(), $responseConfig['locale']); + $this->assertEquals($storeConfig->getBaseCurrencyCode(), $responseConfig['base_currency_code']); $this->assertEquals( $storeConfig->getDefaultDisplayCurrencyCode(), - $response['storeConfig']['default_display_currency_code'] - ); - $this->assertEquals($storeConfig->getTimezone(), $response['storeConfig']['timezone']); - $this->assertEquals($storeConfig->getWeightUnit(), $response['storeConfig']['weight_unit']); - $this->assertEquals($storeConfig->getBaseUrl(), $response['storeConfig']['base_url']); - $this->assertEquals($storeConfig->getBaseLinkUrl(), $response['storeConfig']['base_link_url']); - $this->assertEquals($storeConfig->getBaseStaticUrl(), $response['storeConfig']['base_static_url']); - $this->assertEquals($storeConfig->getBaseMediaUrl(), $response['storeConfig']['base_media_url']); - $this->assertEquals($storeConfig->getSecureBaseUrl(), $response['storeConfig']['secure_base_url']); - $this->assertEquals($storeConfig->getSecureBaseLinkUrl(), $response['storeConfig']['secure_base_link_url']); - $this->assertEquals( - $storeConfig->getSecureBaseStaticUrl(), - $response['storeConfig']['secure_base_static_url'] + $responseConfig['default_display_currency_code'] ); - $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $response['storeConfig']['secure_base_media_url']); - $this->assertEquals('Test Store', $response['storeConfig']['store_name']); + $this->assertEquals($storeConfig->getTimezone(), $responseConfig['timezone']); + $this->assertEquals($storeConfig->getWeightUnit(), $responseConfig['weight_unit']); + $this->assertEquals($storeConfig->getBaseUrl(), $responseConfig['base_url']); + $this->assertEquals($storeConfig->getBaseLinkUrl(), $responseConfig['base_link_url']); + $this->assertEquals($storeConfig->getBaseStaticUrl(), $responseConfig['base_static_url']); + $this->assertEquals($storeConfig->getBaseMediaUrl(), $responseConfig['base_media_url']); + $this->assertEquals($storeConfig->getSecureBaseUrl(), $responseConfig['secure_base_url']); + $this->assertEquals($storeConfig->getSecureBaseLinkUrl(), $responseConfig['secure_base_link_url']); + $this->assertEquals($storeConfig->getSecureBaseStaticUrl(), $responseConfig['secure_base_static_url']); + $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $responseConfig['secure_base_media_url']); + $this->assertEquals($storeName, $responseConfig['store_name']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreSaveTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreSaveTest.php new file mode 100644 index 0000000000000..7c7e21138bc26 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreSaveTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Store; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Store save tests + * + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + */ +class StoreSaveTest extends GraphQlAbstract +{ + /** + * Test a product from newly created store + * + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * @magentoApiDataFixture Magento/Catalog/_files/category_product.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testProductVisibleInNewStore() + { + $newStoreCode = 'fixture_second_store'; + $this->assertCategory($newStoreCode); + $this->assertProduct($newStoreCode); + } + + /** + * Test product in store. + * + * @param string $storeCodeFromFixture + * @throws \Exception + */ + private function assertProduct(string $storeCodeFromFixture) + { + $productSku = 'simple333'; + $productNameInFixtureStore = 'Simple Product Three'; + + $productsQuery = <<<QUERY +{ + products(filter: { sku: { eq: "%s" } }, sort: { name: ASC }) { + items { + id + sku + name + } + } +} +QUERY; + $headerMap = ['Store' => $storeCodeFromFixture]; + $response = $this->graphQlQuery( + sprintf($productsQuery, $productSku), + [], + '', + $headerMap + ); + $this->assertCount( + 1, + $response['products']['items'], + sprintf('Product with sku "%s" not found in store "%s"', $productSku, $storeCodeFromFixture) + ); + $this->assertEquals( + $productNameInFixtureStore, + $response['products']['items'][0]['name'], + 'Product name in fixture store is invalid.' + ); + } + + /** + * Test category in store. + * + * @param string $storeCodeFromFixture + * @throws \Exception + */ + private function assertCategory(string $storeCodeFromFixture) + { + $categoryName = 'Category 1'; + $categoryQuery = <<<QUERY +{ + categoryList(filters: {name: {match: "%s"}}){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $headerMap = ['Store' => $storeCodeFromFixture]; + $response = $this->graphQlQuery( + sprintf($categoryQuery, $categoryName), + [], + '', + $headerMap + ); + $this->assertCount( + 1, + $response['categoryList'], + sprintf('Category with name "%s" not found in store "%s"', $categoryName, $storeCodeFromFixture) + ); + $this->assertEquals( + $categoryName, + $response['categoryList'][0]['name'], + 'Category name in fixture store is invalid.' + ); + } +} 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 new file mode 100644 index 0000000000000..b97cd379e4384 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php @@ -0,0 +1,194 @@ +<?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\Bundle\Model\Option; +use Magento\Bundle\Model\Product\Type; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\AuthenticationException; +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; + +/** + * Test coverage for adding a bundle product to wishlist + */ +class AddBundleProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var mixed + */ + private $productRepository; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $objectManager->get(WishlistFactory::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/product_1.php + * + * @throws Exception + */ + public function testAddBundleProductWithOptions(): void + { + $sku = 'bundle-product'; + $product = $this->productRepository->get($sku); + $customerId = 1; + $qty = 2; + $optionQty = 1; + + /** @var Type $typeInstance */ + $typeInstance = $product->getTypeInstance(); + $typeInstance->setStoreFilter($product->getStoreId(), $product); + /** @var Option $option */ + $option = $typeInstance->getOptionsCollection($product)->getFirstItem(); + /** @var Product $selection */ + $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem(); + $optionId = $option->getId(); + $selectionId = $selection->getSelectionId(); + $bundleOptions = $this->generateBundleOptionUid((int) $optionId, (int) $selectionId, $optionQty); + + $query = $this->getQuery($sku, $qty, $bundleOptions); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $wishlist = $this->wishlistFactory->create()->loadByCustomerId($customerId, true); + /** @var Item $item */ + $item = $wishlist->getItemCollection()->getFirstItem(); + + $this->assertArrayHasKey('addProductsToWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $response = $response['addProductsToWishlist']['wishlist']; + $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_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']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param string $sku + * @param int $qty + * @param string $bundleOptions + * @param int $wishlistId + * + * @return string + */ + private function getQuery( + string $sku, + int $qty, + string $bundleOptions, + int $wishlistId = 0 + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + sku: "{$sku}" + quantity: {$qty} + selected_options: [ + "{$bundleOptions}" + ] + } + ] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items_v2 { + id + description + quantity + added_at + ... on BundleWishlistItem { + bundle_options { + id + label + type + values { + id + label + quantity + price + } + } + } + } + } + } +} +MUTATION; + } + + /** + * @param int $optionId + * @param int $selectionId + * + * @param int $quantity + * + * @return 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 new file mode 100644 index 0000000000000..cffc5eb6f93c1 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php @@ -0,0 +1,234 @@ +<?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\Framework\ObjectManagerInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a configurable product to wishlist + */ +class AddConfigurableProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $objectManager->get(WishlistFactory::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * + * @throws Exception + */ + public function testAddConfigurableProductWithOptions(): void + { + $product = $this->getConfigurableProductInfo(); + $customerId = 1; + $qty = 2; + $attributeId = (int) $product['configurable_options'][0]['attribute_id']; + $valueIndex = $product['configurable_options'][0]['values'][0]['value_index']; + $childSku = $product['variants'][0]['product']['sku']; + $parentSku = $product['sku']; + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesUidQuery($attributeId, $valueIndex); + + $query = $this->getQuery($parentSku, $childSku, $qty, $selectedConfigurableOptionsQuery); + + $response = $this->graphQlMutation($query, [], '', $this->getHeadersMap()); + $wishlist = $this->wishlistFactory->create()->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]['configurable_options']); + $configurableOptions = $wishlistResponse['items_v2'][0]['configurable_options']; + $this->assertEquals('Test Configurable', $configurableOptions[0]['option_label']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeadersMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param string $parentSku + * @param string $childSku + * @param int $qty + * @param string $customizableOptions + * @param int $wishlistId + * + * @return string + */ + private function getQuery( + string $parentSku, + string $childSku, + int $qty, + string $customizableOptions, + int $wishlistId = 0 + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + sku: "{$childSku}" + parent_sku: "{$parentSku}" + quantity: {$qty} + {$customizableOptions} + } + ] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items_v2 { + id + description + quantity + added_at + ... on ConfigurableWishlistItem { + child_sku + configurable_options { + id + option_label + value_id + value_label + } + } + } + } + } +} +MUTATION; + } + + /** + * Generates uid for super configurable product super attributes + * + * @param int $attributeId + * @param int $valueIndex + * + * @return string + */ + private function generateSuperAttributesUidQuery(int $attributeId, int $valueIndex): string + { + return 'selected_options: ["' . base64_encode("configurable/$attributeId/$valueIndex") . '"]'; + } + + /** + * Returns information about testable configurable product retrieved from GraphQl query + * + * @return array + * + * @throws Exception + */ + private function getConfigurableProductInfo(): array + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable')); + + return current($searchResponse['products']['items']); + } + + /** + * Returns GraphQl query for fetching configurable product information + * + * @param string $term + * + * @return string + */ + private function getFetchProductQuery(string $term): string + { + return <<<QUERY +{ + products( + search:"{$term}" + pageSize:1 + ) { + items { + sku + ... on ConfigurableProduct { + variants { + product { + sku + } + } + configurable_options { + attribute_id + attribute_code + id + label + position + product_id + use_default + values { + default_label + label + store_label + use_default_value + value_index + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php new file mode 100644 index 0000000000000..0de45fb21b20b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php @@ -0,0 +1,234 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a downloadable product to wishlist + */ +class AddDownloadableProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var GetCustomOptionsWithUidForQueryBySku + */ + private $getCustomOptionsWithUidForQueryBySku; + + /** + * Set Up + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $this->objectManager->get(WishlistFactory::class); + $this->getCustomOptionsWithUidForQueryBySku = + $this->objectManager->get(GetCustomOptionsWithUidForQueryBySku::class); + } + + /** + * @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 testAddDownloadableProductOnDisabledWishlist(): void + { + $qty = 2; + $sku = 'downloadable-product-with-purchased-separately-links'; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + $itemOptions = $this->getCustomOptionsWithUidForQueryBySku->execute($sku); + $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); + $productOptionsQuery = trim(preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ), '{}'); + $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 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + */ + public function testAddDownloadableProductWithOptions(): void + { + $customerId = 1; + $sku = 'downloadable-product-with-purchased-separately-links'; + $qty = 2; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + $itemOptions = $this->getCustomOptionsWithUidForQueryBySku->execute($sku); + $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); + $productOptionsQuery = 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(); + + $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']); + } + + /** + * Function returns array of all product's links + * + * @param string $sku + * + * @return array + */ + private function getProductsLinks(string $sku): array + { + $result = []; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($sku, false, null, true); + + foreach ($product->getDownloadableLinks() as $linkObject) { + $result[$linkObject->getLinkId()] = [ + 'title' => $linkObject->getTitle(), + 'price' => $linkObject->getPrice(), + ]; + } + + return $result; + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * + * @return string + */ + private function getQuery( + int $qty, + string $sku, + string $customizableOptions + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: 0, + wishlistItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items_v2 { + id + description + quantity + added_at + ... on DownloadableWishlistItem { + links_v2 { + id + title + sample_url + } + samples { + id + title + sample_url + } + } + } + } + } +} +MUTATION; + } + + /** + * Generates uid for downloadable links + * + * @param int $linkId + * + * @return string + */ + private function generateProductLinkSelectedOptions(int $linkId): string + { + return base64_encode("downloadable/$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 2208f904320d9..04095c1679d2f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php @@ -32,6 +32,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php */ public function testCustomerWishlist(): void @@ -74,6 +75,7 @@ public function testCustomerWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Customer/_files/customer.php */ public function testCustomerAlwaysHasWishlist(): void @@ -100,6 +102,7 @@ public function testCustomerAlwaysHasWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 */ public function testGuestCannotGetWishlist() { @@ -121,6 +124,35 @@ public function testGuestCannotGetWishlist() $this->graphQlQuery($query); } + /** + * @magentoConfigFixture default_store wishlist/general/active 0 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCustomerCannotGetWishlistWhenDisabled() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The wishlist configuration is currently disabled.'); + + $query = + <<<QUERY +{ + customer { + wishlist { + items_count + sharing_code + updated_at + } + } +} +QUERY; + $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + } + /** * @param string $email * @param string $password 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 new file mode 100644 index 0000000000000..13aaecbc7b733 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for deleting a product from wishlist + */ +class DeleteProductsFromWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testDeleteWishlistItemFromWishlist(): void + { + $wishlist = $this->getWishlist(); + $wishlistId = $wishlist['customer']['wishlist']['id']; + $wishlist = $wishlist['customer']['wishlist']; + $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()); + + $this->assertArrayHasKey('removeProductsFromWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['removeProductsFromWishlist']); + $wishlistResponse = $response['removeProductsFromWishlist']['wishlist']; + $this->assertEquals(0, $wishlistResponse['items_count']); + $this->assertEmpty($wishlistResponse['items_v2']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $wishlistId + * @param int $wishlistItemId + * + * @return string + */ + private function getQuery( + int $wishlistId, + int $wishlistItemId + ): string { + return <<<MUTATION +mutation { + removeProductsFromWishlist( + wishlistId: {$wishlistId}, + wishlistItemsIds: [{$wishlistItemId}] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + items_v2 { + id + description + quantity + } + } + } +} +MUTATION; + } + + /** + * Get wishlist result + * + * @return array + * + * @throws Exception + */ + public function getWishlist(): array + { + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + } + + /** + * Get customer wishlist query + * + * @return string + */ + private function getCustomerWishlistQuery(): string + { + return <<<QUERY +query { + customer { + wishlist { + id + items_count + items_v2 { + id + quantity + description + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithUidForQueryBySku.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithUidForQueryBySku.php new file mode 100644 index 0000000000000..4bd0c135f039a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithUidForQueryBySku.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; + +/** + * Generate an array with test values for customizable options with encoded uid value + */ +class GetCustomOptionsWithUidForQueryBySku +{ + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $productCustomOptionRepository; + + /** + * @param ProductCustomOptionRepositoryInterface $productCustomOptionRepository + */ + public function __construct(ProductCustomOptionRepositoryInterface $productCustomOptionRepository) + { + $this->productCustomOptionRepository = $productCustomOptionRepository; + } + + /** + * Returns array of custom options for the product + * + * @param string $sku + * + * @return array + */ + public function execute(string $sku): array + { + $customOptions = $this->productCustomOptionRepository->getList($sku); + $selectedOptions = []; + $enteredOptions = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + + if ($optionType === 'field' || $optionType === 'area' || $optionType === 'date') { + $enteredOptions[] = [ + 'uid' => $this->encodeEnteredOption((int)$customOption->getOptionId()), + 'value' => '2012-12-12' + ]; + } elseif ($optionType === 'drop_down') { + $optionSelectValues = $customOption->getValues(); + $selectedOptions[] = $this->encodeSelectedOption( + (int)$customOption->getOptionId(), + (int)reset($optionSelectValues)->getOptionTypeId() + ); + } elseif ($optionType === 'multiple') { + foreach ($customOption->getValues() as $optionValue) { + $selectedOptions[] = $this->encodeSelectedOption( + (int)$customOption->getOptionId(), + (int)$optionValue->getOptionTypeId() + ); + } + } + } + + return [ + 'selected_options' => $selectedOptions, + 'entered_options' => $enteredOptions + ]; + } + + /** + * Returns uid of the selected custom option + * + * @param int $optionId + * @param int $optionValueId + * + * @return string + */ + private function encodeSelectedOption(int $optionId, int $optionValueId): string + { + return base64_encode("custom-option/$optionId/$optionValueId"); + } + + /** + * Returns uid of the entered custom option + * + * @param int $optionId + * + * @return string + */ + private function encodeEnteredOption(int $optionId): string + { + return base64_encode("custom-option/$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 new file mode 100644 index 0000000000000..08273e7936640 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -0,0 +1,159 @@ +<?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; + +/** + * Test coverage for updating a product from wishlist + */ +class UpdateProductsFromWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testUpdateSimpleProductFromWishlist(): void + { + $wishlist = $this->getWishlist(); + $qty = 5; + $description = 'New Description'; + $wishlistId = $wishlist['customer']['wishlist']['id']; + $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()); + + $this->assertArrayHasKey('updateProductsInWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['updateProductsInWishlist']); + $wishlistResponse = $response['updateProductsInWishlist']['wishlist']; + $this->assertEquals($qty, $wishlistResponse['items_v2'][0]['quantity']); + $this->assertEquals($description, $wishlistResponse['items_v2'][0]['description']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $wishlistId + * @param int $wishlistItemId + * @param int $qty + * @param string $description + * + * @return string + */ + private function getQuery( + int $wishlistId, + int $wishlistItemId, + int $qty, + string $description + ): string { + return <<<MUTATION +mutation { + updateProductsInWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + wishlist_item_id: "{$wishlistItemId}" + quantity: {$qty} + description: "{$description}" + } + ] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + items_v2 { + id + description + quantity + } + } + } +} +MUTATION; + } + + /** + * Get wishlist result + * + * @return array + * + * @throws Exception + */ + public function getWishlist(): array + { + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + } + + /** + * Get customer wishlist query + * + * @return string + */ + private function getCustomerWishlistQuery(): string + { + return <<<QUERY +query { + customer { + wishlist { + id + items_count + items_v2 { + id + quantity + description + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php index bb353938239bc..88c59d6dd8428 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php @@ -39,6 +39,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php */ public function testGetCustomerWishlist(): void @@ -94,6 +95,7 @@ public function testGetCustomerWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 */ public function testGetGuestWishlist() { diff --git a/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php b/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php index 91a044f189b4c..0e277ac942263 100644 --- a/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php @@ -7,7 +7,7 @@ namespace Magento\Integration\Model; use Magento\Customer\Api\AccountManagementInterface; -use Magento\Framework\Exception\InputException; +use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Model\Oauth\Token as TokenModel; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; @@ -76,9 +76,15 @@ protected function setUp(): void } /** + * Create customer access token + * + * @dataProvider storesDataProvider * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @param string|null $store + * @return void */ - public function testCreateCustomerAccessToken() + public function testCreateCustomerAccessToken(?string $store): void { $userName = 'customer@example.com'; $password = 'password'; @@ -86,15 +92,28 @@ public function testCreateCustomerAccessToken() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $userName, 'password' => $password]; - $accessToken = $this->_webApiCall($serviceInfo, $requestData); + $accessToken = $this->_webApiCall($serviceInfo, $requestData, null, $store); $this->assertToken($accessToken, $userName, $password); } + /** + * DataProvider for testCreateCustomerAccessToken + * + * @return array + */ + public function storesDataProvider(): array + { + return [ + 'default store' => [null], + 'all store view' => ['all'], + ]; + } + /** * @dataProvider validationDataProvider */ @@ -105,7 +124,7 @@ public function testCreateCustomerAccessTokenEmptyOrNullCredentials($username, $ $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $username, 'password' => $password]; @@ -128,7 +147,7 @@ public function testCreateCustomerAccessTokenInvalidCustomer() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $customerUserName, 'password' => $password]; @@ -195,7 +214,7 @@ public function testThrottlingMaxAttempts() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $invalidCredentials = [ @@ -238,7 +257,7 @@ public function testThrottlingAccountLockout() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $invalidCredentials = [ diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestShipmentEstimationWithExtensionAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestShipmentEstimationWithExtensionAttributesTest.php new file mode 100644 index 0000000000000..dc59a571aa136 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestShipmentEstimationWithExtensionAttributesTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Quote\Api; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Quote\Api\Data\AddressInterface; + +class GuestShipmentEstimationWithExtensionAttributesTest extends WebapiAbstract +{ + const SERVICE_VERSION = 'V1'; + const SERVICE_NAME = 'quoteGuestShipmentEstimationV1'; + const RESOURCE_PATH = '/V1/guest-carts/'; + + /** + * @var ObjectManager + */ + private $objectManager; + + protected function setUp(): void + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_free_shipping.php + * @magentoApiDataFixture Magento/Sales/_files/quote.php + */ + public function testEstimateByExtendedAddress(): void + { + /** @var \Magento\Quote\Model\Quote $quote */ + $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + $quote->load('test01', 'reserved_order_id'); + $cartId = $quote->getId(); + if (!$cartId) { + $this->fail('quote fixture failed'); + } + + /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ + $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); + $quoteIdMask->load($cartId, 'quote_id'); + //Use masked cart Id + $cartId = $quoteIdMask->getMaskedId(); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/guest-carts/' . $cartId . '/estimate-shipping-methods', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => 'V1', + 'operation' => self::SERVICE_NAME . 'EstimateByExtendedAddress', + ], + ]; + if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { + /** @var \Magento\Quote\Model\Quote\Address $address */ + $address = $quote->getShippingAddress(); + + $data = [ + AddressInterface::KEY_ID => (int)$address->getId(), + AddressInterface::KEY_REGION => $address->getRegion(), + AddressInterface::KEY_REGION_ID => $address->getRegionId(), + AddressInterface::KEY_REGION_CODE => $address->getRegionCode(), + AddressInterface::KEY_COUNTRY_ID => $address->getCountryId(), + AddressInterface::KEY_STREET => $address->getStreet(), + AddressInterface::KEY_COMPANY => $address->getCompany(), + AddressInterface::KEY_TELEPHONE => $address->getTelephone(), + AddressInterface::KEY_POSTCODE => $address->getPostcode(), + AddressInterface::KEY_CITY => $address->getCity(), + AddressInterface::KEY_FIRSTNAME => $address->getFirstname(), + AddressInterface::KEY_LASTNAME => $address->getLastname(), + AddressInterface::KEY_CUSTOMER_ID => $address->getCustomerId(), + AddressInterface::KEY_EMAIL => $address->getEmail(), + AddressInterface::SAME_AS_BILLING => $address->getSameAsBilling(), + AddressInterface::CUSTOMER_ADDRESS_ID => $address->getCustomerAddressId(), + AddressInterface::SAVE_IN_ADDRESS_BOOK => $address->getSaveInAddressBook(), + ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => [ + 'discounts' => [] + ] + ]; + + $requestData = [ + 'cartId' => $cartId, + 'address' => $data + ]; + } else { + + $requestData = [ + 'address' => [ + 'country_id' => "US", + 'postcode' => null, + 'region' => null, + 'region_id' => null, + 'extension_attributes' => [ + 'discounts' => [] + ] + ] + ]; + } + + // Cart must be anonymous (see fixture) + $this->assertEmpty($quote->getCustomerId()); + + $result = $this->_webApiCall($serviceInfo, $requestData); + + $this->assertNotEmpty($result); + $this->assertEquals(1, count($result)); + foreach ($result as $rate) { + $this->assertEquals("flatrate", $rate['carrier_code']); + $this->assertEquals(0, $rate['amount']); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderAddressUpdateTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderAddressUpdateTest.php index 1096c0dca6530..c5b06285f1fe1 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderAddressUpdateTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderAddressUpdateTest.php @@ -3,13 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Service\V1; use Magento\Sales\Api\Data\OrderAddressInterface as OrderAddress; use Magento\TestFramework\TestCase\WebapiAbstract; /** - * Class OrderAddressUpdateTest + * Test for address update */ class OrderAddressUpdateTest extends WebapiAbstract { @@ -28,7 +29,7 @@ public function testOrderAddressUpdate() $order = $objectManager->get(\Magento\Sales\Model\Order::class)->loadByIncrementId('100000001'); $address = [ - OrderAddress::REGION => 'CA', + OrderAddress::REGION => 'California', OrderAddress::POSTCODE => '11111', OrderAddress::LASTNAME => 'lastname', OrderAddress::STREET => ['street'], @@ -75,7 +76,7 @@ public function testOrderAddressUpdate() $billingAddress = $actualOrder->getBillingAddress(); $validate = [ - OrderAddress::REGION => 'CA', + OrderAddress::REGION => 'California', OrderAddress::POSTCODE => '11111', OrderAddress::LASTNAME => 'lastname', OrderAddress::STREET => 'street', diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php index 021698f874e55..e28cca72e8fb8 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php @@ -76,7 +76,7 @@ public function testOrderGet(): void 'city' => 'Los Angeles', 'email' => 'customer@null.com', 'postcode' => '11111', - 'region' => 'CA' + 'region' => 'California' ]; $result = $this->makeServiceCall(self::ORDER_INCREMENT_ID); diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php index e5df8c18cda0c..e7ee1acda7982 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php @@ -4,27 +4,64 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Service\V1; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Test for hold order. + */ class OrderHoldTest extends WebapiAbstract { - const SERVICE_VERSION = 'V1'; + private const SERVICE_VERSION = 'V1'; - const SERVICE_NAME = 'salesOrderManagementV1'; + private const SERVICE_NAME = 'salesOrderManagementV1'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + } /** - * @magentoApiDataFixture Magento/Sales/_files/order.php + * Test hold order and check order items product options after. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_two_configurable_variations.php + * + * @return void */ - public function testOrderHold() + public function testOrderHold(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $order = $objectManager->get(\Magento\Sales\Model\Order::class)->loadByIncrementId('100000001'); + $order = $this->objectManager->get(Order::class) + ->loadByIncrementId('100000001'); + $orderId = $order->getId(); + $orderItemsProductOptions = $this->getOrderItemsProductOptions($order); + $serviceInfo = [ 'rest' => [ - 'resourcePath' => '/V1/orders/' . $order->getId() . '/hold', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'resourcePath' => '/V1/orders/' . $orderId . '/hold', + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -32,8 +69,29 @@ public function testOrderHold() 'operation' => self::SERVICE_NAME . 'hold', ], ]; - $requestData = ['id' => $order->getId()]; + $requestData = ['id' => $orderId]; $result = $this->_webApiCall($serviceInfo, $requestData); $this->assertTrue($result); + + $this->assertEquals( + $orderItemsProductOptions, + $this->getOrderItemsProductOptions($this->orderRepository->get($orderId)) + ); + } + + /** + * Return order items product options + * + * @param OrderInterface $order + * @return array + */ + private function getOrderItemsProductOptions(OrderInterface $order): array + { + $result = []; + foreach ($order->getItems() as $orderItem) { + $result[] = $orderItem->getProductOptions(); + } + + return $result; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php index 08b3c4548e08f..29a11f9d68e8f 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php @@ -6,10 +6,16 @@ namespace Magento\Sales\Service\V1; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; /** * Class ShipmentCreateTest + * + * Test shipment save API */ class ShipmentCreateTest extends WebapiAbstract { @@ -20,23 +26,78 @@ class ShipmentCreateTest extends WebapiAbstract const SERVICE_VERSION = 'V1'; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $objectManager; protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); } /** + * Test save shipment return valid result with multiple tracks with multiple comments + * * @magentoApiDataFixture Magento/Sales/_files/order.php */ - public function testInvoke() + public function testInvokeWithMultipleTrackAndComments() { - /** @var \Magento\Sales\Model\Order $order */ - $order = $this->objectManager->create(\Magento\Sales\Model\Order::class)->loadByIncrementId('100000001'); - $orderItem = current($order->getAllItems()); + $data = $this->getEntityData(); + $result = $this->_webApiCall( + $this->getServiceInfo(), + [ + 'entity' => $data['shipment data with multiple tracking and multiple comments']] + ); + $this->assertNotEmpty($result); + $this->assertEquals(3, count($result['tracks'])); + $this->assertEquals(3, count($result['comments'])); + } + + /** + * Test save shipment return valid result with multiple tracks with no comments + * + * @magentoApiDataFixture Magento/Sales/_files/order.php + */ + public function testInvokeWithMultipleTrackAndNoComments() + { + $data = $this->getEntityData(); + $result = $this->_webApiCall( + $this->getServiceInfo(), + [ + 'entity' => $data['shipment data with multiple tracking']] + ); + $this->assertNotEmpty($result); + $this->assertEquals(3, count($result['tracks'])); + $this->assertEquals(0, count($result['comments'])); + } + + /** + * Test save shipment return valid result with no tracks with multiple comments + * + * @magentoApiDataFixture Magento/Sales/_files/order.php + */ + public function testInvokeWithNoTrackAndMultipleComments() + { + $data = $this->getEntityData(); + $result = $this->_webApiCall( + $this->getServiceInfo(), + [ + 'entity' => $data['shipment data with multiple comments']] + ); + $this->assertNotEmpty($result); + $this->assertEquals(0, count($result['tracks'])); + $this->assertEquals(3, count($result['comments'])); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function getEntityData() + { + $existingOrder = $this->getOrder('100000001'); + $orderItem = current($existingOrder->getAllItems()); + $items = [ [ 'order_item_id' => $orderItem->getId(), @@ -53,10 +114,201 @@ public function testInvoke() 'weight' => null, ], ]; - $serviceInfo = [ + return [ + 'shipment data with multiple tracking and multiple comments' => [ + 'order_id' => $existingOrder->getId(), + 'entity_id' => null, + 'store_id' => null, + 'total_weight' => null, + 'total_qty' => null, + 'email_sent' => null, + 'customer_id' => null, + 'shipping_address_id' => null, + 'billing_address_id' => null, + 'shipment_status' => null, + 'increment_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'shipping_label' => null, + 'tracks' => [ + [ + 'carrier_code' => 'UPS', + 'order_id' => $existingOrder->getId(), + 'title' => 'ground', + 'description' => null, + 'track_number' => '12345678', + 'parent_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'qty' => null, + 'weight' => null + ], + [ + 'carrier_code' => 'UPS', + 'order_id' => $existingOrder->getId(), + 'title' => 'ground', + 'description' => null, + 'track_number' => '654563221', + 'parent_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'qty' => null, + 'weight' => null + ], + [ + 'carrier_code' => 'USPS', + 'order_id' => $existingOrder->getId(), + 'title' => 'ground', + 'description' => null, + 'track_number' => '789654565', + 'parent_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'qty' => null, + 'weight' => null + ] + ], + 'items' => $items, + 'comments' => [ + [ + 'comment' => 'Shipment-related comment-1.', + 'is_customer_notified' => null, + 'is_visible_on_front' => null, + 'parent_id' => null + ], + [ + 'comment' => 'Shipment-related comment-2.', + 'is_customer_notified' => null, + 'is_visible_on_front' => null, + 'parent_id' => null + ], + [ + 'comment' => 'Shipment-related comment-3.', + 'is_customer_notified' => null, + 'is_visible_on_front' => null, + 'parent_id' => null + ] + + ] + ], + 'shipment data with multiple tracking' => [ + 'order_id' => $existingOrder->getId(), + 'entity_id' => null, + 'store_id' => null, + 'total_weight' => null, + 'total_qty' => null, + 'email_sent' => null, + 'customer_id' => null, + 'shipping_address_id' => null, + 'billing_address_id' => null, + 'shipment_status' => null, + 'increment_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'shipping_label' => null, + 'tracks' => [ + [ + 'carrier_code' => 'UPS', + 'order_id' => $existingOrder->getId(), + 'title' => 'ground', + 'description' => null, + 'track_number' => '12345678', + 'parent_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'qty' => null, + 'weight' => null + ], + [ + 'carrier_code' => 'UPS', + 'order_id' => $existingOrder->getId(), + 'title' => 'ground', + 'description' => null, + 'track_number' => '654563221', + 'parent_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'qty' => null, + 'weight' => null + ], + [ + 'carrier_code' => 'USPS', + 'order_id' => $existingOrder->getId(), + 'title' => 'ground', + 'description' => null, + 'track_number' => '789654565', + 'parent_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'qty' => null, + 'weight' => null + ] + ], + 'items' => $items, + 'comments' => [] + ], + 'shipment data with multiple comments' => [ + 'order_id' => $existingOrder->getId(), + 'entity_id' => null, + 'store_id' => null, + 'total_weight' => null, + 'total_qty' => null, + 'email_sent' => null, + 'customer_id' => null, + 'shipping_address_id' => null, + 'billing_address_id' => null, + 'shipment_status' => null, + 'increment_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'shipping_label' => null, + 'tracks' => [], + 'items' => $items, + 'comments' => [ + [ + 'comment' => 'Shipment-related comment-1.', + 'is_customer_notified' => null, + 'is_visible_on_front' => null, + 'parent_id' => null + ], + [ + 'comment' => 'Shipment-related comment-2.', + 'is_customer_notified' => null, + 'is_visible_on_front' => null, + 'parent_id' => null + ], + [ + 'comment' => 'Shipment-related comment-3.', + 'is_customer_notified' => null, + 'is_visible_on_front' => null, + 'parent_id' => null + ] + + ] + ] + ]; + } + + /** + * Returns order by increment id. + * + * @param string $incrementId + * @return Order + */ + private function getOrder(string $incrementId): Order + { + return $this->objectManager->create(Order::class)->loadByIncrementId($incrementId); + } + + /** + * @return array + */ + private function getServiceInfo(): array + { + return [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => self::SERVICE_READ_NAME, @@ -64,46 +316,5 @@ public function testInvoke() 'operation' => self::SERVICE_READ_NAME . 'save', ], ]; - $data = [ - 'order_id' => $order->getId(), - 'entity_id' => null, - 'store_id' => null, - 'total_weight' => null, - 'total_qty' => null, - 'email_sent' => null, - 'customer_id' => null, - 'shipping_address_id' => null, - 'billing_address_id' => null, - 'shipment_status' => null, - 'increment_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'shipping_label' => null, - 'tracks' => [ - [ - 'carrier_code' => 'UPS', - 'order_id' => $order->getId(), - 'title' => 'ground', - 'description' => null, - 'track_number' => '12345678', - 'parent_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'qty' => null, - 'weight' => null - ] - ], - 'items' => $items, - 'comments' => [ - [ - 'comment' => 'Shipment-related comment.', - 'is_customer_notified' => null, - 'is_visible_on_front' => null, - 'parent_id' => null - ] - ], - ]; - $result = $this->_webApiCall($serviceInfo, ['entity' => $data]); - $this->assertNotEmpty($result); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php b/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php index 6c8d3f90cf65c..8a68e24c8a21c 100644 --- a/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php @@ -24,19 +24,24 @@ class SearchTest extends WebapiAbstract */ private $product; + /** + * @inheritDoc + */ protected function setUp(): void { $productSku = 'simple'; $objectManager = Bootstrap::getObjectManager(); - $productRepository = $objectManager->create(ProductRepositoryInterface::class); + $productRepository = $objectManager->get(ProductRepositoryInterface::class); $this->product = $productRepository->get($productSku); } /** - * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * Tests that webapi call returns response when search criteria is valid. + * + * @magentoApiDataFixture Magento/Catalog/_files/products.php */ - public function testExistingProductSearch() + public function testExistingProductSearch(): void { $productName = $this->product->getName(); @@ -47,14 +52,16 @@ public function testExistingProductSearch() self::assertArrayHasKey('search_criteria', $response); self::assertArrayHasKey('items', $response); - self::assertGreaterThan(0, count($response['items'])); + self::assertGreaterThan(1, count($response['items'])); self::assertGreaterThan(0, $response['items'][0]['id']); } /** - * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * Tests that response is empty if invalid data is provided. + * + * @magentoApiDataFixture Magento/Catalog/_files/products.php */ - public function testNonExistentProductSearch() + public function testNonExistentProductSearch(): void { $searchCriteria = $this->buildSearchCriteria('nonExistentProduct'); $serviceInfo = $this->buildServiceInfo($searchCriteria); diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/AbstractOverridesTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/AbstractOverridesTest.php new file mode 100644 index 0000000000000..f0dccff848f04 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/AbstractOverridesTest.php @@ -0,0 +1,38 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig; + +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Base class for override config tests. + */ +abstract class AbstractOverridesTest extends WebapiAbstract +{ + /** @var ObjectManagerInterface */ + protected $objectManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $useConfig = (defined('USE_OVERRIDE_CONFIG') && USE_OVERRIDE_CONFIG === 'enabled'); + + if (!$useConfig) { + $this->markTestSkipped('Override config is disabled.'); + } + + $this->objectManager = Bootstrap::getObjectManager(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php new file mode 100644 index 0000000000000..326ec789da45a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Test abstract class for testing fixtures override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +abstract class FixturesAbstractClass extends AbstractOverridesTest +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php new file mode 100644 index 0000000000000..e0049895577cc --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +/** + * Test interface for testing fixtures override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +interface FixturesInterface +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php new file mode 100644 index 0000000000000..ca811c222132e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php @@ -0,0 +1,229 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class checks that fixtures override config inherited from abstract class and interface. + * + * phpcs:disable Generic.Classes.DuplicateClassName + * + * @magentoAppIsolation enabled + */ +class FixturesTest extends FixturesAbstractClass implements FixturesInterface +{ + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var ConfigStorage + */ + private $configStorage; + + /** + * @var FixtureCallStorage + */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * @magentoConfigFixture default_store test_section/test_group/field_2 new_value + * @magentoConfigFixture default_store test_section/test_group/field_3 new_value + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * @dataProvider interfaceDataProvider + * @param array $storeConfigs + * @param array $fixtures + * @return void + */ + public function testInterfaceInheritance( + array $storeConfigs, + array $fixtures + ): void { + $this->assertConfigFieldValues($storeConfigs, ScopeInterface::SCOPE_STORES); + $this->assertUsedFixturesCount($fixtures); + } + + /** + * @magentoConfigFixture default_store test_section/test_group/field_2 new_value + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @dataProvider abstractDataProvider + * @param array $storeConfigs + * @param array $fixtures + * @return void + */ + public function testAbstractInheritance( + array $storeConfigs, + array $fixtures + ): void { + $this->assertConfigFieldValues($storeConfigs, ScopeInterface::SCOPE_STORES); + $this->assertUsedFixturesCount($fixtures); + } + + /** + * @return array + */ + public function interfaceDataProvider(): array + { + return [ + 'first_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for class', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => 'overridden config fixture value for method', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_3' => [ + 'value' => 'new_value', + 'exists_in_db' => true, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 1, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 1, + 'fixture3_first_module.php' => 1, + ], + ], + 'second_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for class', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => 'overridden config fixture value for method', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_3' => [ + 'value' => '3rd field website scope default value', + 'exists_in_db' => false, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 1, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 1, + 'fixture3_first_module.php' => 0, + ], + ], + ]; + } + + /** + * @return array + */ + public function abstractDataProvider(): array + { + return [ + 'first_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for class', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => '2nd field default value', + 'exists_in_db' => false, + ], + 'test_section/test_group/field_3' => [ + 'value' => 'overridden config fixture value for data set from abstract', + 'exists_in_db' => true, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 1, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 0, + 'fixture3_first_module.php' => 1, + ], + ], + 'second_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for data set from abstract', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => '2nd field default value', + 'exists_in_db' => false, + ], + 'test_section/test_group/field_3' => [ + 'value' => '3rd field website scope default value', + 'exists_in_db' => false, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 0, + 'fixture2_first_module.php' => 0, + 'fixture1_second_module.php' => 1, + 'fixture3_first_module.php' => 0, + ], + ], + ]; + } + + /** + * Asserts config field values. + * + * @param array $configs + * @param string $scope + * @return void + */ + private function assertConfigFieldValues( + array $configs, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ): void { + foreach ($configs as $path => $expected) { + $this->assertEquals($expected['value'], $this->config->getValue($path, $scope, 'default')); + if ($expected['exists_in_db']) { + $this->assertEquals( + $expected['value'], + $this->configStorage->getValueFromDb($path, ScopeInterface::SCOPE_STORES, 'default') + ); + } else { + $this->assertFalse( + $this->configStorage->checkIsRecordExist($path, ScopeInterface::SCOPE_STORES, 'default') + ); + } + } + } + + /** + * Asserts count of used fixtures. + * + * @param array $fixtures + * @return void + */ + private function assertUsedFixturesCount(array $fixtures): void + { + foreach ($fixtures as $fixture => $count) { + $this->assertEquals($count, $this->fixtureCallStorage->getFixturesCount($fixture)); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php new file mode 100644 index 0000000000000..445aa0c501c0a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Test abstract class for testing skip override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +abstract class SkipAbstractClass extends AbstractOverridesTest +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php new file mode 100644 index 0000000000000..99a9332460211 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +/** + * Test interface for testing skip override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +interface SkipInterface +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php new file mode 100644 index 0000000000000..e5eb1e3a419f7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +/** + * Class checks that test method can be skipped using inherited from abstract class/interface override config + * + * phpcs:disable Generic.Classes.DuplicateClassName + * + * @magentoAppIsolation enabled + */ +class SkipTest extends SkipAbstractClass implements SkipInterface +{ + /** + * @return void + */ + public function testAbstractSkip(): void + { + $this->fail('This test should be skipped via override config in method node inherited from abstract class'); + } + + /** + * @return void + */ + public function testInterfaceSkip(): void + { + $this->fail('This test should be skipped via override config in method node inherited from interface'); + } + + /** + * @dataProvider skipDataProvider + * + * @param string $message + * @return void + */ + public function testSkipDataSet(string $message): void + { + $this->fail($message); + } + + /** + * @return array + */ + public function skipDataProvider(): array + { + return [ + 'first_data_set' => ['This test should be skipped in data set node inherited from abstract class'], + 'second_data_set' => ['This test should be skipped in data set node inherited from interface'], + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/AddFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/AddFixtureTest.php new file mode 100644 index 0000000000000..bc9933a886f50 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/AddFixtureTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiConfigFixture; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class checks that magentoConfigFixtures can be added via override config + * + * @magentoAppIsolation enabled + */ +class AddFixtureTest extends AbstractOverridesTest +{ + /** @var ScopeConfigInterface */ + private $config; + + /** @var ConfigStorage */ + private $configStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + } + + /** + * Checks that fixture added in test class node successfully applied + * + * @return void + */ + public function testAddFixtureToClass(): void + { + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_STORES, 'default'); + $this->assertEquals('overridden value for full class', $value); + $this->assertEquals( + 'overridden value for full class', + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * Checks that fixtures added in method and data set nodes successfully applied + * + * @dataProvider testDataProvider + * + * @param string $expectedConfigValue + * @return void + */ + public function testAddFixtureToMethod(string $expectedConfigValue): void + { + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_STORES, 'default'); + $this->assertEquals($expectedConfigValue, $value); + $this->assertEquals( + $expectedConfigValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * @return array + */ + public function testDataProvider(): array + { + return [ + 'first_data_set' => ['expected_config_value' => 'overridden value for method'], + 'second_data_set' => ['expected_config_value' => 'overridden value for data set'] + ]; + } + + /** + * Checks that fixtures can be added on website scope + * + * @return void + */ + public function testAddFixtureOnWebsiteScope(): void + { + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_WEBSITES, 'base'); + $this->assertEquals('overridden value for method on website scope', $value); + $this->assertEquals( + 'overridden value for method on website scope', + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_WEBSITES, + 'base' + ) + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/RemoveFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/RemoveFixtureTest.php new file mode 100644 index 0000000000000..148f18b4cc811 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/RemoveFixtureTest.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiConfigFixture; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class checks that magentoConfigFixtures can be removed using override config + * + * @magentoAppIsolation enabled + */ +class RemoveFixtureTest extends AbstractOverridesTest +{ + /** @var ScopeConfigInterface */ + private $config; + + /** @var ConfigStorage */ + private $configStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + } + + /** + * Checks that fixture can be removed in test class node + * + * @magentoConfigFixture default_store test_section/test_group/field_1 new_value + * + * @return void + */ + public function testRemoveFixtureForClass(): void + { + $value = $this->config->getValue( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ); + $this->assertEquals('1st field default value', $value); + $this->assertFalse( + $this->configStorage->checkIsRecordExist( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * Checks that fixtures can be removed in method and data set nodes + * + * @magentoConfigFixture default_store test_section/test_group/field_2 new_value + * @magentoConfigFixture default_store test_section/test_group/field_3 new_value + * + * @dataProvider testDataProvider + * + * @param string $expectedFirstValue + * @param string $expectedSecondValue + * @param bool $firstvalueExist + * @param bool $secondvalueExist + * @return void + */ + public function testRemoveFixtureForMethod( + string $expectedFirstValue, + string $expectedSecondValue, + bool $firstvalueExist, + bool $secondvalueExist + ): void { + $fistValue = $this->config->getValue( + 'test_section/test_group/field_2', + ScopeInterface::SCOPE_STORES, + 'default' + ); + $secondValue = $this->config->getValue( + 'test_section/test_group/field_3', + ScopeInterface::SCOPE_STORES, + 'default' + ); + $this->assertEquals($expectedFirstValue, $fistValue); + if ($firstvalueExist) { + $this->assertEquals( + $expectedFirstValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_2', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + $this->assertEquals( + $firstvalueExist, + $this->configStorage->checkIsRecordExist( + 'test_section/test_group/field_2', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + $this->assertEquals($expectedSecondValue, $secondValue); + if ($secondvalueExist) { + $this->assertEquals( + $expectedSecondValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_3', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + $this->assertEquals( + $secondvalueExist, + $this->configStorage->checkIsRecordExist( + 'test_section/test_group/field_3', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * @return array + */ + public function testDataProvider(): array + { + return [ + 'first_data_set' => [ + 'expected_first_config_value' => '2nd field default value', + 'expected_second_config_value' => 'new_value', + 'first_value_exist' => false, + 'second_value_exist' => true, + ], + 'second_data_set' => [ + 'expected_first_config_value' => '2nd field default value', + 'expected_second_config_value' => '3rd field website scope default value', + 'first_value_exist' => false, + 'second_value_exist' => false, + ], + ]; + } + + /** + * Checks that website scope fixture can be removed + * + * @magentoConfigFixture base_website test_section/test_group/field_3 new_value + * + * @return void + */ + public function testRemoveWebsiteScopeFixture(): void + { + $value = $this->config->getValue( + 'test_section/test_group/field_3', + ScopeInterface::SCOPE_WEBSITES, + 'base' + ); + $this->assertEquals('3rd field website scope default value', $value); + $this->assertFalse( + $this->configStorage->checkIsRecordExist( + 'test_section/test_group/field_3', + ScopeInterface::SCOPE_WEBSITES, + 'base' + ) + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/ReplaceFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/ReplaceFixtureTest.php new file mode 100644 index 0000000000000..d8342fe3394ce --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/ReplaceFixtureTest.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiConfigFixture; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class check that fixtures can be replaced using override config + * + * @magentoAppIsolation enabled + */ +class ReplaceFixtureTest extends AbstractOverridesTest +{ + /** @var ScopeConfigInterface */ + private $config; + + /** @var ConfigStorage */ + private $configStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + } + + /** + * Checks that fixture can be replaced in test class node + * + * @magentoConfigFixture default_store test_section/test_group/field_1 new_value + * + * @return void + */ + public function testReplaceFixtureForClass(): void + { + $expectedValue = 'Overridden fixture for class'; + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_STORES, 'default'); + $this->assertEquals($expectedValue, $value); + $this->assertEquals( + $expectedValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * Checks that fixture can be replaced in method and data set nodes + * + * @magentoConfigFixture default_store test_section/test_group/field_1 new_value + * + * @dataProvider testDataProvider + * + * @param string $expectedConfigValue + * @return void + */ + public function testReplaceFixtureForMethod(string $expectedConfigValue): void + { + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_STORES, 'default'); + $this->assertEquals($expectedConfigValue, $value); + $this->assertEquals( + $expectedConfigValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * @return array + */ + public function testDataProvider(): array + { + return [ + 'first_data_set' => [ + 'expected_config_value' => 'Overridden fixture for method', + ], + 'second_data_set' => [ + 'expected_config_value' => 'Overridden fixture for data set', + ], + ]; + } + + /** + * Checks that website scope fixture can be replaced + * + * @magentoConfigFixture base_website test_section/test_group/field_1 new_value + * + * @return void + */ + public function testReplaceWebsiteScopedFixture(): void + { + $expectedConfigValue = 'Overridden value for website scope'; + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_WEBSITES, 'base'); + $this->assertEquals($expectedConfigValue, $value); + $this->assertEquals( + $expectedConfigValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_WEBSITE, + 'base' + ) + ); + } + + /** + * Checks that replace config from last loaded file will be applied + * + * @magentoConfigFixture default_store test_section/test_group/field_1 new_value + * + * @dataProvider configValuesProvider + * + * @param string $expectedConfigValue + * @return void + */ + public function testReplaceFixtureViaThirdModule(string $expectedConfigValue): void + { + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_STORES, 'default'); + $this->assertEquals($expectedConfigValue, $value); + $this->assertEquals( + $expectedConfigValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * @return array + */ + public function configValuesProvider(): array + { + return [ + 'first_data_set' => [ + 'expected_config_value' => 'Overridden fixture for method from third module', + ], + 'second_data_set' => [ + 'expected_config_value' => 'Overridden fixture for data set from third module', + ], + ]; + } + + /** + * Checks that fixture for global scope can be replaced + * + * @magentoConfigFixture test_section/test_group/field_1 new_value + * + * @return void + */ + public function testReplaceDefaultConfig(): void + { + $expectedConfigValue = 'Overridden value for default scope'; + $value = $this->config->getValue('test_section/test_group/field_1'); + $this->assertEquals('Overridden value for default scope', $value); + $this->assertEquals( + $expectedConfigValue, + $this->configStorage->getValueFromDb('test_section/test_group/field_1') + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/AddFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/AddFixtureTest.php new file mode 100644 index 0000000000000..2021bbf10672b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/AddFixtureTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiDataFixture; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class checks that magentoDataFixtures can be added using override config + * + * @magentoAppIsolation enabled + */ +class AddFixtureTest extends AbstractOverridesTest +{ + /** @var FixtureCallStorage */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * Checks that fixtures added in all nodes successfully applied + * + * @dataProvider addedFixturesProvider + * + * @param array $fixtures + * @return void + */ + public function testAddFixtures(array $fixtures): void + { + foreach ($fixtures as $scope => $fixture) { + $this->assertEquals( + 1, + $this->fixtureCallStorage->getFixturesCount($fixture), + sprintf('Fixture added in %s scope was not called', $scope) + ); + } + } + + /** + * @return array + */ + public function addedFixturesProvider(): array + { + return [ + 'first_data_set' => [ + [ + 'class' => 'fixture1_second_module.php', + 'method' => 'fixture2_second_module.php', + 'data_set' => 'fixture3_second_module.php', + ], + ], + 'second_data_set' => [ + [ + 'class' => 'fixture1_second_module.php', + 'method' => 'fixture2_second_module.php', + ], + ], + ]; + } + + /** + * Checks that same fixture can be added via override config from few files + * + * @return void + */ + public function testAddSameFixtures(): void + { + $this->assertEquals( + 3, + $this->fixtureCallStorage->getFixturesCount('fixture2_second_module.php') + ); + } + + /** + * Checks that fixture which require another fixture can be added using override + * + * @return void + */ + public function testAddFixtureWithRequiredFixture(): void + { + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture_with_required_fixture.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture3_second_module.php')); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/RemoveFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/RemoveFixtureTest.php new file mode 100644 index 0000000000000..7521770873b0a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/RemoveFixtureTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiDataFixture; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Checks that magentoDataFixture can be removed using override config + * + * @magentoAppIsolation enabled + */ +class RemoveFixtureTest extends AbstractOverridesTest +{ + /** @var FixtureCallStorage */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * Checks that fixture can be removed in test class node + * + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php + * + * @return void + */ + public function testRemoveFixtureForClass(): void + { + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount('fixture1_first_module.php')); + } + + /** + * Checks that fixture can be removed in method and data set nodes + * + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * + * @dataProvider testDataProvider + * + * @param string $fixtureName + * @return void + */ + public function testRemoveFixtureForMethod(string $fixtureName): void + { + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount($fixtureName)); + } + + /** + * @return array + */ + public function testDataProvider(): array + { + return [ + 'first_data_set' => ['fixture2_first_module.php'], + 'second_data_set' => ['fixture3_first_module.php'], + ]; + } + + /** + * Checks that same fixtures can be removed few times + * + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * + * @return void + */ + public function testRemoveSameFixtures(): void + { + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount('fixture3_first_module.php')); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/ReplaceFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/ReplaceFixtureTest.php new file mode 100644 index 0000000000000..a1892cf6af32a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/ReplaceFixtureTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiDataFixture; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class check that magentoApiDataFixtures can be replaced using override config + * + * @magentoAppIsolation enabled + */ +class ReplaceFixtureTest extends AbstractOverridesTest +{ + /** @var FixtureCallStorage */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * Checks that fixture can be replaced in test class node + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php + * + * @return void + */ + public function testReplaceFixtureForClass(): void + { + $this->assertEquals(0, $this->fixtureCallStorage->getFixturesCount('fixture1_first_module.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture1_second_module.php')); + } + + /** + * Checks that fixture can be replaced in method and data set nodes + * + * @dataProvider replacedFixturesProvider + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php + * + * @param string $fixture + * @return void + */ + public function testReplaceFixturesForMethod(string $fixture): void + { + $this->assertEquals(0, $this->fixtureCallStorage->getFixturesCount('fixture1_first_module.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount($fixture)); + } + + /** + * @return array + */ + public function replacedFixturesProvider(): array + { + return [ + 'first_data_set' => [ + 'fixture2_second_module.php', + ], + 'second_data_set' => [ + 'fixture3_second_module.php', + ], + ]; + } + + /** + * Checks that replace config from last loaded file will be applied + * + * @dataProvider dataProvider + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php + * + * @param string $fixture + * @return void + */ + public function testReplaceFixtureViaThirdModule(string $fixture): void + { + $this->assertEquals(0, $this->fixtureCallStorage->getFixturesCount('fixture1_first_module.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount($fixture)); + } + + /** + * @return array + */ + public function dataProvider(): array + { + return [ + 'first_data_set' => [ + 'fixture2_second_module.php', + ], + 'second_data_set' => [ + 'fixture3_second_module.php', + ], + ]; + } + + /** + * Checks that fixture required in the another fixture can be replaced using override + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig2/_files/fixture_with_required_fixture.php + * + * @return void + */ + public function testReplaceRequiredFixture(): void + { + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture_with_required_fixture.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture2_second_module.php')); + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount('fixture3_second_module.php')); + } + + /** + * Checks that fixture required in the another fixture will be replaced according to last loaded override + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig2/_files/fixture_with_required_fixture.php + * + * @return void + */ + public function testReplaceRequiredFixtureViaThirdModule(): void + { + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture_with_required_fixture.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture1_third_module.php')); + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount('fixture2_second_module.php')); + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount('fixture3_second_module.php')); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/SortFixturesTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/SortFixturesTest.php new file mode 100644 index 0000000000000..dc5b7d9e167a5 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/SortFixturesTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiDataFixture; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class checks that magentoConfigFixtures can be placed into certain place using override config + * + * @magentoAppIsolation enabled + */ +class SortFixturesTest extends AbstractOverridesTest +{ + /** @var FixtureCallStorage */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * Checks that fixtures can be placed to specific place according to config + * + * @dataProvider sortFixturesProvider + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * + * @param array $sortedFixtures + * @return void + */ + public function testSortFixtures(array $sortedFixtures): void + { + $this->assertEquals($sortedFixtures, $this->fixtureCallStorage->getStorage()); + } + + /** + * @return array + */ + public function sortFixturesProvider(): array + { + return [ + 'first_data_set' => [ + 'sorted_fixtures' => [ + 'fixture3_second_module.php', + 'fixture1_first_module.php', + 'fixture1_second_module.php', + 'fixture2_first_module.php', + 'fixture1_third_module.php', + 'fixture3_first_module.php', + 'fixture2_second_module.php', + ], + ], + 'second_data_set' => [ + 'sorted_fixtures' => [ + 'fixture1_first_module.php', + 'fixture1_second_module.php', + 'fixture2_first_module.php', + 'fixture3_first_module.php', + 'fixture2_second_module.php', + ], + ], + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipClassTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipClassTest.php new file mode 100644 index 0000000000000..05f6648c0559a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipClassTest.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class checks that full test class can be skipped + * + * @magentoAppIsolation enabled + */ +class SkipClassTest extends AbstractOverridesTest +{ + /** + * This test should not be executed according to override config it should be mark as skipped + * + * @return void + */ + public function testClassSkip(): void + { + $this->fail('This test should be skipped via override config in test class node'); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipDataSetTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipDataSetTest.php new file mode 100644 index 0000000000000..fec1caa5370e7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipDataSetTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class checks that only specific data set can be skipped using override config + * + * @magentoAppIsolation enabled + */ +class SkipDataSetTest extends AbstractOverridesTest +{ + /** + * The first_data_set should not be executed according to override config it should be mark as skipped + * + * @dataProvider testDataProvider + * + * @return void + */ + public function testSkipDataSet(): void + { + if ($this->dataName() === 'first_data_set') { + $this->fail('This test should be skipped via override config in data set node'); + } + } + + /** + * @return array + */ + public function testDataProvider(): array + { + return [ + 'first_data_set' => [], + 'second_data_set' => [], + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipMethodTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipMethodTest.php new file mode 100644 index 0000000000000..f824755650e1a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipMethodTest.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class checks that test method can be skipped using override config + * + * @magentoAppIsolation enabled + */ +class SkipMethodTest extends AbstractOverridesTest +{ + /** + * This test should not be executed according to override config it should be mark as skipped + * + * @return void + */ + public function testMethodSkip(): void + { + $this->fail('This test should be skipped via override config in method node'); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/WebApiTest.php b/dev/tests/api-functional/testsuite/Magento/WebApiTest.php new file mode 100644 index 0000000000000..32670dfeb7b1b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/WebApiTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento; + +use Magento\TestFramework\SkippableInterface; +use Magento\TestFramework\Workaround\Override\Config; +use Magento\TestFramework\Workaround\Override\WrapperGenerator; +use PHPUnit\Framework\TestSuite; +use PHPUnit\TextUI\Configuration\Registry; +use PHPUnit\TextUI\Configuration\TestSuiteCollection; +use PHPUnit\TextUI\Configuration\TestSuiteMapper; + +/** + * Web API tests wrapper. + */ +class WebApiTest extends TestSuite +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param string $className + * @return TestSuite + */ + public static function suite($className) + { + $generator = new WrapperGenerator(); + $overrideConfig = Config::getInstance(); + $configuration = Registry::getInstance()->get(self::getConfigurationFile()); + $suitesConfig = $configuration->testSuite(); + $suite = new TestSuite(); + /** @var \PHPUnit\TextUI\Configuration\TestSuite $suiteConfig */ + foreach ($suitesConfig as $suiteConfig) { + $suites = (new TestSuiteMapper())->map(TestSuiteCollection::fromArray([$suiteConfig]), ''); + /** @var TestSuite $testSuite */ + foreach ($suites as $testSuite) { + /** @var TestSuite $test */ + foreach ($testSuite as $test) { + $testName = $test->getName(); + + if ($overrideConfig->hasSkippedTest($testName) && !$test instanceof SkippableInterface) { + $reflectionClass = new \ReflectionClass($testName); + $resultTest = $generator->generateTestWrapper($reflectionClass); + $suite->addTest(new TestSuite($resultTest, $testName)); + } else { + $suite->addTest($test); + } + } + } + } + + return $suite; + } + + /** + * Returns config file name from command line params. + * + * @return string + */ + private static function getConfigurationFile(): string + { + $params = getopt('c:', ['configuration:']); + $longConfig = $params['configuration'] ?? ''; + $shortConfig = $params['c'] ?? ''; + + return $shortConfig ? $shortConfig : $longConfig; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php b/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php index 4d89e3a0b582a..5e278e6058dc9 100644 --- a/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php @@ -50,6 +50,7 @@ protected function setUp(): void * * @magentoApiDataFixture Magento/SalesRule/_files/rules_rollback.php * @magentoApiDataFixture Magento/Sales/_files/quote.php + * @magentoAppIsolation enabled */ public function testGetList() { @@ -87,6 +88,7 @@ public function testGetList() /** * @magentoApiDataFixture Magento/Sales/_files/invoice.php + * @magentoAppIsolation enabled */ public function testAutoGeneratedGetList() { @@ -131,6 +133,7 @@ public function testAutoGeneratedGetList() * Test get list of orders with extension attributes. * * @magentoApiDataFixture Magento/Sales/_files/order.php + * @magentoAppIsolation enabled */ public function testGetOrdertList() { diff --git a/dev/tests/api-functional/testsuite/Magento/Webapi/RestSessionCookieTest.php b/dev/tests/api-functional/testsuite/Magento/Webapi/RestSessionCookieTest.php new file mode 100644 index 0000000000000..36dc7a9afeba0 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Webapi/RestSessionCookieTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Webapi; + +use Magento\Framework\Module\Manager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class for RestSessionCookieTest + */ +class RestSessionCookieTest extends \Magento\TestFramework\TestCase\WebapiAbstract +{ + + private $moduleManager; + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->moduleManager = $this->objectManager->get(Manager::class); + if ($this->moduleManager->isEnabled('Magento_B2b')) { + $this->markTestSkipped('Skipped, because this logic is rewritten on B2B.'); + } + } + + /** + * Check for non exist cookie PHPSESSID + */ + public function testRestSessionNoCookie() + { + $this->_markTestAsRestOnly(); + /** @var $curlClient CurlClientWithCookies */ + + $curlClient = $this->objectManager + ->get(\Magento\TestFramework\TestCase\HttpClient\CurlClientWithCookies::class); + $phpSessionCookieName = + [ + 'cookie_name' => 'PHPSESSID', + ]; + + $response = $curlClient->get('/rest/V1/directory/countries', []); + + $cookie = $this->findCookie($phpSessionCookieName['cookie_name'], $response['cookies']); + $this->assertNull($cookie); + } + + /** + * Find cookie with given name in the list of cookies + * + * @param string $cookieName + * @param array $cookies + * @return $cookie|null + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + private function findCookie($cookieName, $cookies) + { + foreach ($cookies as $cookieIndex => $cookie) { + if ($cookie['name'] === $cookieName) { + return $cookie; + } + } + return null; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php b/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php index dadc2caef7a13..c43cb81683aac 100644 --- a/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php @@ -116,7 +116,7 @@ protected function _getWsdlContent($wsdlUrl) $responseDom->loadXML($responseContent), "Valid XML is always expected as a response for WSDL request." ); - return $responseContent; + return $responseDom->saveXML(); } /** @@ -207,7 +207,7 @@ protected function _checkComplexTypesDeclaration($wsdlContent) <xsd:sequence> <xsd:element name="id" minOccurs="1" maxOccurs="1" type="xsd:int"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:min/> <inf:max/> @@ -231,7 +231,7 @@ protected function _checkComplexTypesDeclaration($wsdlContent) <xsd:sequence> <xsd:element name="entityId" minOccurs="1" maxOccurs="1" type="xsd:int"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:min/> <inf:max/> @@ -266,7 +266,7 @@ protected function _checkComplexTypesDeclaration($wsdlContent) <xsd:sequence> <xsd:element name="result" minOccurs="1" maxOccurs="1" type="tns:TestModule5V2EntityAllSoapAndRest"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:callInfo> <inf:callName>testModule5AllSoapAndRestV2Item</inf:callName> @@ -290,7 +290,7 @@ protected function _checkComplexTypesDeclaration($wsdlContent) <xsd:sequence> <xsd:element name="result" minOccurs="1" maxOccurs="1" type="tns:TestModule5V1EntityAllSoapAndRest"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:callInfo> <inf:callName>testModule5AllSoapAndRestV1Item</inf:callName> @@ -331,7 +331,7 @@ protected function _checkReferencedTypeDeclaration($wsdlContent) <xsd:sequence> <xsd:element name="price" minOccurs="1" maxOccurs="1" type="xsd:int"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:min/> <inf:max/> @@ -835,7 +835,7 @@ protected function _checkFaultsComplexTypeSection($wsdlContent) <xsd:sequence> <xsd:element name="key" minOccurs="1" maxOccurs="1" type="xsd:string"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:maxLength/> </xsd:appinfo> @@ -843,7 +843,7 @@ protected function _checkFaultsComplexTypeSection($wsdlContent) </xsd:element> <xsd:element name="value" minOccurs="1" maxOccurs="1" type="xsd:string"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:maxLength/> </xsd:appinfo> @@ -865,7 +865,7 @@ protected function _checkFaultsComplexTypeSection($wsdlContent) <xsd:sequence> <xsd:element name="message" minOccurs="1" maxOccurs="1" type="xsd:string"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_baseUrl}/soap/{$this->_storeCode}?services=testModule5AllSoapAndRestV2"> <inf:maxLength/> </xsd:appinfo> @@ -888,7 +888,7 @@ protected function _checkFaultsComplexTypeSection($wsdlContent) <xsd:sequence> <xsd:element name="message" minOccurs="1" maxOccurs="1" type="xsd:string"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_baseUrl}/soap/{$this->_storeCode}?services=testModule5AllSoapAndRestV1%2CtestModule5AllSoapAndRestV2"> <inf:maxLength/> </xsd:appinfo> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml b/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml index e9eb1fe21aa4f..7c5d66427aecb 100644 --- a/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml @@ -11,6 +11,7 @@ <values> <value id="mage-base" type="host">https://magento.com</value> <value id="hash" type="hash" algorithm="sha256">B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=</value> + <value id="hash2" type="hash" algorithm="sha256">B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF9=</value> </values> </policy> <policy id="media-src"> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Aware.php b/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Aware.php new file mode 100644 index 0000000000000..567c308330ba3 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Aware.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleCspUtil\Controller\Csp; + +use Magento\Csp\Api\CspAwareActionInterface; +use Magento\Csp\Model\Policy\FetchPolicy; +use Magento\Framework\App\Action\Action; +use Magento\Framework\Controller\ResultFactory; + +/** + * CSP Aware controller. + */ +class Aware extends Action implements CspAwareActionInterface +{ + /** + * @inheritDoc + */ + public function execute() + { + return $this->resultFactory->create(ResultFactory::TYPE_PAGE); + } + + /** + * @inheritDoc + */ + public function modifyCsp(array $appliedPolicies): array + { + $policies = []; + foreach ($appliedPolicies as $policy) { + if ($policy instanceof FetchPolicy + && in_array('http://controller.magento.com', $policy->getHostSources(), true) + ) { + $policies[] = new FetchPolicy( + 'script-src', + false, + ['https://controller.magento.com'], + [], + true, + false, + false, + [], + ['H4RRnauTM2X2Xg/z9zkno1crqhsaY3uKKu97uwmnXXE=' => 'sha256'] + ); + } + } + + return $policies; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Helper.php b/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Helper.php new file mode 100644 index 0000000000000..8dde67de73dfa --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Helper.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleCspUtil\Controller\Csp; + +use Magento\Framework\App\Action\Action; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\View\Result\PageFactory; + +/** + * .phtml templates utilizes CSP helper. + */ +class Helper extends Action +{ + /** + * @inheritDoc + */ + public function execute() + { + return $this->resultFactory->create(ResultFactory::TYPE_PAGE); + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/composer.json b/dev/tests/integration/_files/Magento/TestModuleCspUtil/composer.json new file mode 100644 index 0000000000000..aece82306d9d5 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-csp-util", + "description": "test csp module", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleCspUtil" + ] + ] + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/csp_whitelist.xml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..c39e74edafd5e --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/csp_whitelist.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * 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="script-src"> + <values> + <value id="devdocs-base" type="host">https://devdocs.magento.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/frontend/routes.xml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/frontend/routes.xml new file mode 100644 index 0000000000000..f78ddb740ec6c --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/frontend/routes.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:App/etc/routes.xsd"> + <router id="standard"> + <route id="csputil" frontName="csputil"> + <module name="Magento_TestModuleCspUtil" /> + </route> + </router> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/module.xml new file mode 100644 index 0000000000000..1e9bc9b6fa9bc --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/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_TestModuleCspUtil" active="true" /> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/registration.php b/dev/tests/integration/_files/Magento/TestModuleCspUtil/registration.php new file mode 100644 index 0000000000000..570aed9fe5ce6 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleCspUtil') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleCspUtil', __DIR__); +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_aware.xml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_aware.xml new file mode 100644 index 0000000000000..89550403fa6bf --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_aware.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer name="content"> + <block class="Magento\Framework\View\Element\Template" + name="csp_helper" + cacheable="false" + template="Magento_TestModuleCspUtil::helper.phtml" /> + </referenceContainer> + </body> +</page> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_helper.xml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_helper.xml new file mode 100644 index 0000000000000..89550403fa6bf --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_helper.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer name="content"> + <block class="Magento\Framework\View\Element\Template" + name="csp_helper" + cacheable="false" + template="Magento_TestModuleCspUtil::helper.phtml" /> + </referenceContainer> + </body> +</page> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/helper.phtml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/helper.phtml new file mode 100644 index 0000000000000..fa349062aafcb --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/helper.phtml @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Csp\Api\InlineUtilInterface $csp */ +?> +<h1>Hello there!</h1> +<?= /* @noEscape */ $csp->renderTag('script', ['src' => 'http://my.magento.com/static/script.js']); ?> +<?= /* @noEscape */ $csp->renderTag('script', [], "\n let myVar = 1;\n") ?> + diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/secure.phtml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/secure.phtml new file mode 100644 index 0000000000000..f3ed9365d18b4 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/secure.phtml @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> +<?= /* @NoEscape */ $secureRenderer->renderTag('script', ['type' => 'text/javascript'], 'let var = 1; console.log("var = " + var);', false) ?> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php index daf4f7284b2d8..aa21cba143841 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php @@ -10,6 +10,8 @@ /** * Class represent simple container to save data + * + * phpcs:disable Generic.Classes.DuplicateClassName */ class FixtureCallStorage { @@ -30,11 +32,11 @@ public function addFixtureToStorage(string $fixture): void * Get fixture position in storage * * @param string $fixture - * @return false|int + * @return null|int */ - public function getFixturePosition(string $fixture) + public function getFixturePosition(string $fixture): ?int { - return array_search($fixture, $this->storage); + return array_search($fixture, $this->storage) ?: null; } /** diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/composer.json b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/composer.json index 85dfc1f4499e6..47ac2d4ac4a3b 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/composer.json +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/composer.json @@ -5,7 +5,7 @@ "sort-packages": true }, "require": { - "php": "~7.1.3||~7.2.0||~7.3.0", + "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-integration": "*" }, diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml index 8c0badac4b1d1..c0873b9968132 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml @@ -12,6 +12,8 @@ <field id="field_1" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> <field id="field_2" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> <field id="field_3" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> + <field id="field_4" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> + <field id="field_5" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> </group> </section> </system> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml index 3b2f2a1ddde1e..8604428274194 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml @@ -12,6 +12,8 @@ <field_1>1st field default value</field_1> <field_2>2nd field default value</field_2> <field_3>3rd field default value</field_3> + <field_4>4th field default value</field_4> + <field_5>5th field default value</field_5> </test_group> </test_section> </default> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml index 3d0db9f3c4283..aadddfcd2827a 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml @@ -152,12 +152,13 @@ <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> <dataSet name="first_data_set"> - <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> - <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" /> </dataSet> </method> <method name="testReplaceRequiredFixture"> <magentoDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> </method> </test> <test class="Magento\TestModuleOverrideConfig\MagentoDataFixture\SortFixturesTest"> @@ -203,4 +204,9 @@ <dataSet name="first_data_set" skip="true"/> </method> </test> + <global> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/global_fixture_first_module.php" /> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_4" value="4th field globally overridden value"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_5" newValue="5th field globally replaced value"/> + </global> </overrides> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/composer.json b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/composer.json index 315db25a09731..43b7bec56945d 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/composer.json +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/composer.json @@ -5,7 +5,7 @@ "sort-packages": true }, "require": { - "php": "~7.1.3||~7.2.0||~7.3.0", + "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-integration": "*" }, diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml index fd22cc21f6c6a..45c45a79eeafa 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml @@ -35,12 +35,15 @@ <test class="Magento\TestModuleOverrideConfig\MagentoDataFixture\ReplaceFixtureTest"> <method name="testReplaceFixtureViaThirdModule"> <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" /> <dataSet name="first_data_set"> <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> </dataSet> </method> <method name="testReplaceRequiredFixtureViaThirdModule"> <magentoDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" newPath="Magento/TestModuleOverrideConfig3/_files/fixture1_third_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" newPath="Magento/TestModuleOverrideConfig3/_files/fixture1_third_module_rollback.php" /> </method> </test> <test class="Magento\TestModuleOverrideConfig\MagentoDataFixture\SortFixturesTest"> @@ -50,4 +53,58 @@ </dataSet> </method> </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesInterface"> + <magentoAdminConfigFixture path="test_section/test_group/field_1" value="overridden config fixture value for class"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_1" value="overridden config fixture value for class"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <method name="testInterfaceInheritance"> + <magentoAdminConfigFixture path="test_section/test_group/field_2" newValue="overridden config fixture value for method"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_2" newValue="overridden config fixture value for method"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <dataSet name="second_data_set"> + <magentoAdminConfigFixture path="test_section/test_group/field_3" remove="true"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_3" remove="true"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesAbstractClass"> + <method name="testAbstractInheritance"> + <magentoAdminConfigFixture path="test_section/test_group/field_2" remove="true"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_2" remove="true"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <dataSet name="first_data_set"> + <magentoAdminConfigFixture path="test_section/test_group/field_3" value="overridden config fixture value for data set from abstract"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_3" value="overridden config fixture value for data set from abstract"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php"/> + </dataSet> + <dataSet name="second_data_set"> + <magentoAdminConfigFixture path="test_section/test_group/field_1" newValue="overridden config fixture value for data set from abstract"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_1" newValue="overridden config fixture value for data set from abstract"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipAbstractClass"> + <method name="testAbstractSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="first_data_set" skip="true"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipInterface"> + <method name="testInterfaceSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="second_data_set" skip="true"/> + </method> + </test> </overrides> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/composer.json b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/composer.json index 6ada46e46fbe3..432b2ef703a57 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/composer.json +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/composer.json @@ -5,7 +5,7 @@ "sort-packages": true }, "require": { - "php": "~7.1.3||~7.2.0||~7.3.0", + "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-integration": "*" }, diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/Controller/Secure/Helper.php b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/Controller/Secure/Helper.php new file mode 100644 index 0000000000000..4657f10374514 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/Controller/Secure/Helper.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleSecureHtmlRenderer\Controller\Secure; + +use Magento\Framework\App\Action\Action; +use Magento\Framework\Controller\ResultFactory; + +/** + * .phtml template utilizing secure-html helper. + */ +class Helper extends Action +{ + /** + * @inheritDoc + */ + public function execute() + { + return $this->resultFactory->create(ResultFactory::TYPE_PAGE); + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/composer.json b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/composer.json new file mode 100644 index 0000000000000..316d3c8428d08 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-secure-html-renderer", + "description": "test secure html renderer module", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleSecureHtmlRenderer" + ] + ] + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/frontend/routes.xml b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/frontend/routes.xml new file mode 100644 index 0000000000000..8cfe6080149b8 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/frontend/routes.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:App/etc/routes.xsd"> + <router id="standard"> + <route id="securehtml" frontName="securehtml"> + <module name="Magento_TestModuleSecureHtmlRenderer" /> + </route> + </router> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/module.xml new file mode 100644 index 0000000000000..653ce176d4e0e --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/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_TestModuleSecureHtmlRenderer" active="true" /> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/registration.php b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/registration.php new file mode 100644 index 0000000000000..4fff392257f8a --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleSecureHtmlRenderer') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleSecureHtmlRenderer', __DIR__); +} diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/layout/securehtml_secure_helper.xml b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/layout/securehtml_secure_helper.xml new file mode 100644 index 0000000000000..0ea70a15a3299 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/layout/securehtml_secure_helper.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer name="content"> + <block class="Magento\Framework\View\Element\Template" + name="secure_helper" + cacheable="false" + template="Magento_TestModuleSecureHtmlRenderer::helper.phtml" /> + </referenceContainer> + </body> +</page> diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/templates/helper.phtml b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/templates/helper.phtml new file mode 100644 index 0000000000000..a7a5eb9555cc3 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/templates/helper.phtml @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> +<h1 <?= /* @noEscape */$secureRenderer->renderEventListener('onclick', 'alert()') ?>>Hello there!</h1> +<?= /* @noEscape */ $secureRenderer->renderTag('script', ['src' => 'http://my.magento.com/static/script.js'], false); ?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], "\n let myVar = 1;\n", false) ?> +<?= /* @noEscape */ $secureRenderer->renderTag('div', [], 'I am just <a> text', true) ?> + diff --git a/dev/tests/integration/bin/magento b/dev/tests/integration/bin/magento index 303fbfb217d2b..8226f5c711708 100755 --- a/dev/tests/integration/bin/magento +++ b/dev/tests/integration/bin/magento @@ -5,6 +5,9 @@ * See COPYING.txt for license details. */ +use Magento\Framework\Console\Cli; +use Magento\TestFramework\Console\CliProxy; + if (PHP_SAPI !== 'cli') { echo 'bin/magento must be run as a CLI application'; exit(1); @@ -21,7 +24,8 @@ if (isset($_SERVER['INTEGRATION_TEST_PARAMS'])) { } try { - require $_SERVER['MAGE_DIRS']['base']['path'] . '/app/bootstrap.php'; + require $_SERVER['INTEGRATION_TESTS_CLI_AUTOLOADER'] ?? + ($_SERVER['MAGE_DIRS']['base']['path'] . '/app/bootstrap.php'); } catch (\Exception $e) { echo 'Autoload error: ' . $e->getMessage(); exit(1); @@ -29,7 +33,11 @@ try { try { $handler = new \Magento\Framework\App\ErrorHandler(); set_error_handler([$handler, 'handler']); - $application = new Magento\Framework\Console\Cli('Magento CLI'); + if (isset($_SERVER['INTEGRATION_TESTS_CLI_AUTOLOADER'])) { + $application = new CliProxy('Magento CLI'); + } else { + $application = new Cli('Magento CLI'); + } $application->run(); } catch (\Exception $e) { while ($e) { diff --git a/dev/tests/integration/etc/di/preferences/ce.php b/dev/tests/integration/etc/di/preferences/ce.php index 68de1cc009d68..9374fb4dfe5df 100644 --- a/dev/tests/integration/etc/di/preferences/ce.php +++ b/dev/tests/integration/etc/di/preferences/ce.php @@ -30,11 +30,5 @@ \Magento\Framework\Lock\Backend\Database::class => \Magento\TestFramework\Lock\Backend\DummyLocker::class, \Magento\Framework\Session\SessionStartChecker::class => \Magento\TestFramework\Session\SessionStartChecker::class, - \Magento\Framework\HTTP\AsyncClientInterface::class => \Magento\TestFramework\HTTP\AsyncClientInterfaceMock::class, - \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class => - \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class, - \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => - \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class, - \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => - \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + \Magento\Framework\HTTP\AsyncClientInterface::class => \Magento\TestFramework\HTTP\AsyncClientInterfaceMock::class ]; 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/etc/install-config-mysql.travis.php.dist b/dev/tests/integration/etc/install-config-mysql.travis.php.dist deleted file mode 100644 index 8c41b0a0f2626..0000000000000 --- a/dev/tests/integration/etc/install-config-mysql.travis.php.dist +++ /dev/null @@ -1,23 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -return [ - 'db-host' => '127.0.0.1', - 'db-user' => 'root', - 'db-password' => '', - 'db-name' => 'magento_integration_tests', - 'db-prefix' => 'trv_', - 'backend-frontname' => 'backend', - 'admin-user' => \Magento\TestFramework\Bootstrap::ADMIN_NAME, - 'admin-password' => \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD, - 'admin-email' => \Magento\TestFramework\Bootstrap::ADMIN_EMAIL, - 'admin-firstname' => \Magento\TestFramework\Bootstrap::ADMIN_FIRSTNAME, - 'admin-lastname' => \Magento\TestFramework\Bootstrap::ADMIN_LASTNAME, - 'amqp-host' => 'localhost', - 'amqp-port' => '5672', - 'amqp-user' => 'guest', - 'amqp-password' => 'guest', -]; diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/AbstractDataFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/AbstractDataFixture.php index a835e73cce826..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,11 +104,18 @@ protected function _applyOneFixture($fixture) * Execute fixture scripts if any * * @param array $fixtures + * @param TestCase $test * @return void - * @throws LocalizedException */ - 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); @@ -123,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()); @@ -150,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/ConfigFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/ConfigFixture.php index e2c54584db41d..dc3971d1896a1 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/ConfigFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/ConfigFixture.php @@ -35,26 +35,21 @@ class ConfigFixture * * @var array */ - private $globalConfigValues = []; + protected $globalConfigValues = []; /** * Original values for website-scoped configuration options that need to be restored * * @var array */ - private $websiteConfigValues = []; + protected $websiteConfigValues = []; /** * Original values for store-scoped configuration options that need to be restored * * @var array */ - private $storeConfigValues = []; - - /** - * @var string - */ - protected $annotation = 'magentoConfigFixture'; + protected $storeConfigValues = []; /** * Retrieve configuration node value @@ -74,9 +69,9 @@ protected function _getConfigValue($configPath, $scopeCode = null) * @param string $configPath * @param string $scopeType * @param string|null $scopeCode - * @return string|null + * @return mixed|null */ - protected function getScopeConfigValue(string $configPath, string $scopeType, string $scopeCode = null): ?string + protected function getScopeConfigValue(string $configPath, string $scopeType, string $scopeCode = null) { $result = null; if ($scopeCode !== false) { @@ -163,34 +158,67 @@ protected function _assignConfigData(TestCase $test) self::ANNOTATION ); foreach ($testAnnotations as $configPathAndValue) { - if (preg_match('/^.+?(?=_store\s)/', $configPathAndValue, $matches)) { - /* Store-scoped config value */ - $storeCode = $matches[0] != 'current' ? $matches[0] : null; - $parts = preg_split('/\s+/', $configPathAndValue, 3); - list($configScope, $configPath, $requiredValue) = $parts + ['', '', '']; - $originalValue = $this->_getConfigValue($configPath, $storeCode); - $this->storeConfigValues[$storeCode][$configPath] = $originalValue; - $this->_setConfigValue($configPath, $requiredValue, $storeCode); - } elseif (preg_match('/^.+?(?=_website\s)/', $configPathAndValue, $matches)) { - /* Website-scoped config value */ - $websiteCode = $matches[0] != 'current' ? $matches[0] : null; - $parts = preg_split('/\s+/', $configPathAndValue, 3); - list($configScope, $configPath, $requiredValue) = $parts + ['', '', '']; - $originalValue = $this->getScopeConfigValue($configPath, ScopeInterface::SCOPE_WEBSITES, $websiteCode); - $this->websiteConfigValues[$websiteCode][$configPath] = $originalValue; - $this->setScopeConfigValue($configPath, $requiredValue, ScopeInterface::SCOPE_WEBSITES, $websiteCode); + if (preg_match('/^[^\/]+?(?=_store\s)/', $configPathAndValue, $matches)) { + $this->setStoreConfigValue($matches ?? [], $configPathAndValue); + } elseif (preg_match('/^[^\/]+?(?=_website\s)/', $configPathAndValue, $matches)) { + $this->setWebsiteConfigValue($matches ?? [], $configPathAndValue); } else { - /* Global config value */ - list($configPath, $requiredValue) = preg_split('/\s+/', $configPathAndValue, 2); - - $originalValue = $this->_getConfigValue($configPath); - $this->globalConfigValues[$configPath] = $originalValue; - - $this->_setConfigValue($configPath, $requiredValue); + $this->setGlobalConfigValue($configPathAndValue); } } } + /** + * Sets store-scoped config value + * + * @param array $matches + * @param string $configPathAndValue + * @return void + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + protected function setStoreConfigValue(array $matches, $configPathAndValue): void + { + $storeCode = $matches[0] != 'current' ? $matches[0] : null; + $parts = preg_split('/\s+/', $configPathAndValue, 3); + list($configScope, $configPath, $requiredValue) = $parts + ['', '', '']; + $originalValue = $this->_getConfigValue($configPath, $storeCode); + $this->storeConfigValues[$storeCode][$configPath] = $originalValue; + $this->_setConfigValue($configPath, $requiredValue, $storeCode); + } + + /** + * Sets website-scoped config value + * + * @param array $matches + * @param string $configPathAndValue + * @return void + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + protected function setWebsiteConfigValue(array $matches, $configPathAndValue): void + { + $websiteCode = $matches[0] != 'current' ? $matches[0] : null; + $parts = preg_split('/\s+/', $configPathAndValue, 3); + list($configScope, $configPath, $requiredValue) = $parts + ['', '', '']; + $originalValue = $this->getScopeConfigValue($configPath, ScopeInterface::SCOPE_WEBSITES, $websiteCode); + $this->websiteConfigValues[$websiteCode][$configPath] = $originalValue; + $this->setScopeConfigValue($configPath, $requiredValue, ScopeInterface::SCOPE_WEBSITES, $websiteCode); + } + + /** + * Sets global config value + * + * @param string $configPathAndValue + * @return void + */ + protected function setGlobalConfigValue($configPathAndValue): void + { + /* Global config value */ + list($configPath, $requiredValue) = preg_split('/\s+/', $configPathAndValue, 2); + $originalValue = $this->_getConfigValue($configPath); + $this->globalConfigValues[$configPath] = $originalValue; + $this->_setConfigValue($configPath, $requiredValue); + } + /** * Restore original values for changed config options * 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/Application.php b/dev/tests/integration/framework/Magento/TestFramework/Application.php index f0ce2e24545eb..e39e7c0f05bd8 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Application.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Application.php @@ -168,10 +168,12 @@ public function __construct( $loadTestExtensionAttributes = false ) { if (getcwd() != BP . '/dev/tests/integration') { + // phpcs:ignore Magento2.Functions.DiscouragedFunction chdir(BP . '/dev/tests/integration'); } $this->_shell = $shell; $this->installConfigFile = $installConfigFile; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $this->_globalConfigDir = realpath($globalConfigDir); $this->_appMode = $appMode; $this->installDir = $installDir; @@ -251,6 +253,7 @@ public function getDbInstance() protected function getInstallConfig() { if (null === $this->installConfig) { + // phpcs:ignore Magento2.Security.IncludeFile $this->installConfig = include $this->installConfigFile; } return $this->installConfig; @@ -293,6 +296,7 @@ public function getInitParams() */ public function isInstalled() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction return is_file($this->getLocalConfig()); } @@ -371,7 +375,7 @@ public function initialize($overriddenParams = []) \Magento\Framework\Mail\TransportInterface::class => \Magento\TestFramework\Mail\TransportInterfaceMock::class, \Magento\Framework\Mail\Template\TransportBuilder::class - => \Magento\TestFramework\Mail\Template\TransportBuilderMock::class, + => \Magento\TestFramework\Mail\Template\TransportBuilderMock::class, ] ]; if ($this->loadTestExtensionAttributes) { @@ -552,8 +556,10 @@ private function copyAppConfigFiles() ); foreach ($globalConfigFiles as $file) { $targetFile = $this->_configDir . str_replace($this->_globalConfigDir, '', $file); + // phpcs:ignore Magento2.Functions.DiscouragedFunction $this->_ensureDirExists(dirname($targetFile)); if ($file !== $targetFile) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction copy($file, $targetFile); } } @@ -567,6 +573,7 @@ private function copyAppConfigFiles() private function copyGlobalConfigFile() { $targetFile = $this->_configDir . '/config.local.php'; + // phpcs:ignore Magento2.Functions.DiscouragedFunction copy($this->globalConfigFile, $targetFile); } @@ -585,7 +592,7 @@ private function getInstallCliParams() $params['magento-init-params'] = $this->getInitParamsQuery(); $result = []; foreach ($params as $key => $value) { - if (!empty($value)) { + if (isset($value)) { $result["--{$key}=%s"] = $value; } } @@ -636,10 +643,13 @@ protected function _resetApp() */ protected function _ensureDirExists($dir) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction if (!file_exists($dir)) { $old = umask(0); + // phpcs:ignore Magento2.Functions.DiscouragedFunction mkdir($dir, 0777, true); umask($old); + // phpcs:ignore Magento2.Functions.DiscouragedFunction } elseif (!is_dir($dir)) { throw new \Magento\Framework\Exception\LocalizedException(__("'%1' is not a directory.", $dir)); } @@ -704,6 +714,7 @@ protected function getCustomDirs() $customDirs = [ DirectoryList::CONFIG => [$path => "{$this->installDir}/etc"], DirectoryList::VAR_DIR => [$path => $var], + DirectoryList::VAR_EXPORT => [$path => "{$var}/export"], DirectoryList::MEDIA => [$path => "{$this->installDir}/pub/media"], DirectoryList::STATIC_VIEW => [$path => "{$this->installDir}/pub/static"], DirectoryList::TMP_MATERIALIZATION_DIR => [$path => "{$var}/view_preprocessed/pub/static"], diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php index d2aa20a005ec4..35f449a404410 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php @@ -8,7 +8,7 @@ namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; use Magento\Catalog\Api\Data\ProductCustomOptionInterface; -use Magento\Catalog\Model\Product\Option; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractBase; /** * Data provider for options from file group with type "file". @@ -20,44 +20,41 @@ class File extends AbstractBase */ public function getDataForCreateOptions(): array { - return $this->injectFileExtension( - array_merge_recursive( - parent::getDataForCreateOptions(), - [ - "type_{$this->getType()}_option_file_extension" => [ - [ - 'record_id' => 0, - 'sort_order' => 1, - 'is_require' => 1, - 'sku' => 'test-option-title-1', - 'max_characters' => 30, - 'title' => 'Test option title 1', - 'type' => $this->getType(), - 'price' => 10, - 'price_type' => 'fixed', - 'file_extension' => 'gif', - 'image_size_x' => 10, - 'image_size_y' => 20, - ], + return array_merge_recursive( + parent::getDataForCreateOptions(), + [ + "type_{$this->getType()}_option_file_extension" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 30, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + 'file_extension' => 'gif', + 'image_size_x' => 10, + 'image_size_y' => 20, ], - "type_{$this->getType()}_option_maximum_file_size" => [ - [ - 'record_id' => 0, - 'sort_order' => 1, - 'is_require' => 1, - 'sku' => 'test-option-title-1', - 'title' => 'Test option title 1', - 'type' => $this->getType(), - 'price' => 10, - 'price_type' => 'fixed', - 'file_extension' => 'gif', - 'image_size_x' => 10, - 'image_size_y' => 20, - ], + ], + "type_{$this->getType()}_option_maximum_file_size" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + 'file_extension' => 'gif', + 'image_size_x' => 10, + 'image_size_y' => 20, ], - ] - ), - 'png' + ], + ] ); } @@ -66,24 +63,21 @@ public function getDataForCreateOptions(): array */ public function getDataForUpdateOptions(): array { - return $this->injectFileExtension( - array_merge_recursive( - parent::getDataForUpdateOptions(), - [ - "type_{$this->getType()}_option_file_extension" => [ - [ - 'file_extension' => 'jpg', - ], + return array_merge_recursive( + parent::getDataForUpdateOptions(), + [ + "type_{$this->getType()}_option_file_extension" => [ + [ + 'file_extension' => 'jpg', ], - "type_{$this->getType()}_option_maximum_file_size" => [ - [ - 'image_size_x' => 300, - 'image_size_y' => 815, - ], + ], + "type_{$this->getType()}_option_maximum_file_size" => [ + [ + 'image_size_x' => 300, + 'image_size_y' => 815, ], - ] - ), - '' + ], + ] ); } @@ -94,24 +88,4 @@ protected function getType(): string { return ProductCustomOptionInterface::OPTION_TYPE_FILE; } - - /** - * Add 'file_extension' value to each option. - * - * @param array $data - * @param string $extension - * @return array - */ - private function injectFileExtension(array $data, string $extension): array - { - foreach ($data as &$caseData) { - foreach ($caseData as &$option) { - if (!isset($option[Option::KEY_FILE_EXTENSION])) { - $option[Option::KEY_FILE_EXTENSION] = $extension; - } - } - } - - return $data; - } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/ValidatorFileMock.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/Type/File/ValidatorFileMock.php similarity index 84% rename from dev/tests/integration/testsuite/Magento/Checkout/_files/ValidatorFileMock.php rename to dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/Type/File/ValidatorFileMock.php index 9b5650b1826c3..b7df1205f2ba3 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/ValidatorFileMock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/Type/File/ValidatorFileMock.php @@ -5,19 +5,22 @@ */ declare(strict_types=1); -namespace Magento\Checkout\_files; +namespace Magento\TestFramework\Catalog\Model\Product\Option\Type\File; use Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * Creates mock for ValidatorFile to replace real instance in fixtures. */ -class ValidatorFileMock extends \PHPUnit\Framework\TestCase +class ValidatorFileMock extends TestCase { /** * Returns mock. + * * @param array|null $fileData - * @return ValidatorFile|\PHPUnit_Framework_MockObject_MockObject + * @return ValidatorFile|MockObject */ public function getInstance($fileData = null) { diff --git a/dev/tests/integration/framework/Magento/TestFramework/Config/Model/ConfigStorage.php b/dev/tests/integration/framework/Magento/TestFramework/Config/Model/ConfigStorage.php new file mode 100644 index 0000000000000..7fe25f3a6f61c --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Config/Model/ConfigStorage.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Config\Model; + +use Magento\Framework\App\Config\ConfigResource\ConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Class checks config table data using direct calls + */ +class ConfigStorage +{ + /** @var ConfigInterface */ + private $configResource; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** + * @param ConfigInterface $configResource + * @param StoreManagerInterface $storeManager + */ + public function __construct(ConfigInterface $configResource, StoreManagerInterface $storeManager) + { + $this->configResource = $configResource; + $this->storeManager = $storeManager; + } + + /** + * Get value from db + * + * @param string $path + * @param string $scope + * @param string|null $scopeCode + * @return string|false + */ + public function getValueFromDb( + string $path, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + ?string $scopeCode = null + ) { + $connect = $this->configResource->getConnection(); + $scope = $this->normalizeScope($scope); + $scopeId = $this->getIdByScope($scope, $scopeCode); + + $select = $connect->select()->from(['main_table' => $this->configResource->getMainTable()], 'value') + ->where('main_table.path = ?', $path) + ->where('main_table.scope = ?', $scope) + ->where('main_table.scope_id = ?', $scopeId); + + return $connect->fetchOne($select); + } + + /** + * Check is record exist in DB + * + * @param string $path + * @param string $scope + * @param string|null $scopeCode + * @return bool + */ + public function checkIsRecordExist( + string $path, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + ?string $scopeCode = null + ): bool { + $connect = $this->configResource->getConnection(); + $scope = $this->normalizeScope($scope); + $scopeId = $this->getIdByScope($scope, $scopeCode); + + $select = $connect->select()->from(['main_table' => $this->configResource->getMainTable()], 'COUNT(*)') + ->where('main_table.path = ?', $path) + ->where('main_table.scope = ?', $scope) + ->where('main_table.scope_id = ?', $scopeId); + + return (bool)$connect->fetchOne($select); + } + + /** + * Get scope id by scope code + * + * @param string $scope + * @param string|null $scopeCode + * @return int + */ + private function getIdByScope(string $scope, ?string $scopeCode): int + { + $scopeId = 0; + + if ($scope === ScopeInterface::SCOPE_WEBSITES) { + $scopeId = (int)$this->storeManager->getWebsite($scopeCode)->getId(); + } elseif ($scope === ScopeInterface::SCOPE_STORES) { + $scopeId = (int)$this->storeManager->getStore($scopeCode)->getId(); + } + + return $scopeId; + } + + /** + * Normalize scope + * + * @param string $scope + * @return string + */ + private function normalizeScope(string $scope): string + { + if ($scope === ScopeInterface::SCOPE_WEBSITE) { + $scope = ScopeInterface::SCOPE_WEBSITES; + } + if ($scope === ScopeInterface::SCOPE_STORE) { + $scope = ScopeInterface::SCOPE_STORES; + } + + return $scope; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php b/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php new file mode 100644 index 0000000000000..497f234dfa84b --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Console; + +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManagerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Provides the ability to inject additional DI configuration to call a CLI command + */ +class CliProxy implements \Magento\Framework\ObjectManager\NoninterceptableInterface +{ + /** + * @var Cli + */ + private $subject; + + /** + * @param string $name + * @param string $version + * @throws \ReflectionException + * @throws LocalizedException + */ + public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') + { + $this->subject = new Cli($name, $version); + $this->injectDiConfiguration($this->subject); + } + + /** + * Runs the current application. + * + * @see \Magento\Framework\Console\Cli::doRun + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null + * @throws \Exception + */ + public function doRun(InputInterface $input, OutputInterface $output) + { + return $this->getSubject()->doRun($input, $output); + } + + /** + * Runs the current application. + * + * @see \Symfony\Component\Console\Application::run + * @param InputInterface|null $input + * @param OutputInterface|null $output + * @return int + * @throws \Exception + */ + public function run(InputInterface $input = null, OutputInterface $output = null) + { + return $this->getSubject()->run($input, $output); + } + + /** + * Get subject + * + * @return Cli + */ + private function getSubject(): Cli + { + return $this->subject; + } + + /** + * Inject additional DI configuration + * + * @param Cli $cli + * @return bool + * @throws LocalizedException + * @throws \ReflectionException + */ + private function injectDiConfiguration(Cli $cli): bool + { + $diPreferences = $this->getDiPreferences(); + if ($diPreferences) { + $object = new \ReflectionObject($cli); + + $attribute = $object->getProperty('objectManager'); + $attribute->setAccessible(true); + + /** @var ObjectManagerInterface $objectManager */ + $objectManager = $attribute->getValue($cli); + $objectManager->configure($diPreferences); + + $attribute->setAccessible(false); + } + + return true; + } + + /** + * Get additional DI preferences + * + * @return array|array[] + * @throws LocalizedException + * @SuppressWarnings(PHPMD.Superglobals) + */ + private function getDiPreferences(): array + { + $diPreferences = []; + $diPreferencesPath = $_SERVER['TESTS_BASE_DIR'] . '/etc/di/preferences/cli/'; + + $preferenceFiles = glob($diPreferencesPath . '*.php'); + + foreach ($preferenceFiles as $file) { + if (!is_readable($file)) { + throw new LocalizedException(__("'%1' is not readable file.", $file)); + } + $diPreferences = array_replace($diPreferences, include $file); + } + + return $diPreferences ? ['preferences' => $diPreferences] : $diPreferences; + } +} 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/Customer/Model/DeleteCustomer.php b/dev/tests/integration/framework/Magento/TestFramework/Customer/Model/DeleteCustomer.php new file mode 100644 index 0000000000000..5158191efef22 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Customer/Model/DeleteCustomer.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Customer\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; + +/** + * Delete customer by email or id + */ +class DeleteCustomer +{ + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var Registry */ + private $registry; + + /** + * @param CustomerRepositoryInterface $customerRepository + * @param Registry $registry + */ + public function __construct(CustomerRepositoryInterface $customerRepository, Registry $registry) + { + $this->customerRepository = $customerRepository; + $this->registry = $registry; + } + + /** + * Delete customer by id or email + * + * @param int|string $id + * @return void + */ + public function execute($id): void + { + $isSecure = $this->registry->registry('isSecureArea'); + + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + try { + $customer = is_numeric($id) ? $this->customerRepository->getById($id) : $this->customerRepository->get($id); + $this->customerRepository->delete($customer); + } catch (NoSuchEntityException $e) { + //customer already deleted + } + + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', $isSecure); + } +} 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/Magento/TestFramework/Interception/PluginList.php b/dev/tests/integration/framework/Magento/TestFramework/Interception/PluginList.php index 88c9086f8270b..0b5fc407d438b 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Interception/PluginList.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Interception/PluginList.php @@ -5,6 +5,8 @@ */ namespace Magento\TestFramework\Interception; +use Magento\Framework\Interception\ConfigLoaderInterface; +use Magento\Framework\Interception\PluginListGenerator; use Magento\Framework\Serialize\SerializerInterface; /** @@ -31,6 +33,8 @@ class PluginList extends \Magento\Framework\Interception\PluginList\PluginList * @param array $scopePriorityScheme * @param string|null $cacheId * @param SerializerInterface|null $serializer + * @param ConfigLoaderInterface|null $configLoader + * @param PluginListGenerator|null $pluginListGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -44,7 +48,9 @@ public function __construct( \Magento\Framework\ObjectManager\DefinitionInterface $classDefinitions, array $scopePriorityScheme, $cacheId = 'plugins', - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + ConfigLoaderInterface $configLoader = null, + PluginListGenerator $pluginListGenerator = null ) { parent::__construct( $reader, @@ -57,7 +63,9 @@ public function __construct( $classDefinitions, $scopePriorityScheme, $cacheId, - $serializer + $serializer, + $configLoader, + $pluginListGenerator ); $this->_originScopeScheme = $this->_scopePriorityScheme; } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Serialize/Serializer.php b/dev/tests/integration/framework/Magento/TestFramework/Serialize/Serializer.php new file mode 100644 index 0000000000000..60f2a7ce6c4cc --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Serialize/Serializer.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestFramework\Serialize; + +use Magento\Framework\Serialize\SerializerInterface; + +/** + * Insecure SerializerInterface implementation for test use only. + */ +class Serializer implements SerializerInterface +{ + /** + * @inheritDoc + */ + public function serialize($data) + { + if (is_resource($data)) { + throw new \InvalidArgumentException('Unable to serialize value.'); + } + + // phpcs:ignore Magento2.Security.InsecureFunction + return serialize($data); + } + + /** + * @inheritDoc + */ + public function unserialize($string) + { + if (false === $string || null === $string || '' === $string) { + throw new \InvalidArgumentException('Unable to unserialize value.'); + } + set_error_handler( + function () { + restore_error_handler(); + throw new \InvalidArgumentException('Unable to unserialize value, string is corrupted.'); + }, + E_NOTICE + ); + // phpcs:ignore Magento2.Security.InsecureFunction + $result = unserialize($string); + restore_error_handler(); + + return $result; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php index bf8ca0dc51b18..fff16a7edc1ba 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php @@ -58,7 +58,14 @@ protected function setUp(): void parent::setUp(); $this->_objectManager->get(\Magento\Backend\Model\UrlInterface::class)->turnOffSecretKey(); - + /** + * Authorization can be created on test bootstrap... + * If it will be created on test bootstrap we will have invalid RoleLocator object. + * As tests by default are run not from adminhtml area... + */ + \Magento\TestFramework\ObjectManager::getInstance()->removeSharedInstance( + \Magento\Framework\Authorization::class + ); $this->_auth = $this->_objectManager->get(\Magento\Backend\Model\Auth::class); $this->_session = $this->_auth->getAuthStorage(); $credentials = $this->_getAdminCredentials(); diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php index 73786707b417b..495e14224d482 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php @@ -46,6 +46,8 @@ class StaticProperties \Magento\TestFramework\Annotation\AppIsolation::class, \Magento\TestFramework\Workaround\Cleanup\StaticProperties::class, \Magento\Framework\Phrase::class, + \Magento\TestFramework\Workaround\Override\Fixture\ResolverInterface::class, + \Magento\TestFramework\Workaround\Override\ConfigInterface::class, ]; private const CACHE_NAME = 'integration_test_static_properties'; @@ -79,7 +81,7 @@ public function __construct() */ protected static function _isClassCleanable(\ReflectionClass $reflectionClass) { - // do not process blacklisted classes from integration framework + // do not process skipped classes from integration framework foreach (self::$_classesToSkip as $notCleanableClass) { if ($reflectionClass->getName() == $notCleanableClass || is_subclass_of( $reflectionClass->getName(), @@ -168,7 +170,7 @@ public static function backupStaticVariables() $objectManager = Bootstrap::getInstance()->getObjectManager(); $cache = $objectManager->get(CacheInterface::class); - $serializer = $objectManager->get(SerializerInterface::class); + $serializer = $objectManager->get(\Magento\TestFramework\Serialize\Serializer::class); $cachedProperties = $cache->load(self::CACHE_NAME); if ($cachedProperties) { @@ -185,9 +187,10 @@ public static function backupStaticVariables() | Files::INCLUDE_TESTS ), function ($classFile) { - return StaticProperties::_isClassInCleanableFolders($classFile) - // phpcs:ignore Magento2.Functions.DiscouragedFunction - && strpos(file_get_contents($classFile), ' static ') > 0; + return strpos($classFile, 'TestFramework') === -1 + && StaticProperties::_isClassInCleanableFolders($classFile) + // phpcs:ignore Magento2.Functions.DiscouragedFunction + && strpos(file_get_contents($classFile), ' static ') > 0; } ); $namespacePattern = '/namespace [a-zA-Z0-9\\\\]+;/'; diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php index 5d2d0e385b9e3..f34eec274873d 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php @@ -8,13 +8,22 @@ namespace Magento\TestFramework\Workaround\Override; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\ConverterInterface; use Magento\Framework\Config\Reader\Filesystem; +use Magento\Framework\Config\SchemaLocatorInterface; +use Magento\Framework\Config\ValidationStateInterface; use Magento\Framework\View\File\Collector\Decorator\ModuleDependency; use Magento\Framework\View\File\Collector\Decorator\ModuleOutput; +use Magento\Framework\View\File\CollectorInterface; +use Magento\TestFramework\Annotation\AdminConfigFixture; +use Magento\TestFramework\Annotation\ConfigFixture; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; use Magento\TestFramework\Workaround\Override\Config\Converter; +use Magento\TestFramework\Workaround\Override\Config\Dom; use Magento\TestFramework\Workaround\Override\Config\FileCollector; use Magento\TestFramework\Workaround\Override\Config\FileResolver; -use Magento\TestFramework\Workaround\Override\Config\Dom; +use Magento\TestFramework\Workaround\Override\Config\RelationsCollector; use Magento\TestFramework\Workaround\Override\Config\SchemaLocator; use Magento\TestFramework\Workaround\Override\Config\ValidationState; use PHPUnit\Framework\TestCase; @@ -24,10 +33,20 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Config +class Config implements ConfigInterface { /** - * @var self + * List of allowed fixture types + */ + protected const FIXTURE_TYPES = [ + DataFixture::ANNOTATION, + DataFixtureBeforeTransaction::ANNOTATION, + ConfigFixture::ANNOTATION, + AdminConfigFixture::ANNOTATION, + ]; + + /** + * @var ConfigInterface */ private static $instance; @@ -37,152 +56,136 @@ class Config private $config; /** - * @param array $config + * @var array */ - public function __construct(array $config) - { - $this->config = $config; - } + private $inheritedConfig; /** - * Returns an array with skip key and skipMessage key if test is skipped. + * Self instance getter. * - * @param TestCase $test - * @return array + * @return ConfigInterface */ - public function getSkipConfiguration(TestCase $test): array + public static function getInstance(): ConfigInterface { - $classConfig = $this->getClassConfig($test); - $testConfig = $this->getMethodConfig($test); - $dataSetConfig = $this->getDataSetConfig($test); - $result['skip'] = false; - - if (isset($dataSetConfig['skip']) && $dataSetConfig['skip']) { - $result = $this->prepareSkipConfig($dataSetConfig); - } elseif (isset($testConfig['skip']) && $testConfig['skip']) { - $result = $this->prepareSkipConfig($testConfig); - } elseif (isset($classConfig['skip']) && $classConfig['skip']) { - $result = $this->prepareSkipConfig($classConfig); + if (empty(self::$instance)) { + throw new \RuntimeException('Override config isn\'t initialized'); } - return $result; + return self::$instance; } /** - * Test has configuration flag. + * Get config from global node * - * @param string $className - * @return bool + * @param string|null $fixtureType + * @return array */ - public function hasSkippedTest(string $className): bool + public function getGlobalConfig(?string $fixtureType = null): array { - $classConfig = $this->config[$className] ?? []; + $result = $this->config['global'] ?? []; + if ($fixtureType) { + $result = $result[$fixtureType] ?? []; + } - return $this->isSkippedByConfig($classConfig); + return $result; } /** - * Check that class has even one test skipped + * Self instance setter. * - * @param array $config - * @return bool + * @param ConfigInterface $config + * @return void */ - private function isSkippedByConfig(array $config): bool + public static function setInstance(ConfigInterface $config): void { - if (isset($config['skip']) && $config['skip']) { - return true; - } - - foreach ($config as $lowerLevelConfig) { - if (is_array($lowerLevelConfig)) { - return $this->isSkippedByConfig($lowerLevelConfig); - } - } - - return false; + self::$instance = $config; } /** - * Self instance getter. + * Reads configuration from files. * - * @return static + * @return void */ - public static function getInstance(): self + public function init(): void { - if (empty(self::$instance)) { + if (empty($this->config)) { $data = []; - $objectManager = ObjectManager::getInstance(); $useConfig = (defined('USE_OVERRIDE_CONFIG') && USE_OVERRIDE_CONFIG === 'enabled'); if ($useConfig) { - $fileResolver = $objectManager->create( - FileResolver::class, - [ - 'baseFiles' => $objectManager->create( - ModuleDependency::class, - [ - 'subject' => $objectManager->create( - ModuleOutput::class, - [ - 'subject' => $objectManager->create(FileCollector::class) - ] - ) - ] - ) - ] - ); - $reader = $objectManager->create( + $reader = ObjectManager::getInstance()->create( Filesystem::class, [ 'fileName' => 'overrides.xml', - 'fileResolver' => $fileResolver, + 'fileResolver' => $this->getFileResolver(), 'idAttributes' => [ '/overrides/test' => 'class', '/overrides/test/method' => 'name', '/overrides/test/method/dataSet' => 'name', ], - 'schemaLocator' => $objectManager->create(SchemaLocator::class), - 'validationState' => $objectManager->create(ValidationState::class), - 'converter' => $objectManager->create(Converter::class), - 'domDocumentClass' => Dom::class, + 'schemaLocator' => $this->getSchemaLocator(), + 'validationState' => $this->getValidationState(), + 'converter' => $this->getConverter(), + 'domDocumentClass' => $this->getDomClass(), ] ); $data = $reader->read(); } - self::$instance = new self($data); + $this->config = $data; } - - return self::$instance; } /** - * Get config from class node - * - * @param TestCase $test - * @param string|null $fixtureType - * @return array + * @inheritdoc */ - public function getClassConfig(TestCase $test, ?string $fixtureType = null): array + public function getSkipConfiguration(TestCase $test): array { - $result = $this->config[$this->getOriginalClassName($test)] ?? []; - if ($fixtureType) { - $result = $result[$fixtureType] ?? []; + $classConfig = $this->getClassConfig($test); + $testConfig = $this->getMethodConfig($test); + $dataSetConfig = $this->getDataSetConfig($test); + $result['skip'] = false; + + if (isset($dataSetConfig['skip']) && $dataSetConfig['skip']) { + $result = $this->prepareSkipConfig($dataSetConfig); + } elseif (isset($testConfig['skip']) && $testConfig['skip']) { + $result = $this->prepareSkipConfig($testConfig); + } elseif (isset($classConfig['skip']) && $classConfig['skip']) { + $result = $this->prepareSkipConfig($classConfig); } return $result; } /** - * Get config from method node - * - * @param TestCase $test - * @param string|null $fixtureType - * @return array + * @inheritdoc + */ + public function hasSkippedTest(string $className): bool + { + $classConfig = $this->getInheritedClassConfig($className); + + return $this->isSkippedByConfig($classConfig); + } + + /** + * @inheritdoc + */ + public function getClassConfig(TestCase $test, ?string $fixtureType = null): array + { + $config = $this->getInheritedClassConfig($this->getOriginalClassName($test)); + + return $fixtureType + ? $config[$fixtureType] ?? [] + : $config; + } + + /** + * @inheritdoc */ public function getMethodConfig(TestCase $test, ?string $fixtureType = null): array { $config = $this->getClassConfig($test)[$test->getName(false)] ?? []; + if ($fixtureType) { $config = $config[$fixtureType] ?? []; } @@ -191,11 +194,7 @@ public function getMethodConfig(TestCase $test, ?string $fixtureType = null): ar } /** - * Get config from dataSet node - * - * @param TestCase $test - * @param string|null $fixtureType - * @return array + * @inheritdoc */ public function getDataSetConfig(TestCase $test, ?string $fixtureType = null): array { @@ -207,6 +206,106 @@ public function getDataSetConfig(TestCase $test, ?string $fixtureType = null): a return $config; } + /** + * Returns file resolver. + * + * @return FileResolver + */ + protected function getFileResolver(): FileResolver + { + return ObjectManager::getInstance()->create( + FileResolver::class, + [ + 'baseFiles' => ObjectManager::getInstance()->create( + ModuleDependency::class, + [ + 'subject' => ObjectManager::getInstance()->create( + ModuleOutput::class, + [ + 'subject' => $this->getFileCollector() + ] + ) + ] + ) + ] + ); + } + + /** + * Returns schema locator. + * + * @return SchemaLocatorInterface + */ + protected function getSchemaLocator(): SchemaLocatorInterface + { + return ObjectManager::getInstance()->create(SchemaLocator::class); + } + + /** + * Returns validation state. + * + * @return ValidationStateInterface + */ + protected function getValidationState(): ValidationStateInterface + { + return ObjectManager::getInstance()->create(ValidationState::class); + } + + /** + * Returns converter for config files. + * + * @return ConverterInterface + */ + protected function getConverter(): ConverterInterface + { + return ObjectManager::getInstance()->create(Converter::class, ['types' => $this::FIXTURE_TYPES]); + } + + /** + * Returns DOM class name. + * + * @return string + */ + protected function getDomClass(): string + { + return Dom::class; + } + + /** + * Returns file collector. + * + * @return CollectorInterface + */ + protected function getFileCollector(): CollectorInterface + { + return ObjectManager::getInstance()->create(FileCollector::class); + } + + /** + * Check that class has even one test skipped + * + * @param array $config + * @return bool + */ + private function isSkippedByConfig(array $config): bool + { + $result = false; + if (isset($config['skip']) && $config['skip']) { + $result = true; + } else { + foreach ($config as $lowerLevelConfig) { + if (is_array($lowerLevelConfig)) { + $result = $this->isSkippedByConfig($lowerLevelConfig); + if ($result === true) { + break; + } + } + } + } + + return $result; + } + /** * Returns original test class name. * @@ -231,4 +330,70 @@ private function prepareSkipConfig(array $config): array 'skipMessage' => $config['skipMessage'] ?: 'Skipped according to override configurations', ]; } + + /** + * Returns class relation collector. + * + * @return RelationsCollector + */ + private function getRelationsCollector(): RelationsCollector + { + return ObjectManager::getInstance()->get(RelationsCollector::class); + } + + /** + * Returns config for test including config from parents. + * + * @param string $originalClassName + * @return array + */ + private function getInheritedClassConfig(string $originalClassName): array + { + if (empty($this->inheritedConfig[$originalClassName])) { + $classConfig = $this->config[$originalClassName] ?? []; + foreach ($this->getRelationsCollector()->getParents($originalClassName) as $parent) { + $parentConfig = $this->config[$parent] ?? []; + $classConfig = $this->mergeConfiguration($classConfig, $parentConfig); + } + $this->inheritedConfig[$originalClassName] = $classConfig; + } + + return $this->inheritedConfig[$originalClassName]; + } + + /** + * Merges test configurations. + * + * @param array $mainConfig + * @param array $parentConfig + * @return array + */ + private function mergeConfiguration(array $mainConfig, array $parentConfig): array + { + $merged = $mainConfig; + + foreach ($parentConfig as $key => &$value) { + if (is_array($value)) { + $merged[$key] = $merged[$key] ?? []; + if (in_array($key, $this::FIXTURE_TYPES, true)) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $merged[$key] = array_merge($merged[$key], $value); + } else { + $merged[$key] = $this->mergeConfiguration($merged[$key], $value); + } + } elseif ($key === 'skip') { + $merged['skip_from_config'] = $merged['skip_from_config'] ?? false; + $merged['skip'] = $merged['skip'] ?? false; + $merged['skipMessage'] = $merged['skipMessage'] ?? null; + + if (!$merged['skip_from_config'] && $parentConfig['skip_from_config']) { + $merged[$key] = $value; + $merged['skipMessage'] = $parentConfig['skipMessage']; + $merged['skip_from_config'] = $parentConfig['skip_from_config']; + } + } + } + + return $merged; + } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php index 571bcdd7007bf..3f4b4687da793 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php @@ -18,12 +18,18 @@ */ class Converter implements ConverterInterface { - private const FIXTURE_TYPES = [ - DataFixture::ANNOTATION, - DataFixtureBeforeTransaction::ANNOTATION, - ConfigFixture::ANNOTATION, - AdminConfigFixture::ANNOTATION, - ]; + /** + * @var array + */ + private $supportedFixtureTypes; + + /** + * @param array $types + */ + public function __construct(array $types = []) + { + $this->supportedFixtureTypes = $types; + } /** @var \DOMXPath */ private $xpath; @@ -34,7 +40,7 @@ class Converter implements ConverterInterface public function convert($source) { $this->xpath = new \DOMXPath($source); - $config = []; + $config = $this->getGlobalConfig($this->xpath); foreach ($this->xpath->query('//test') as $testOverride) { $className = ltrim($testOverride->getAttribute('class'), '\\'); $config[$className] = $this->getTestConfigByFixtureType($testOverride); @@ -46,7 +52,7 @@ public function convert($source) foreach ($this->xpath->query('./dataSet', $method) as $dataSet) { $setName = $dataSet->getAttribute('name'); - $config[$className][$methodName][$setName] = $config[$className][$methodName][$setName] ?? []; + $config[$className][$methodName][$setName] = $config[$className][$methodName][$setName] ?? []; $config[$className][$methodName][$setName] = $this->fillSkipSection( $dataSet, $config[$className][$methodName][$setName] @@ -67,6 +73,7 @@ public function convert($source) */ private function fillSkipSection(\DOMElement $node, array $config): array { + $config['skip_from_config'] = !empty($node->getAttribute('skip')); $config['skip'] = $node->getAttribute('skip') === 'true'; $config['skipMessage'] = $node->getAttribute('skipMessage') ?: null; @@ -81,7 +88,7 @@ private function fillSkipSection(\DOMElement $node, array $config): array */ private function getTestConfigByFixtureType(\DOMElement $node): array { - foreach (self::FIXTURE_TYPES as $fixtureType) { + foreach ($this->supportedFixtureTypes as $fixtureType) { $currentTestNodePath = sprintf("//test[@class ='%s']/%s", $node->getAttribute('class'), $fixtureType); foreach ($this->xpath->query($currentTestNodePath) as $classDataFixture) { $config[$fixtureType][] = $this->fillAttributes($classDataFixture); @@ -111,7 +118,7 @@ private function getTestConfigByFixtureType(\DOMElement $node): array * @param \DOMElement $fixture * @return array */ - private function fillAttributes(\DOMElement $fixture): array + protected function fillAttributes(\DOMElement $fixture): array { $result = []; switch ($fixture->nodeName) { @@ -138,7 +145,7 @@ private function fillAttributes(\DOMElement $fixture): array * @param \DOMElement $fixture * @return array */ - private function fillDataFixtureAttributes(\DOMElement $fixture): array + protected function fillDataFixtureAttributes(\DOMElement $fixture): array { return [ 'path' => $fixture->getAttribute('path'), @@ -155,7 +162,7 @@ private function fillDataFixtureAttributes(\DOMElement $fixture): array * @param \DOMElement $fixture * @return array */ - private function fillConfigFixtureAttributes(\DOMElement $fixture): array + protected function fillConfigFixtureAttributes(\DOMElement $fixture): array { return [ 'path' => $fixture->getAttribute('path'), @@ -173,7 +180,7 @@ private function fillConfigFixtureAttributes(\DOMElement $fixture): array * @param \DOMElement $fixture * @return array */ - private function fillAdminConfigFixtureAttributes(\DOMElement $fixture): array + protected function fillAdminConfigFixtureAttributes(\DOMElement $fixture): array { return [ 'path' => $fixture->getAttribute('path'), @@ -182,4 +189,36 @@ private function fillAdminConfigFixtureAttributes(\DOMElement $fixture): array 'remove' => $fixture->getAttribute('remove'), ]; } + /** + * Get global configurations + * + * @param \DOMXPath $xpath + * @return array + */ + private function getGlobalConfig(\DOMXPath $xpath): array + { + foreach ($xpath->query('//global') as $globalOverride) { + $config = $this->fillGlobalConfigByFixtureType($globalOverride); + } + + return $config ?? []; + } + + /** + * Fill global configurations node + * + * @param \DOMElement $node + * @return array + */ + private function fillGlobalConfigByFixtureType(\DOMElement $node): array + { + $config = []; + foreach ($this->supportedFixtureTypes as $fixtureType) { + foreach ($node->getElementsByTagName($fixtureType) as $fixture) { + $config['global'][$fixtureType][] = $this->fillAttributes($fixture); + } + } + + return $config; + } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/RelationsCollector.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/RelationsCollector.php new file mode 100644 index 0000000000000..2a17e7dba4904 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/RelationsCollector.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Workaround\Override\Config; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\Relations\Runtime; +use Magento\Framework\ObjectManager\RelationsInterface; +use PHPUnit\Framework\TestCase; + +/** + * Class collects test class parents and interfaces. + */ +class RelationsCollector +{ + /** + * @var RelationsInterface + */ + private $relations; + + /** + * @var array + */ + private $internalParents = []; + + /** + * Returns filtered list of parent classes and interfaces for given class name. + * + * @param string $className + * @return array + */ + public function getParents(string $className): array + { + return array_diff($this->getRelations($className), $this->getInternalParents()); + } + + /** + * Returns list of parent classes and interfaces for given class name. + * + * @param string $className + * @return array + */ + private function getRelations(string $className): array + { + $result = $this->getRelationsReader()->getParents($className); + + foreach ($result as $parent) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $result = array_merge($result, $this->getRelations($parent)); + } + + return $result; + } + + /** + * Returns class relations reader. + * + * @return RelationsInterface + */ + private function getRelationsReader(): RelationsInterface + { + if (empty($this->relations)) { + $this->relations = ObjectManager::getInstance()->create(Runtime::class); + } + + return $this->relations; + } + + /** + * Returns list of classes that should not be in list of parent classes. + * + * @return array + */ + private function getInternalParents(): array + { + if (empty($this->internalParents)) { + $this->internalParents = $this->getRelations(TestCase::class); + $this->internalParents[] = TestCase::class; + } + + return $this->internalParents; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/ConfigInterface.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/ConfigInterface.php new file mode 100644 index 0000000000000..dc5e885dcacc1 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/ConfigInterface.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Workaround\Override; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\Reader\Filesystem; +use Magento\Framework\View\File\Collector\Decorator\ModuleDependency; +use Magento\Framework\View\File\Collector\Decorator\ModuleOutput; +use Magento\TestFramework\Workaround\Override\Config\Converter; +use Magento\TestFramework\Workaround\Override\Config\FileCollector; +use Magento\TestFramework\Workaround\Override\Config\FileResolver; +use Magento\TestFramework\Workaround\Override\Config\Dom; +use Magento\TestFramework\Workaround\Override\Config\SchemaLocator; +use Magento\TestFramework\Workaround\Override\Config\ValidationState; +use PHPUnit\Framework\TestCase; + +/** + * Provides integration tests configuration. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +interface ConfigInterface +{ + /** + * Returns an array with skip key and skipMessage key if test is skipped. + * + * @param TestCase $test + * @return array + */ + public function getSkipConfiguration(TestCase $test): array; + + /** + * Test has configuration flag. + * + * @param string $className + * @return bool + */ + public function hasSkippedTest(string $className): bool; + + /** + * Get config from class node + * + * @param TestCase $test + * @param string|null $fixtureType + * @return array + */ + public function getClassConfig(TestCase $test, ?string $fixtureType = null): array; + + /** + * Get config from method node + * + * @param TestCase $test + * @param string|null $fixtureType + * @return array + */ + public function getMethodConfig(TestCase $test, ?string $fixtureType = null): array; + + /** + * Get config from dataSet node + * + * @param TestCase $test + * @param string|null $fixtureType + * @return array + */ + public function getDataSetConfig(TestCase $test, ?string $fixtureType = null): array; +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php index 556f4e22d6f45..0f0579c49a94c 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php @@ -12,6 +12,9 @@ */ abstract class Base implements ApplierInterface { + /** @var array */ + private $globalConfig; + /** @var array */ private $classConfig; @@ -21,6 +24,27 @@ abstract class Base implements ApplierInterface /** @var array */ private $dataSetConfig; + /** + * Get global node config + * + * @return array + */ + public function getGlobalConfig(): array + { + return $this->globalConfig; + } + + /** + * Set global node config + * + * @param array $globalConfig + * @return void + */ + public function setGlobalConfig(array $globalConfig): void + { + $this->globalConfig = $globalConfig; + } + /** * Get class node config * @@ -92,6 +116,7 @@ public function setDataSetConfig(array $dataSetConfig): void protected function getPrioritizedConfig(): array { return [ + $this->getGlobalConfig(), $this->getClassConfig(), $this->getMethodConfig(), $this->getDataSetConfig(), diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/ConfigFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/ConfigFixture.php index 7ea04fce47b1c..11c5692ecfbfe 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/ConfigFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/ConfigFixture.php @@ -7,6 +7,8 @@ namespace Magento\TestFramework\Workaround\Override\Fixture\Applier; +use Magento\Framework\App\Config\ScopeConfigInterface; + /** * Class represent config fixtures applying logic */ @@ -63,9 +65,21 @@ protected function initConfigFixture(array $attributes): string { $value = !empty($attributes['newValue']) ? $attributes['newValue'] : $attributes['value']; - return $attributes['scopeType'] === 'default' - ? sprintf('%s/%s %s', $attributes['scopeType'], $attributes['path'], $value) - : sprintf('%s_%s %s %s', $attributes['scopeCode'], $attributes['scopeType'], $attributes['path'], $value); + if (empty($attributes['scopeType'])) { + $result = sprintf('%s %s', $attributes['path'], $value); + } elseif ($attributes['scopeType'] === ScopeConfigInterface::SCOPE_TYPE_DEFAULT) { + $result = sprintf('%s/%s %s', $attributes['scopeType'], $attributes['path'], $value); + } else { + $result = sprintf( + '%s_%s %s %s', + $attributes['scopeCode'], + $attributes['scopeType'], + $attributes['path'], + $value + ); + } + + return $result; } /** diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/DataFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/DataFixture.php index b0bcbc35e5f1d..efd92b46a2bf7 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/DataFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/DataFixture.php @@ -7,6 +7,8 @@ namespace Magento\TestFramework\Workaround\Override\Fixture\Applier; +use Magento\Framework\Exception\LocalizedException; + /** * Class represent data fixtures applying logic */ @@ -104,7 +106,7 @@ private function sortFixtures(array $fixtures, array $attributes): array $beforeFixtures = []; $afterFixtures = []; if (!empty($attributes['before'])) { - $offset = array_search($attributes['before'], $fixtures); + $offset = $this->getFixturePosition($attributes['before'], $fixtures); if ($attributes['before'] === '-' || $offset === 0) { $beforeFixtures[] = $attributes['path']; } else { @@ -115,7 +117,7 @@ private function sortFixtures(array $fixtures, array $attributes): array if ($attributes['after'] === '-') { $afterFixtures[] = $attributes['path']; } else { - $offset = array_search($attributes['after'], $fixtures); + $offset = $this->getFixturePosition($attributes['after'], $fixtures); $fixtures = $this->insertFixture($fixtures, $attributes['path'], $offset + 1); } } elseif (empty($attributes['before'])) { @@ -125,6 +127,27 @@ private function sortFixtures(array $fixtures, array $attributes): array return array_merge($beforeFixtures, $fixtures, $afterFixtures); } + /** + * Get fixture position in added fixtures list + * + * @param string $fixtureToFind + * @param array $existingFixtures + * @return int + * @throws LocalizedException if fixture which have to be found does not exist in added fixtures list + */ + private function getFixturePosition(string $fixtureToFind, array $existingFixtures): int + { + $offset = 0; + if ($fixtureToFind !== '-') { + $offset = array_search($fixtureToFind, $existingFixtures); + if ($offset === false) { + throw new LocalizedException(__('The fixture %1 does not exist in fixtures list', $fixtureToFind)); + } + } + + return $offset; + } + /** * Insert fixture into position * diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php index 932870448f85b..33bf1011c5b7b 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php @@ -7,16 +7,16 @@ namespace Magento\TestFramework\Workaround\Override\Fixture; +use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\Component\ComponentRegistrarInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Annotation\AdminConfigFixture; use Magento\TestFramework\Annotation\ConfigFixture; use Magento\TestFramework\Annotation\DataFixture; use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\Workaround\Override\Config; +use Magento\TestFramework\Workaround\Override\ConfigInterface; use Magento\TestFramework\Workaround\Override\Fixture\Applier\AdminConfigFixture as AdminConfigFixtureApplier; use Magento\TestFramework\Workaround\Override\Fixture\Applier\ApplierInterface; use Magento\TestFramework\Workaround\Override\Fixture\Applier\Base; @@ -29,30 +29,30 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Resolver +class Resolver implements ResolverInterface { + /** @var ObjectManagerInterface */ + protected $objectManager; + /** @var self */ private static $instance; /** @var TestCase */ private $currentTest; - /** @var Config */ + /** @var ConfigInterface */ private $config; /** @var ApplierInterface[] */ private $appliersList; - /** @var ObjectManagerInterface */ - private $objectManager; - /** @var string */ private $currentFixtureType = null; /** - * @param Config $config + * @param ConfigInterface $config */ - public function __construct(Config $config) + public function __construct(ConfigInterface $config) { $this->config = $config; $this->objectManager = Bootstrap::getObjectManager(); @@ -61,32 +61,38 @@ public function __construct(Config $config) /** * Get class instance * - * @return self + * @return ResolverInterface */ - public static function getInstance(): self + public static function getInstance(): ResolverInterface { if (empty(self::$instance)) { - self::$instance = new self(Config::getInstance()); + throw new \RuntimeException('Override fixture resolver isn\'t initialized'); } return self::$instance; } /** - * Set current test to instance + * Instance setter. * - * @param TestCase $currentTest + * @param ResolverInterface $instance * @return void */ + public static function setInstance(ResolverInterface $instance): void + { + self::$instance = $instance; + } + + /** + * @inheritdoc + */ public function setCurrentTest(?TestCase $currentTest): void { $this->currentTest = $currentTest; } /** - * Get current test - * - * @return TestCase|null + * @inheritdoc */ public function getCurrentTest(): ?TestCase { @@ -94,10 +100,7 @@ public function getCurrentTest(): ?TestCase } /** - * Set which fixture type is executed - * - * @param null|string $fixtureType - * @return void + * @inheritdoc */ public function setCurrentFixtureType(?string $fixtureType): void { @@ -105,10 +108,7 @@ public function setCurrentFixtureType(?string $fixtureType): void } /** - * Require fixture wrapper - * - * @param string $path - * @return void + * @inheritdoc */ public function requireDataFixture(string $path): void { @@ -123,38 +123,62 @@ public function requireDataFixture(string $path): void } /** - * Apply override configurations to config fixtures list - * - * @param TestCase $test - * @param array $fixtures - * @param string $fixtureType - * @return array + * @inheritdoc */ public function applyConfigFixtures(TestCase $test, array $fixtures, string $fixtureType): array { - return $this->getApplier($test, $fixtureType)->apply($fixtures); + $skipConfig = $this->config->getSkipConfiguration($test); + + return $skipConfig['skip'] + ? [] + : $this->getApplier($test, $fixtureType)->apply($fixtures); } /** - * Apply override configurations to data fixtures list - * - * @param TestCase $test - * @param array $fixtures - * @param string $fixtureType - * @return array + * @inheritdoc */ public function applyDataFixtures(TestCase $test, array $fixtures, string $fixtureType): array { $result = []; - $fixtures = $this->getApplier($test, $fixtureType)->apply($fixtures); + $skipConfig = $this->config->getSkipConfiguration($test); + + if (!$skipConfig['skip']) { + $fixtures = $this->getApplier($test, $fixtureType)->apply($fixtures); - foreach ($fixtures as $fixture) { - $result[] = $this->processFixturePath($test, $fixture); + foreach ($fixtures as $fixture) { + $result[] = $this->processFixturePath($test, $fixture); + } } return $result; } + /** + * Get appropriate fixture applier according to fixture type + * + * @param string $fixtureType + * @return ApplierInterface + */ + protected function getApplierByFixtureType(string $fixtureType): ApplierInterface + { + switch ($fixtureType) { + case DataFixture::ANNOTATION: + case DataFixtureBeforeTransaction::ANNOTATION: + $applier = $this->objectManager->get(DataFixtureApplier::class); + break; + case ConfigFixture::ANNOTATION: + $applier = $this->objectManager->get(ConfigFixtureApplier::class); + break; + case AdminConfigFixture::ANNOTATION: + $applier = $this->objectManager->get(AdminConfigFixtureApplier::class); + break; + default: + throw new \InvalidArgumentException(sprintf('Unsupported fixture type %s provided', $fixtureType)); + } + + return $applier; + } + /** * Get ComponentRegistrar object * @@ -179,6 +203,7 @@ private function getApplier(TestCase $test, string $fixtureType): ApplierInterfa } /** @var Base $applier */ $applier = $this->appliersList[$fixtureType]; + $applier->setGlobalConfig($this->config->getGlobalConfig($fixtureType)); $applier->setClassConfig($this->config->getClassConfig($test, $fixtureType)); $applier->setMethodConfig($this->config->getMethodConfig($test, $fixtureType)); $applier->setDataSetConfig( @@ -190,32 +215,6 @@ private function getApplier(TestCase $test, string $fixtureType): ApplierInterfa return $applier; } - /** - * Get appropriate fixture applier according to fixture type - * - * @param string $fixtureType - * @return ApplierInterface - */ - private function getApplierByFixtureType(string $fixtureType): ApplierInterface - { - switch ($fixtureType) { - case DataFixture::ANNOTATION: - case DataFixtureBeforeTransaction::ANNOTATION: - $applier = $this->objectManager->get(DataFixtureApplier::class); - break; - case ConfigFixture::ANNOTATION: - $applier = $this->objectManager->get(ConfigFixtureApplier::class); - break; - case AdminConfigFixture::ANNOTATION: - $applier = $this->objectManager->get(AdminConfigFixtureApplier::class); - break; - default: - throw new \InvalidArgumentException(sprintf('Unsupported fixture type %s provided', $fixtureType)); - } - - return $applier; - } - /** * Converts fixture path. * diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/ResolverInterface.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/ResolverInterface.php new file mode 100644 index 0000000000000..3701ba033802f --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/ResolverInterface.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Workaround\Override\Fixture; + +use PHPUnit\Framework\TestCase; + +/** + * Class determines fixture applying according to configurations + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +interface ResolverInterface +{ + /** + * Set current test to instance + * + * @param TestCase $currentTest + * @return void + */ + public function setCurrentTest(?TestCase $currentTest): void; + + /** + * Get current test + * + * @return TestCase|null + */ + public function getCurrentTest(): ?TestCase; + + /** + * Set which fixture type is executed + * + * @param null|string $fixtureType + * @return void + */ + public function setCurrentFixtureType(?string $fixtureType): void; + + /** + * Require fixture wrapper + * + * @param string $path + * @return void + */ + public function requireDataFixture(string $path): void; + + /** + * Apply override configurations to config fixtures list + * + * @param TestCase $test + * @param array $fixtures + * @param string $fixtureType + * @return array + */ + public function applyConfigFixtures(TestCase $test, array $fixtures, string $fixtureType): array; + + /** + * Apply override configurations to data fixtures list + * + * @param TestCase $test + * @param array $fixtures + * @param string $fixtureType + * @return array + */ + public function applyDataFixtures(TestCase $test, array $fixtures, string $fixtureType): array; +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd b/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd index 3e18c4bb7daca..424381b5cb2b9 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd @@ -10,6 +10,7 @@ <xs:complexType> <xs:sequence minOccurs="0" maxOccurs="unbounded"> <xs:element name="test" type="test" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="global" type="global" minOccurs="0" maxOccurs="unbounded" /> </xs:sequence> </xs:complexType> </xs:element> @@ -77,4 +78,12 @@ <xs:attribute name="newValue" type="xs:string"/> <xs:attribute name="remove" type="xs:boolean"/> </xs:complexType> + <xs:complexType name="global"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="magentoDataFixture" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoDataFixtureBeforeTransaction" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoConfigFixture" type="configFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoAdminConfigFixture" type="adminConfigFixture" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> </xs:schema> diff --git a/dev/tests/integration/framework/bootstrap.php b/dev/tests/integration/framework/bootstrap.php index 59fb1535d1884..acf3056c8d923 100644 --- a/dev/tests/integration/framework/bootstrap.php +++ b/dev/tests/integration/framework/bootstrap.php @@ -103,9 +103,16 @@ $themePackageList ) ); - + $overrideConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + Magento\TestFramework\Workaround\Override\Config::class + ); + $overrideConfig->init(); + Magento\TestFramework\Workaround\Override\Config::setInstance($overrideConfig); + Magento\TestFramework\Workaround\Override\Fixture\Resolver::setInstance( + new \Magento\TestFramework\Workaround\Override\Fixture\Resolver($overrideConfig) + ); /* Unset declared global variables to release the PHPUnit from maintaining their values between tests */ - unset($testsBaseDir, $logWriter, $settings, $shell, $application, $bootstrap); + unset($testsBaseDir, $settings, $shell, $application, $bootstrap, $overrideConfig); } catch (\Exception $e) { // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $e . PHP_EOL; diff --git a/dev/tests/integration/framework/tests/unit/phpunit.xml.dist b/dev/tests/integration/framework/tests/unit/phpunit.xml.dist index 1a93397caaa4a..d15c5f1818784 100644 --- a/dev/tests/integration/framework/tests/unit/phpunit.xml.dist +++ b/dev/tests/integration/framework/tests/unit/phpunit.xml.dist @@ -6,7 +6,7 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" beStrictAboutTestsThatDoNotTestAnything="false" @@ -15,7 +15,7 @@ <!-- Test suites definition --> <testsuites> <testsuite name="Unit Tests for Integration Tests Framework"> - <directory suffix="Test.php">testsuite</directory> + <directory>testsuite</directory> </testsuite> </testsuites> <php> 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/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php index 524e6933dfe06..4a6461a32df9d 100644 --- a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php +++ b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php @@ -484,6 +484,7 @@ private function processApply(array $existingFixtures, array $config): array */ private function setConfig(array $config): void { + $this->object->setGlobalConfig([]); $this->object->setClassConfig([]); $this->object->setDataSetConfig([]); $this->object->setMethodConfig($config); diff --git a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php index 6dd5df493353a..921c78e7bd482 100644 --- a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php +++ b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php @@ -34,8 +34,11 @@ protected function setUp(): void public function testGetPrioritizedConfig(): void { $this->object = $this->getMockBuilder(DataFixture::class) - ->setMethods(['getClassConfig', 'getMethodConfig', 'getDataSetConfig']) + ->setMethods(['getGlobalConfig','getClassConfig', 'getMethodConfig', 'getDataSetConfig']) ->getMock(); + $this->object->expects($this->once()) + ->method('getGlobalConfig') + ->willReturn(['global_config']); $this->object->expects($this->once()) ->method('getClassConfig') ->willReturn(['class_config']); @@ -46,6 +49,7 @@ public function testGetPrioritizedConfig(): void ->method('getDataSetConfig') ->willReturn(['data_set_config']); $expectedResult = [ + ['global_config'], ['class_config'], ['method_config'], ['data_set_config'], @@ -271,6 +275,7 @@ private function processApply(array $existingFixtures, array $config): array */ private function setConfig(array $config): void { + $this->object->setGlobalConfig([]); $this->object->setClassConfig([]); $this->object->setDataSetConfig([]); $this->object->setMethodConfig($config); diff --git a/dev/tests/integration/testsuite/Magento/AdminNotification/_files/notifications.php b/dev/tests/integration/testsuite/Magento/AdminNotification/_files/notifications.php index 6615c24320b21..0a8f2670b5740 100644 --- a/dev/tests/integration/testsuite/Magento/AdminNotification/_files/notifications.php +++ b/dev/tests/integration/testsuite/Magento/AdminNotification/_files/notifications.php @@ -3,52 +3,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$om = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity( - \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL -)->setTitle( - 'Unread Critical 1' -)->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity(\Magento\Framework\Notification\MessageInterface::SEVERITY_MAJOR) - ->setTitle('Unread Major 1') - ->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity( - \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL -)->setTitle( - 'Unread Critical 2' -)->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity( - \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL -)->setTitle( - 'Unread Critical 3' -)->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity( - \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL -)->setTitle( - 'Read Critical 1' -)->setIsRead( - 1 -)->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity(\Magento\Framework\Notification\MessageInterface::SEVERITY_MAJOR) - ->setTitle('Unread Major 2') - ->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity( - \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL -)->setTitle( - 'Removed Critical 1' -)->setIsRemove( - 1 -)->save(); + +declare(strict_types=1); + +use Magento\AdminNotification\Model\Inbox; +use Magento\AdminNotification\Model\ResourceModel\Inbox as InboxResource; +use Magento\Framework\Notification\MessageInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var Inbox $message + * @var InboxResource $messageResource + */ +$message = $objectManager->create(Inbox::class); +$messageResource = $objectManager->create(InboxResource::class); + +$message->setSeverity(MessageInterface::SEVERITY_CRITICAL)->setTitle('Unread Critical 1'); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_MAJOR)->setTitle('Unread Major 1'); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_CRITICAL)->setTitle('Unread Critical 2'); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_CRITICAL)->setTitle('Unread Critical 3'); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_CRITICAL)->setTitle('Read Critical 1')->setIsRead(1); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_MAJOR)->setTitle('Unread Major 2'); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_CRITICAL)->setTitle('Removed Critical 1')->setIsRemove(1); +$messageResource->save($message); 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.php b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products.php index ef5877612a3b9..2ae807f0a401b 100644 --- a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products.php +++ b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products.php @@ -4,26 +4,50 @@ * See COPYING.txt for license details. */ -$productModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\Product::class); +declare(strict_types=1); -$productModel->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\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); + +$productModel->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId(4) ->setName('AdvancedPricingSimple 1') ->setSku('AdvancedPricingSimple 1') ->setPrice(321) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) ->setWebsiteIds([1]) ->setCategoryIds([]) ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) - ->setIsObjectNew(true) - ->save(); + ->setIsObjectNew(true); -$productModel->setName('AdvancedPricingSimple 2') - ->setId(null) - ->setUrlKey(null) +$productRepository->save($productModel); + +$productModel = $objectManager->create(Product::class); +$productModel->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('AdvancedPricingSimple 2') ->setSku('AdvancedPricingSimple 2') ->setPrice(654) - ->setIsObjectNew(true) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCategoryIds([]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) + ->setIsObjectNew(true); +$productRepository->save($productModel); 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.php b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/product_with_second_website.php index 47456de5ab07e..17b6a700e0c07 100644 --- a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/product_with_second_website.php +++ b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/product_with_second_website.php @@ -4,7 +4,12 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Model\Group; +use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; use Magento\Store\Api\WebsiteRepositoryInterface; @@ -13,15 +18,16 @@ Resolver::getInstance()->requireDataFixture('Magento/AdvancedPricingImportExport/_files/create_products.php'); $objectManager = Bootstrap::getObjectManager(); -/** @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager - ->get(Magento\Catalog\Api\ProductAttributeRepositoryInterface::class); -$groupPriceAttribute = $attributeRepository->get('tier_price') - ->setScope(Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_WEBSITE); + +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +$groupPriceAttribute = $attributeRepository->get('tier_price')->setScope(ScopedAttributeInterface::SCOPE_WEBSITE); $attributeRepository->save($groupPriceAttribute); + /** @var WebsiteRepositoryInterface $websiteRepository */ $websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); $website = $websiteRepository->get('test'); + /** @var ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(ProductRepositoryInterface::class); $productModel = $productRepository->get('AdvancedPricingSimple 2'); @@ -30,10 +36,10 @@ [ [ 'website_id' => $website->getId(), - 'cust_group' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'cust_group' => Group::CUST_GROUP_ALL, 'price_qty' => 3, 'price' => 5 ] ] ); -$productModel->save(); +$productRepository->save($productModel); 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/AsynchronousOperations/Model/BulkManagementTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/BulkManagementTest.php index 8c72977f6d8c8..3014fe37acb78 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/BulkManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/BulkManagementTest.php @@ -94,9 +94,6 @@ public function testRetryBulk() ->create() ->addFieldToFilter('bulk_uuid', ['eq' => $bulkUuid]) ->getItems(); - foreach ($operations as $operation) { - $operation->setId(null); - } $this->publisherMock->expects($this->once()) ->method('publish') ->with($topicName, array_values($operations)); diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php index 7ef6aa94768de..4976c8098103b 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php @@ -26,6 +26,8 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @magentoDbIsolation disabled */ class MassScheduleTest extends \PHPUnit\Framework\TestCase { @@ -64,6 +66,9 @@ class MassScheduleTest extends \PHPUnit\Framework\TestCase */ private $skus = []; + /** @var string */ + private $logFilePath; + /** * @var Registry */ diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/OperationManagementTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/OperationManagementTest.php index 7633a161253cd..4be795dd654cd 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/OperationManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/OperationManagementTest.php @@ -8,7 +8,11 @@ use Magento\AsynchronousOperations\Api\Data\OperationInterface; use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; +use Magento\AsynchronousOperations\Model\BulkStatus; +use Magento\AsynchronousOperations\Model\OperationManagement; use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\App\ResourceConnection; +use Magento\TestFramework\Helper\Bootstrap; class OperationManagementTest extends \PHPUnit\Framework\TestCase { @@ -32,21 +36,18 @@ class OperationManagementTest extends \PHPUnit\Framework\TestCase */ private $entityManager; + /** + * @var ResourceConnection + */ + private $connection; + protected function setUp(): void { - $this->model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\AsynchronousOperations\Model\OperationManagement::class - ); - $this->bulkStatusManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\AsynchronousOperations\Model\BulkStatus::class - ); - - $this->operationFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - OperationInterfaceFactory::class - ); - $this->entityManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - EntityManager::class - ); + $this->connection = Bootstrap::getObjectManager()->get(ResourceConnection::class); + $this->model = Bootstrap::getObjectManager()->get(OperationManagement::class); + $this->bulkStatusManagement = Bootstrap::getObjectManager()->get(BulkStatus::class); + $this->operationFactory = Bootstrap::getObjectManager()->get(OperationInterfaceFactory::class); + $this->entityManager = Bootstrap::getObjectManager()->get(EntityManager::class); } /** @@ -62,13 +63,22 @@ public function testGetBulkStatus() $operation = array_shift($operations); $operationId = $operation->getId(); - $this->assertTrue($this->model->changeOperationStatus($operationId, OperationInterface::STATUS_TYPE_OPEN)); + $this->assertTrue($this->model->changeOperationStatus( + 'bulk-uuid-5', + $operationId, + OperationInterface::STATUS_TYPE_OPEN + )); + + $table = $this->connection->getTableName('magento_operation'); + $connection = $this->connection->getConnection(); + $select = $connection->select() + ->from($table) + ->where("bulk_uuid = ?", 'bulk-uuid-5') + ->where("operation_key = ?", $operationId); + $updatedOperation = $connection->fetchRow($select); - /** @var OperationInterface $updatedOperation */ - $updatedOperation = $this->operationFactory->create(); - $this->entityManager->load($updatedOperation, $operationId); - $this->assertEquals(OperationInterface::STATUS_TYPE_OPEN, $updatedOperation->getStatus()); - $this->assertNull($updatedOperation->getResultMessage()); - $this->assertNull($updatedOperation->getSerializedData()); + $this->assertEquals(OperationInterface::STATUS_TYPE_OPEN, $updatedOperation['status']); + $this->assertNull($updatedOperation['result_message']); + $this->assertNull($updatedOperation['serialized_data']); } } diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php index 9e215667903d3..576927184ba8a 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php @@ -60,6 +60,7 @@ 'status' => OperationInterface::STATUS_TYPE_COMPLETE, 'error_code' => null, 'result_message' => null, + 'operation_key' => 0 ], [ 'bulk_uuid' => 'bulk-uuid-3', @@ -68,6 +69,7 @@ 'status' => OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, 'error_code' => 1111, 'result_message' => 'Something went wrong during your request', + 'operation_key' => 0 ], [ 'bulk_uuid' => 'bulk-uuid-4', @@ -76,6 +78,7 @@ 'status' => OperationInterface::STATUS_TYPE_COMPLETE, 'error_code' => null, 'result_message' => null, + 'operation_key' => 0 ], [ 'bulk_uuid' => 'bulk-uuid-5', @@ -84,6 +87,7 @@ 'status' => OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED, 'error_code' => 1111, 'result_message' => 'Something went wrong during your request', + 'operation_key' => 0 ], [ 'bulk_uuid' => 'bulk-uuid-5', @@ -92,6 +96,7 @@ 'status' => OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, 'error_code' => 2222, 'result_message' => 'Entity with ID=4 does not exist', + 'operation_key' => 1 ], ]; @@ -102,8 +107,8 @@ } $operationQuery = "INSERT INTO {$operationTable}" - . " (`bulk_uuid`, `topic_name`, `serialized_data`, `status`, `error_code`, `result_message`)" - . " VALUES (:bulk_uuid, :topic_name, :serialized_data, :status, :error_code, :result_message);"; + . " (`bulk_uuid`, `topic_name`, `serialized_data`, `status`, `error_code`, `result_message`, `operation_key`)" + . " VALUES (:bulk_uuid, :topic_name, :serialized_data, :status, :error_code, :result_message, :operation_key);"; foreach ($operations as $operation) { $connection->query($operationQuery, $operation); } diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php index a3547566c4245..1e27df71d5709 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php @@ -42,6 +42,7 @@ 'status' => OperationInterface::STATUS_TYPE_COMPLETE, 'error_code' => null, 'result_message' => null, + 'operation_key' => 0 ], [ 'bulk_uuid' => 'bulk-uuid-searchable-6', @@ -50,6 +51,7 @@ 'status' => OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED, 'error_code' => 1111, 'result_message' => 'Something went wrong during your request', + 'operation_key' => 1 ], [ 'bulk_uuid' => 'bulk-uuid-searchable-6', @@ -58,6 +60,7 @@ 'status' => OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, 'error_code' => 2222, 'result_message' => 'Entity with ID=4 does not exist', + 'operation_key' => 2 ], [ 'bulk_uuid' => 'bulk-uuid-searchable-6', @@ -66,6 +69,7 @@ 'status' => OperationInterface::STATUS_TYPE_OPEN, 'error_code' => null, 'result_message' => '', + 'operation_key' => 3 ], [ 'bulk_uuid' => 'bulk-uuid-searchable-6', @@ -74,6 +78,7 @@ 'status' => OperationInterface::STATUS_TYPE_OPEN, 'error_code' => null, 'result_message' => '', + 'operation_key' => 4 ], [ 'bulk_uuid' => 'bulk-uuid-searchable-6', @@ -82,6 +87,7 @@ 'status' => OperationInterface::STATUS_TYPE_REJECTED, 'error_code' => null, 'result_message' => '', + 'operation_key' => 5 ], ]; @@ -92,8 +98,8 @@ } $operationQuery = "INSERT INTO {$operationTable}" - . " (`bulk_uuid`, `topic_name`, `serialized_data`, `status`, `error_code`, `result_message`)" - . " VALUES (:bulk_uuid, :topic_name, :serialized_data, :status, :error_code, :result_message);"; + . " (`bulk_uuid`, `topic_name`, `serialized_data`, `status`, `error_code`, `result_message`, `operation_key`)" + . " VALUES (:bulk_uuid, :topic_name, :serialized_data, :status, :error_code, :result_message, :operation_key);"; foreach ($operations as $operation) { $connection->query($operationQuery, $operation); } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Button/SplitButtonTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Button/SplitButtonTest.php new file mode 100644 index 0000000000000..473caf1d6737e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Button/SplitButtonTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Backend\Block\Widget\Button; + +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Testing SplitButton widget + * + * @magentoAppArea adminhtml + */ +class SplitButtonTest extends TestCase +{ + + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->layout = $objectManager->get(LayoutInterface::class); + } + + /** + * Create the block. + * + * @return SplitButton + */ + private function createBlock(): SplitButton + { + /** @var SplitButton $block */ + $block = $this->layout->createBlock(SplitButton::class, 'button_block'); + $block->setLayout($this->layout); + + return $block; + } + + /** + * Test resulting button HTML. + * + * @return void + */ + public function testToHtml(): void + { + $block = $this->createBlock(); + $block->addData( + [ + 'title' => 'A button', + 'label' => 'A button', + 'has_split' => true, + 'button_class' => 'aclass', + 'id' => 'split-button', + 'disabled' => false, + 'class' => 'aclass', + 'data_attribute' => ['bind' => ['var' => 'val']], + 'options' => [ + [ + 'disabled' => false, + 'title' => 'An option', + 'label' => 'An option', + 'onclick' => $onclick = 'console.log("option")', + 'style' => 'width: 100px' + ] + ] + ] + ); + + $html = $block->toHtml(); + $this->assertStringContainsString('<button ', $html); + $this->assertStringContainsString('<span>A button</span>', $html); + $this->assertStringNotContainsString('onclick=', $html); + $this->assertStringNotContainsString('style=', $html); + $this->assertMatchesRegularExpression('/\<script.*?\>.*?' . preg_quote($onclick) . '.*?\<\/script\>/ims', $html); + $this->assertStringContainsString('width', $html); + $this->assertStringContainsString('100px', $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/ButtonTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/ButtonTest.php new file mode 100644 index 0000000000000..e48a6bae2ff80 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/ButtonTest.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Backend\Block\Widget; + +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Framework\View\LayoutInterface; + +/** + * Test for the button widget. + * + * @magentoAppArea adminhtml + */ +class ButtonTest extends TestCase +{ + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->layout = $objectManager->get(LayoutInterface::class); + } + + /** + * Create the block. + * + * @return Button + */ + private function createBlock(): Button + { + /** @var Button $block */ + $block = $this->layout->createBlock(Button::class, 'button_block'); + $block->setLayout($this->layout); + + return $block; + } + + /** + * Test resulting button HTML. + * + * @return void + */ + public function testToHtml(): void + { + $block = $this->createBlock(); + $block->addData( + [ + 'type' => 'button', + 'onclick' => 'console.log("Button pressed!")', + 'disabled' => false, + 'title' => 'A button', + 'label' => 'A button', + 'class' => 'button', + 'id' => 'button', + 'element_name' => 'some-name', + 'value' => 'Press a button', + 'data-style' => 'width: 100px', + 'style' => 'height: 200px' + ] + ); + + $html = $block->toHtml(); + $this->assertStringContainsString('<button ', $html); + $this->assertStringContainsString('<span>A button</span>', $html); + $this->assertStringNotContainsString('onclick=', $html); + $this->assertStringNotContainsString('style=', $html); + $this->assertMatchesRegularExpression('/\<script.*?\>.*?' .preg_quote($block->getOnClick()) .'.*?\<\/script\>/ims', $html); + $this->assertStringContainsString('height', $html); + $this->assertStringContainsString('200px', $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/WidgetTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/WidgetTest.php index cdcecabe00f8c..ef984c8289d99 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/WidgetTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/WidgetTest.php @@ -27,7 +27,8 @@ public function testGetButtonHtml() $widget = $layout->createBlock(\Magento\Backend\Block\Widget::class); $this->assertMatchesRegularExpression( - '/<button.*onclick\=\"this.form.submit\(\)\".*\>[\s\S]*Button Label[\s\S]*<\/button>/iu', + '/\<button.*\>[\s\S]*Button Label[\s\S]*<\/button>' + . '.*?\<script.*?\>.*?this\.form\.submit\(\).*?\<\/script\>/is', $widget->getButtonHtml('Button Label', 'this.form.submit()') ); } @@ -49,12 +50,14 @@ public function testGetButtonHtmlForTwoButtonsInOneBlock() $widget = $layout->createBlock(\Magento\Backend\Block\Widget::class); $this->assertMatchesRegularExpression( - '/<button.*onclick\=\"this.form.submit\(\)\".*\>[\s\S]*Button Label[\s\S]*<\/button>/iu', + '/<button.*\>[\s\S]*Button Label[\s\S]*<\/button>' + . '.*?\<script.*?\>.*?this\.form\.submit\(\).*?\<\/script\>/ius', $widget->getButtonHtml('Button Label', 'this.form.submit()') ); $this->assertMatchesRegularExpression( - '/<button.*onclick\=\"this.form.submit\(\)\".*\>[\s\S]*Button Label2[\s\S]*<\/button>/iu', + '/<button.*\>[\s\S]*Button Label2[\s\S]*<\/button>' + . '.*?\<script.*?\>.*?this\.form\.submit\(\).*?\<\/script\>/ius', $widget->getButtonHtml('Button Label2', 'this.form.submit()') ); } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search/GridTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search/GridTest.php index 369cbcf8ead33..980c5fe8a6e0a 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search/GridTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search/GridTest.php @@ -27,7 +27,7 @@ public function testToHtmlHasOnClick() $html = $block->toHtml(); - $regexpTemplate = '/<button [^>]* onclick="temp_id[^"]*\\.%s/i'; + $regexpTemplate = '/\<script.*?\>.*?temp_id[^"]*\\.%s/is'; $jsFuncs = ['doFilter', 'resetFilter']; foreach ($jsFuncs as $func) { $regexp = sprintf($regexpTemplate, $func); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/MassDeleteTest.php new file mode 100644 index 0000000000000..9343e6201f7ff --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/MassDeleteTest.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; + +use Magento\Catalog\Controller\Adminhtml\Product\MassDeleteTest as CatalogMassDeleteTest; + +/** + * Test for mass bundle product deleting. + * + * @see \Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit\MassDelete + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class MassDeleteTest extends CatalogMassDeleteTest +{ + /** + * @magentoDataFixture Magento/Bundle/_files/bundle_product_checkbox_required_option.php + * + * @return void + */ + public function testDeleteBundleProductViaMassAction(): void + { + $product = $this->productRepository->get('bundle-product-checkbox-required-option'); + $this->dispatchMassDeleteAction([$product->getId()]); + $this->assertSuccessfulDeleteProducts(1); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php index 572a526da07da..bf369ed28167b 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php @@ -3,13 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Bundle\Model\Product; /** * Abstract class for testing bundle prices - * + * @codingStandardsIgnoreStart * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class BundlePriceAbstract extends \PHPUnit\Framework\TestCase @@ -31,14 +30,6 @@ abstract class BundlePriceAbstract extends \PHPUnit\Framework\TestCase */ protected $productCollectionFactory; - /** - * @var \Magento\CatalogRule\Model\RuleFactory - */ - private $ruleFactory; - - /** - * @inheritdoc - */ protected function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -52,19 +43,15 @@ protected function setUp(): void true, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); - $this->ruleFactory = $this->objectManager->get(\Magento\CatalogRule\Model\RuleFactory::class); } /** - * Get test cases. - * + * Get test cases * @return array */ abstract public function getTestCases(); /** - * Prepare fixture. - * * @param array $strategyModifiers * @param string $productSku * @return void @@ -75,14 +62,11 @@ abstract public function getTestCases(); */ protected function prepareFixture($strategyModifiers, $productSku) { - $this->ruleFactory->create()->clearPriceRulesData(); - $bundleProduct = $this->productRepository->get($productSku); foreach ($strategyModifiers as $modifier) { if (method_exists($this, $modifier['modifierName'])) { array_unshift($modifier['data'], $bundleProduct); - // phpcs:ignore Magento2.Functions.DiscouragedFunction $bundleProduct = call_user_func_array([$this, $modifier['modifierName']], $modifier['data']); } else { throw new \Magento\Framework\Exception\InputException( @@ -130,8 +114,6 @@ protected function addSimpleProduct(\Magento\Catalog\Model\Product $bundleProduc } /** - * Add custom option. - * * @param \Magento\Catalog\Model\Product $bundleProduct * @param array $optionsData * @return \Magento\Catalog\Model\Product @@ -161,3 +143,4 @@ protected function addCustomOption(\Magento\Catalog\Model\Product $bundleProduct return $bundleProduct; } } +// @codingStandardsIgnoreEnd diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options.php new file mode 100644 index 0000000000000..245656f536463 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$productIds = range(10, 12, 1); +foreach ($productIds as $productId) { + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Bundle Product With Two dropdown options') + ->setSku('bundle-product-two-dropdown-options') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setPriceView(1) + ->setPriceType(1) + ->setPrice(10.0) + ->setShipmentType(0) + ->setBundleOptionsData( + [ + // "Drop-down" option + [ + 'title' => 'Drop Down Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 0, + 'position' => 1, + 'delete' => '', + ], + [ + 'title' => 'Drop Down Option 2', + 'default_title' => 'Option 2', + 'type' => 'select', + 'required' => 0, + 'position' => 2, + 'delete' => '', + ] + ] + )->setBundleSelectionsData( + [ + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_price_value' => 1.00, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_price_value' => 2.00, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_price_value' => 1.00, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 2 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_price_value' => 2.00, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 2 + ] + ], + ] + ); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +if ($product->getBundleOptionsData()) { + $options = []; + foreach ($product->getBundleOptionsData() as $key => $optionData) { + if (!(bool)$optionData['delete']) { + $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) + ->create(['data' => $optionData]); + $option->setSku($product->getSku()); + $option->setOptionId(null); + + $links = []; + $bundleLinks = $product->getBundleSelectionsData(); + if (!empty($bundleLinks[$key])) { + foreach ($bundleLinks[$key] as $linkData) { + if (!(bool)$linkData['delete']) { + $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) + ->create(['data' => $linkData]); + $linkProduct = $productRepository->getById($linkData['product_id']); + $link->setSku($linkProduct->getSku()); + $link->setQty($linkData['selection_qty']); + $link->setPrice($linkData['selection_price_value']); + if (isset($linkData['selection_can_change_qty'])) { + $link->setCanChangeQuantity($linkData['selection_can_change_qty']); + } + $links[] = $link; + } + } + $option->setProductLinks($links); + $options[] = $option; + } + } + } + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); +} +$productRepository->save($product, true); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options_rollback.php new file mode 100644 index 0000000000000..7088621f14c74 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options_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\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products_rollback.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('bundle-product-two-dropdown-options', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price.php new file mode 100644 index 0000000000000..7e0ba34b8ea9f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Bundle\Model\Product\Price; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Type\AbstractType; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Bundle\Model\PrepareBundleLinks; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var PrepareBundleLinks $prepareBundleLinks */ +$prepareBundleLinks = $objectManager->get(PrepareBundleLinks::class); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$defaultWebsiteId = $storeManager->getWebsite('base')->getId(); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Magento\Catalog\Api\Data\ProductInterface $bundleProduct */ +$bundleProduct = $productFactory->create(); +$bundleProduct->setTypeId(Type::TYPE_BUNDLE) + ->setAttributeSetId($bundleProduct->getDefaultAttributeSetId()) + ->setWebsiteIds([$defaultWebsiteId]) + ->setName('Bundle Product With Dynamic Price') + ->setSku('bundle_product_with_dynamic_price') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 0, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + ) + ->setSkuType(0) + ->setPriceView(0) + ->setPriceType(Price::PRICE_TYPE_DYNAMIC) + ->setPrice(null) + ->setWeightType(0) + ->setShipmentType(AbstractType::SHIPMENT_TOGETHER); + +$bundleOptionsData = [ + [ + 'title' => 'Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 1, + ], + [ + 'title' => 'Option 2', + 'default_title' => 'Option 2', + 'type' => 'select', + 'required' => 1, + ], +]; +$bundleSelectionsData = [ + [ + [ + 'sku' => 'simple1', + 'selection_qty' => 1, + ], + ], + [ + [ + 'sku' => 'simple2', + 'selection_qty' => 1, + ], + ] +]; +$bundleProduct = $prepareBundleLinks->execute($bundleProduct, $bundleOptionsData, $bundleSelectionsData); +$productRepository->save($bundleProduct); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price_rollback.php new file mode 100644 index 0000000000000..85b7d8377ab9e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price_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\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +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); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $product = $productRepository->get('bundle_product_with_dynamic_price', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + //product already deleted. +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php index fa957a0bfd3f8..1da7f821bb36e 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php @@ -28,7 +28,7 @@ ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('Simple Product') ->setSku('simple1') - ->setTaxClassId(0) + ->setTaxClassId(2) ->setDescription('description') ->setShortDescription('short description') ->setOptionsContainer('container1') @@ -57,7 +57,7 @@ ->setAttributeSetId($product2->getDefaultAttributeSetId()) ->setName('Simple Product2') ->setSku('simple2') - ->setTaxClassId(0) + ->setTaxClassId(2) ->setDescription('description') ->setShortDescription('short description') ->setOptionsContainer('container1') diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/order_with_bundle_shipped_separately.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/order_with_bundle_shipped_separately.php index b91d479cdf1ef..e5f089ae9637c 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/order_with_bundle_shipped_separately.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/order_with_bundle_shipped_separately.php @@ -155,6 +155,8 @@ /** @var \Magento\Sales\Model\Order\Item $orderItem */ $orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); $orderItem->setProductId($product->getId()); +$orderItem->setSku($product->getSku()); +$orderItem->setName($product->getName()); $orderItem->setQtyOrdered(1); $orderItem->setBasePrice($product->getPrice()); $orderItem->setPrice($product->getPrice()); @@ -172,6 +174,8 @@ /** @var \Magento\Sales\Model\Order\Item $orderItem */ $orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); $orderItem->setProductId($productId); + $orderItem->setSku($selectedProduct->getSku()); + $orderItem->setName($selectedProduct->getName()); $orderItem->setQtyOrdered(1); $orderItem->setBasePrice($selectedProduct->getPrice()); $orderItem->setPrice($selectedProduct->getPrice()); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity.php new file mode 100644 index 0000000000000..a623c583fb599 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$productIds = range(10, 12, 1); +foreach ($productIds as $productId) { + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) + ->setId(3) + ->setAttributeSetId(4) + ->setName('Bundle Product') + ->setSku('bundle-product') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setWebsiteIds([1]) + ->setPriceType(1) + ->setPrice(10.0) + ->setShipmentType(0) + ->setPriceView(1) + ->setBundleOptionsData( + [ + // Required "Drop-down" option + [ + 'title' => 'Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 1, + 'position' => 1, + 'delete' => '', + ], + // Required "Radio Buttons" option + [ + 'title' => 'Option 2', + 'default_title' => 'Option 2', + 'type' => 'radio', + 'required' => 1, + 'position' => 2, + 'delete' => '', + ], + ] + )->setBundleSelectionsData( + [ + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_can_change_qty' => 0, + 'delete' => '', + 'option_id' => 2 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_can_change_qty' => 0, + 'delete' => '', + 'option_id' => 2 + ] + ] + ] + ); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +if ($product->getBundleOptionsData()) { + $options = []; + foreach ($product->getBundleOptionsData() as $key => $optionData) { + if (!(bool)$optionData['delete']) { + $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) + ->create(['data' => $optionData]); + $option->setSku($product->getSku()); + $option->setOptionId(null); + + $links = []; + $bundleLinks = $product->getBundleSelectionsData(); + if (!empty($bundleLinks[$key])) { + foreach ($bundleLinks[$key] as $linkData) { + if (!(bool)$linkData['delete']) { + $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) + ->create(['data' => $linkData]); + $linkProduct = $productRepository->getById($linkData['product_id']); + $link->setSku($linkProduct->getSku()); + $link->setQty($linkData['selection_qty']); + if (isset($linkData['selection_can_change_qty'])) { + $link->setCanChangeQuantity($linkData['selection_can_change_qty']); + } + $links[] = $link; + } + } + $option->setProductLinks($links); + $options[] = $option; + } + } + } + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); +} +$product->save(); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity_rollback.php new file mode 100644 index 0000000000000..9d702b4506551 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\TestFramework\Helper\Bootstrap; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products_rollback.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +/** @var \Magento\Framework\Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $product = $productRepository->get('bundle-product'); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select.php new file mode 100644 index 0000000000000..74182d830dc6d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\Product; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php'); + +$objectManager = Bootstrap::getObjectManager(); + +$productIds = range(10, 12, 1); +foreach ($productIds as $productId) { + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); +} + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) + ->setId(3) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Bundle Product') + ->setSku('bundle-product') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setPriceType(1) + ->setPrice(10.0) + ->setShipmentType(0) + ->setBundleOptionsData( + [ + // Required "Drop-down" option + [ + 'title' => 'Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 1, + 'position' => 1, + 'delete' => '', + ], + // Required "Radio Buttons" option + [ + 'title' => 'Option 2', + 'default_title' => 'Option 2', + 'type' => 'radio', + 'required' => 1, + 'position' => 2, + 'delete' => '', + ] + ] + )->setBundleSelectionsData( + [ + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 2 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 2 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 3 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 3 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 4 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 4 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 5 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 5 + ] + ] + ] + ); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +if ($product->getBundleOptionsData()) { + $options = []; + foreach ($product->getBundleOptionsData() as $key => $optionData) { + if (!(bool)$optionData['delete']) { + $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) + ->create(['data' => $optionData]); + $option->setSku($product->getSku()); + $option->setOptionId(null); + + $links = []; + $bundleLinks = $product->getBundleSelectionsData(); + if (!empty($bundleLinks[$key])) { + foreach ($bundleLinks[$key] as $linkData) { + if (!(bool)$linkData['delete']) { + $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) + ->create(['data' => $linkData]); + $linkProduct = $productRepository->getById($linkData['product_id']); + $link->setSku($linkProduct->getSku()); + $link->setQty($linkData['selection_qty']); + $links[] = $link; + } + } + $option->setProductLinks($links); + $options[] = $option; + } + } + } + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); +} +$product->save(); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select_rollback.php new file mode 100644 index 0000000000000..57b4eb2e6cc91 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products_rollback.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $product = $productRepository->get('bundle-product'); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/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/Captcha/Observer/ResetAttemptForFrontendObserverTest.php b/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendObserverTest.php index c0acf3344f60f..33c42d794bd78 100644 --- a/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendObserverTest.php @@ -34,7 +34,7 @@ protected function setUp(): void /** * @magentoDataFixture Magento/Captcha/_files/failed_logins_frontend.php */ - public function testSuccesfulLoginRemovesFailedAttempts() + public function testSuccessfulLoginRemovesFailedAttempts() { $customerEmail = 'mageuser@dummy.com'; $customerFactory = $this->objectManager->get(CustomerFactory::class); 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/AbstractTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/AbstractTest.php index a80b229bbbd15..fd1ff22dc6525 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/AbstractTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/AbstractTest.php @@ -3,15 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Block\Product; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Framework\Pricing\Render; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; + /** * Test class for \Magento\Catalog\Block\Product\Abstract. * * @magentoDataFixture Magento/Catalog/_files/product_with_image.php * @magentoAppArea frontend */ -class AbstractTest extends \PHPUnit\Framework\TestCase +class AbstractTest extends TestCase { /** * Stub class name for class under test @@ -19,17 +32,17 @@ class AbstractTest extends \PHPUnit\Framework\TestCase const STUB_CLASS = 'Magento_Catalog_Block_Product_AbstractProduct_Stub'; /** - * @var \Magento\Catalog\Block\Product\AbstractProduct + * @var AbstractProduct */ protected $block; /** - * @var \Magento\Catalog\Model\Product + * @var ProductInterface */ protected $product; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var ProductRepositoryInterface */ protected $productRepository; @@ -40,43 +53,51 @@ class AbstractTest extends \PHPUnit\Framework\TestCase */ protected static $isStubClass = false; + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @var SerializerInterface + */ + private $json; + + /** + * @inheritdoc + */ + protected function setUp(): void { if (!self::$isStubClass) { $this->getMockForAbstractClass( - \Magento\Catalog\Block\Product\AbstractProduct::class, + AbstractProduct::class, [], self::STUB_CLASS, false ); self::$isStubClass = true; } - - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $objectManager->get(\Magento\Framework\App\State::class)->setAreaCode('frontend'); - $objectManager->get(\Magento\Framework\View\DesignInterface::class)->setDefaultDesignTheme(); - $this->block = $objectManager->get( - \Magento\Framework\View\LayoutInterface::class - )->createBlock(self::STUB_CLASS); - $this->productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); - - $this->product = $this->productRepository->get('simple'); - $this->product->addData( - [ - 'image' => '/m/a/magento_image.jpg', - 'small_image' => '/m/a/magento_image.jpg', - 'thumbnail' => '/m/a/magento_image.jpg', - ] - ); - $this->block->setProduct($this->product); + $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->get(DesignInterface::class)->setDefaultDesignTheme(); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(self::STUB_CLASS); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->json = $this->objectManager->get(SerializerInterface::class); } /** * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_simple.php * @magentoAppIsolation enabled + * @return void */ - public function testGetAddToCartUrl() + public function testGetAddToCartUrlWithProductRequiredOptions(): void { $product = $this->productRepository->get('simple'); $url = $this->block->getAddToCartUrl($product); @@ -84,18 +105,38 @@ public function testGetAddToCartUrl() $this->assertStringMatchesFormat('%ssimple-product.html%s', $url); } - public function testGetSubmitUrl() + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testGetAddToCartUrlWithSimpleProduct(): void + { + $product = $this->productRepository->get('simple-1'); + $url = $this->block->getAddToCartUrl($product); + $this->assertStringEndsWith(sprintf('product/%s/', $product->getId()), $url); + $this->assertStringContainsString('checkout/cart/add', $url); + } + + /** + * @return void + */ + public function testGetSubmitUrl(): void { + $this->product = $this->productRepository->get('simple'); /* by default same as add to cart */ $this->assertStringEndsWith('?options=cart', $this->block->getSubmitUrl($this->product)); $this->block->setData('submit_route_data', ['route' => 'catalog/product/view']); $this->assertStringEndsWith('catalog/product/view/', $this->block->getSubmitUrl($this->product)); } - public function testGetAddToWishlistParams() + /** + * @return void + */ + public function testGetAddToWishlistParams(): void { + $this->product = $this->productRepository->get('simple'); $json = $this->block->getAddToWishlistParams($this->product); - $params = (array)json_decode($json); + $params = (array)$this->json->unserialize($json); $data = (array)$params['data']; $this->assertEquals($this->product->getId(), $data['product']); $this->assertArrayHasKey('uenc', $data); @@ -105,53 +146,70 @@ public function testGetAddToWishlistParams() ); } - public function testGetAddToCompareUrl() + /** + * @return void + */ + public function testGetAddToCompareUrl(): void { $this->assertStringMatchesFormat('%scatalog/product_compare/add/', $this->block->getAddToCompareUrl()); } - public function testGetMinimalQty() + /** + * @return void + */ + public function testGetMinimalQty(): void { + $this->product = $this->productRepository->get('simple'); $this->assertGreaterThan(0, $this->block->getMinimalQty($this->product)); } - public function testGetReviewsSummaryHtml() + /** + * @return void + */ + public function testGetReviewsSummaryHtml(): void { - $this->block->setLayout( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Magento\Framework\View\LayoutInterface::class) - ); + $this->product = $this->productRepository->get('simple'); $html = $this->block->getReviewsSummaryHtml($this->product, false, true); $this->assertNotEmpty($html); $this->assertStringContainsString('review', $html); } - public function testGetProduct() + /** + * @return void + */ + public function testGetProduct(): void { + $this->product = $this->productRepository->get('simple'); + $this->block->setProduct($this->product); $this->assertSame($this->product, $this->block->getProduct()); } /** * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_simple.php * @magentoAppIsolation enabled + * @return void */ - public function testGetProductUrl() + public function testGetProductUrl(): void { $product = $this->productRepository->get('simple'); $this->assertStringEndsWith('simple-product.html', $this->block->getProductUrl($product)); } - public function testHasProductUrl() + /** + * @return void + */ + public function testHasProductUrl(): void { + $this->product = $this->productRepository->get('simple'); $this->assertTrue($this->block->hasProductUrl($this->product)); } - public function testLayoutDependColumnCount() + /** + * @return void + */ + public function testLayoutDependColumnCount(): void { - $this->block->setLayout( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Magento\Framework\View\LayoutInterface::class) - ); + $this->block->setLayout($this->layout); $this->assertEquals(3, $this->block->getColumnCount()); /* default column count */ @@ -161,8 +219,35 @@ public function testLayoutDependColumnCount() $this->assertFalse($this->block->getColumnCountLayoutDepend('test')); } - public function testGetCanShowProductPrice() + /** + * @return void + */ + public function testGetCanShowProductPrice(): void { + $this->product = $this->productRepository->get('simple'); $this->assertTrue($this->block->getCanShowProductPrice($this->product)); } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testGetProductPriceHtml(): void + { + $product = $this->productRepository->get('simple-1'); + $this->assertEmpty($this->block->getProductPriceHtml($product, FinalPrice::PRICE_CODE)); + $this->layout->createBlock( + Render::class, + 'product.price.render.default', + [ + 'data' => [ + 'price_render_handle' => 'catalog_product_prices', + 'use_link_for_as_low_as' => true, + ], + ] + ); + $finalPriceHtml = $this->block->getProductPriceHtml($product, FinalPrice::PRICE_CODE); + $this->assertStringContainsString('price-' . FinalPrice::PRICE_CODE, $finalPriceHtml); + $this->assertStringContainsString('product-price-' . $product->getId(), $finalPriceHtml); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php index 699df30c7bf3d..5badbef361b62 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php @@ -66,6 +66,12 @@ abstract class AbstractLinksTest extends TestCase /** @var string */ protected $linkType; + /** @var string */ + protected $titleName; + + /** @var string */ + protected $titleXpath = "//strong[@id = 'block-%s-heading'][contains(text(), '%s')]"; + /** * @inheritdoc */ @@ -297,7 +303,7 @@ protected function linkProducts(string $sku, array $productLinks): void * * @return array */ - protected function prepareWebsiteIdsProducts(): array + protected function prepareProductsWebsiteIds(): array { $websiteId = $this->storeManager->getWebsite('test')->getId(); $defaultWebsiteId = $this->storeManager->getWebsite('base')->getId(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php index 2c61743ae6aa5..11ce7b1328df8 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php @@ -132,7 +132,7 @@ public function testPositionRelatedProducts(): void */ public function testMultipleWebsitesRelatedProducts(array $data): void { - $this->updateProducts($this->prepareWebsiteIdsProducts()); + $this->updateProducts($this->prepareProductsWebsiteIds()); $productLinks = array_replace_recursive($this->existingProducts, $data['productLinks']); $this->linkProducts('simple-1', $productLinks); $this->product = $this->productRepository->get( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php index fd9d4e7e68fff..4d24a5aabafdb 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php @@ -120,7 +120,7 @@ public function testPositionUpsellProducts(): void */ public function testMultipleWebsitesUpsellProducts(array $data): void { - $this->updateProducts($this->prepareWebsiteIdsProducts()); + $this->updateProducts($this->prepareProductsWebsiteIds()); $productLinks = array_replace_recursive($this->existingProducts, $data['productLinks']); $this->linkProducts('simple-1', $productLinks); $this->product = $this->productRepository->get( 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/Block/Product/View/OptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/OptionsTest.php index 28357919ed566..57782fc17c9f5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/OptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/OptionsTest.php @@ -3,12 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); - namespace Magento\Catalog\Block\Product\View; -use Magento\CatalogRule\Model\Indexer\IndexBuilder; - /** * Test class for \Magento\Catalog\Block\Product\View\Options. */ @@ -34,19 +30,12 @@ class OptionsTest extends \PHPUnit\Framework\TestCase */ protected $productRepository; - /** - * @var IndexBuilder - */ - private $indexBuilder; - protected function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $this->indexBuilder = $this->objectManager->create(IndexBuilder::class); - try { $this->product = $this->productRepository->get('simple'); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { @@ -124,7 +113,9 @@ private function getExpectedJsonConfig() { return [ 0 => [ - 'prices' => ['oldPrice' => ['amount' => 10, 'adjustments' => []], + 'prices' => + ['oldPrice' => + ['amount' => 10, 'adjustments' => []], 'basePrice' => ['amount' => 10], 'finalPrice' => ['amount' => 10] ], @@ -132,7 +123,9 @@ private function getExpectedJsonConfig() 'name' => 'drop_down option 1', ], 1 => [ - 'prices' => ['oldPrice' => ['amount' => 40, 'adjustments' => []], + 'prices' => + ['oldPrice' => + ['amount' => 40, 'adjustments' => []], 'basePrice' => ['amount' => 40], 'finalPrice' => ['amount' => 40], ], @@ -141,47 +134,4 @@ private function getExpectedJsonConfig() ], ]; } - - /** - * Test option prices with catalog price rules applied. - * - * @magentoDbIsolation disabled - * @magentoDataFixture Magento/CatalogRule/_files/two_rules.php - * @magentoDataFixture Magento/Catalog/_files/product_with_dropdown_option.php - */ - public function testGetJsonConfigWithCatalogRules() - { - $this->indexBuilder->reindexFull(); - sleep(1); - $config = json_decode($this->block->getJsonConfig(), true); - $configValues = array_values($config); - $this->assertEquals($this->getExpectedJsonConfigWithCatalogRules(), array_values($configValues[0])); - } - - /** - * Expected data for testGetJsonConfigWithCatalogRules - * - * @return array - */ - private function getExpectedJsonConfigWithCatalogRules() - { - return [ - 0 => [ - 'prices' => ['oldPrice' => ['amount' => 10, 'adjustments' => []], - 'basePrice' => ['amount' => 9.5], - 'finalPrice' => ['amount' => 9.5], - ], - 'type' => 'fixed', - 'name' => 'drop_down option 1', - ], - 1 => [ - 'prices' => ['oldPrice' => ['amount' => 40, 'adjustments' => []], - 'basePrice' => ['amount' => 38], - 'finalPrice' => ['amount' => 38], - ], - 'type' => 'percent', - 'name' => 'drop_down option 2', - ], - ]; - } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php index 7389799c00362..6245e4e9f8de7 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -67,6 +67,12 @@ class CategoryTest extends AbstractBackendController */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class + => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + ] + ]); parent::setUp(); /** @var ProductResource $productResource */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php index 6bf521f098fa0..c53ee2170d4b4 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php @@ -5,16 +5,26 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action; +use Magento\Backend\Model\Session; +use Magento\Catalog\Block\Product\ListProduct; +use Magento\Catalog\Helper\Product\Edit\Action\Attribute; +use Magento\Catalog\Model\CategoryFactory; use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Message\MessageInterface; use Magento\Catalog\Model\ProductRepository; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\UrlInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\MessageQueue\EnvironmentPreconditionException; +use Magento\TestFramework\MessageQueue\PreconditionFailedException; use Magento\TestFramework\MessageQueue\PublisherConsumerController; +use Magento\TestFramework\TestCase\AbstractBackendController; /** * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController +class AttributeTest extends AbstractBackendController { /** @var PublisherConsumerController */ private $publisherConsumerController; @@ -22,7 +32,9 @@ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendContr protected function setUp(): void { - $this->publisherConsumerController = Bootstrap::getObjectManager()->create( + parent::setUp(); + + $this->publisherConsumerController = $this->_objectManager->create( PublisherConsumerController::class, [ 'consumers' => $this->consumers, @@ -34,15 +46,13 @@ protected function setUp(): void try { $this->publisherConsumerController->startConsumers(); - } catch (\Magento\TestFramework\MessageQueue\EnvironmentPreconditionException $e) { + } catch (EnvironmentPreconditionException $e) { $this->markTestSkipped($e->getMessage()); - } catch (\Magento\TestFramework\MessageQueue\PreconditionFailedException $e) { + } catch (PreconditionFailedException $e) { $this->fail( $e->getMessage() ); } - - parent::setUp(); } protected function tearDown(): void @@ -59,10 +69,8 @@ protected function tearDown(): void */ public function testSaveActionRedirectsSuccessfully() { - $objectManager = Bootstrap::getObjectManager(); - - /** @var $session \Magento\Backend\Model\Session */ - $session = $objectManager->get(\Magento\Backend\Model\Session::class); + /** @var $session Session */ + $session = $this->_objectManager->get(Session::class); $session->setProductIds([1]); $this->getRequest()->setMethod(HttpRequest::METHOD_POST); @@ -70,10 +78,10 @@ public function testSaveActionRedirectsSuccessfully() $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); /** @var \Magento\Backend\Model\UrlInterface $urlBuilder */ - $urlBuilder = $objectManager->get(\Magento\Framework\UrlInterface::class); + $urlBuilder = $this->_objectManager->get(UrlInterface::class); - /** @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper */ - $attributeHelper = $objectManager->get(\Magento\Catalog\Helper\Product\Edit\Action\Attribute::class); + /** @var Attribute $attributeHelper */ + $attributeHelper = $this->_objectManager->get(Attribute::class); $expectedUrl = $urlBuilder->getUrl( 'catalog/product/index', ['store' => $attributeHelper->getSelectedStoreId()] @@ -98,18 +106,15 @@ public function testSaveActionRedirectsSuccessfully() */ public function testSaveActionChangeVisibility($attributes) { - $objectManager = Bootstrap::getObjectManager(); /** @var ProductRepository $repository */ - $repository = Bootstrap::getObjectManager()->create( - ProductRepository::class - ); + $repository = $this->_objectManager->create(ProductRepository::class); $product = $repository->get('simple'); $product->setOrigData(); $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); $product->save(); - /** @var $session \Magento\Backend\Model\Session */ - $session = $objectManager->get(\Magento\Backend\Model\Session::class); + /** @var $session Session */ + $session = $this->_objectManager->get(Session::class); $session->setProductIds([$product->getId()]); $this->getRequest()->setParam('attributes', $attributes); $this->getRequest()->setMethod(HttpRequest::METHOD_POST); @@ -117,13 +122,9 @@ public function testSaveActionChangeVisibility($attributes) $this->dispatch('backend/catalog/product_action_attribute/save/store/0'); /** @var \Magento\Catalog\Model\Category $category */ - $categoryFactory = Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\CategoryFactory::class - ); - /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */ - $listProduct = Bootstrap::getObjectManager()->get( - \Magento\Catalog\Block\Product\ListProduct::class - ); + $categoryFactory = $this->_objectManager->get(CategoryFactory::class); + /** @var ListProduct $listProduct */ + $listProduct = $this->_objectManager->get(ListProduct::class); $this->publisherConsumerController->waitForAsynchronousResult( function () use ($repository) { @@ -159,10 +160,8 @@ function () use ($repository) { */ public function testValidateActionWithMassUpdate($attributes) { - $objectManager = Bootstrap::getObjectManager(); - - /** @var $session \Magento\Backend\Model\Session */ - $session = $objectManager->get(\Magento\Backend\Model\Session::class); + /** @var $session Session */ + $session = $this->_objectManager->get(Session::class); $session->setProductIds([1, 2]); $this->getRequest()->setParam('attributes', $attributes); @@ -214,4 +213,34 @@ public function saveActionVisibilityAttrDataProvider() ['arguments' => ['visibility' => Visibility::VISIBILITY_IN_CATALOG]] ]; } + + /** + * Assert that custom layout update can not be change for existing entity. + * + * @return void + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testSaveActionCantChangeCustomLayoutUpdate(): void + { + /** @var ProductRepository $repository */ + $repository = $this->_objectManager->get(ProductRepository::class); + $product = $repository->get('simple'); + + $product->setOrigData('custom_layout_update', 'test'); + $product->setData('custom_layout_update', 'test'); + $product->save(); + /** @var $session Session */ + $session = $this->_objectManager->get(Session::class); + $session->setProductIds([$product->getId()]); + $this->getRequest()->setParam('attributes', ['custom_layout_update' => 'test2']); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + + $this->dispatch('backend/catalog/product_action_attribute/save/store/0'); + + $this->assertSessionMessages( + $this->equalTo(['Custom layout update text cannot be changed, only removed']), + MessageInterface::TYPE_ERROR + ); + $this->assertEquals('test', $product->getData('custom_layout_update')); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/MassDeleteTest.php new file mode 100644 index 0000000000000..6384883c56c58 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/MassDeleteTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for mass product deleting. + * + * @see \Magento\Catalog\Controller\Adminhtml\Product\MassDelete + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class MassDeleteTest extends AbstractBackendController +{ + /** @var ProductRepositoryInterface */ + protected $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/multiple_products.php + * + * @return void + */ + public function testDeleteSimpleProductViaMassAction(): void + { + $productIds = [10, 11, 12]; + $this->dispatchMassDeleteAction($productIds); + $this->assertSuccessfulDeleteProducts(count($productIds)); + } + + /** + * @return void + */ + public function testDeleteNotExistingProductViaMassAction(): void + { + $this->dispatchMassDeleteAction([989]); + $this->assertSessionMessages($this->isEmpty(), MessageInterface::TYPE_ERROR); + $this->assertRedirect($this->stringContains('backend/catalog/product/index')); + } + + /** + * @return void + */ + public function testMassDeleteWithoutProductIds(): void + { + $this->markTestSkipped('Test is blocked by issue MC-34495'); + $this->dispatchMassDeleteAction(); + $this->assertSessionMessages( + $this->equalTo('An item needs to be selected. Select and try again.'), + MessageInterface::TYPE_ERROR + ); + $this->assertRedirect($this->stringContains('backend/catalog/product/index')); + } + + /** + * Assert successful delete products. + * + * @param int $productCount + * @return void + */ + protected function assertSuccessfulDeleteProducts(int $productCount): void + { + $this->assertSessionMessages( + $this->equalTo([(string)__('A total of %1 record(s) have been deleted.', $productCount)]), + MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('backend/catalog/product/index')); + } + + /** + * Dispatch mass delete action. + * + * @param array $productIds + * @return void + */ + protected function dispatchMassDeleteAction(array $productIds = []): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams(['selected' => $productIds, 'namespace' => 'product_listing']); + $this->dispatch('backend/catalog/product/massDelete/'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php index 65e7e94f4aa24..7032199e9fc4c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php @@ -49,6 +49,12 @@ class ProductTest extends \Magento\TestFramework\TestCase\AbstractBackendControl */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => + \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class + ] + ]); parent::setUp(); $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); @@ -412,6 +418,7 @@ private function getProductData(array $tierPrice) $repo = $this->repositoryFactory->create(); $product = $repo->get('tier_prices')->getData(); $product['tier_price'] = $tierPrice; + $product['entity_id'] = null; /** @phpstan-ignore-next-line */ unset($product['entity_id']); return $product; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php index 07cc43921d59f..3f023a75d0f92 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php @@ -53,6 +53,12 @@ protected function setUp(): void parent::setUp(); $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class + => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + ] + ]); $this->registry = $this->objectManager->get(Registry::class); $this->layout = $this->objectManager->get(LayoutInterface::class); $this->session = $this->objectManager->get(Session::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php index 460488fdfae76..4f046eccbe59f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php @@ -412,7 +412,7 @@ protected function _assertCompareListEquals(array $expectedProductIds) $compareItems = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection::class ); - $compareItems->useProductItem(true); + $compareItems->useProductItem(); // important $compareItems->setVisitorId( \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php index 5458de89e9b82..e8f9607530fba 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php @@ -13,6 +13,7 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Visibility; use Magento\Eav\Model\Entity\Type; +use Magento\Framework\App\Cache\Manager; use Magento\Framework\App\Http; use Magento\Framework\Registry; use Magento\Store\Model\StoreManagerInterface; @@ -268,6 +269,30 @@ public function testProductWithoutWebsite(): void $this->assert404NotFound(); } + /** + * Test that 404 page has product tag if product is not visible + * + * @magentoDataFixture Magento/Quote/_files/is_not_salable_product.php + * @magentoCache full_page enabled + * @return void + */ + public function test404NotFoundPageCacheTags(): void + { + $cache = $this->_objectManager->get(Manager::class); + $cache->clean(['full_page']); + $product = $this->productRepository->get('simple-99'); + $this->dispatch(sprintf('catalog/product/view/id/%s/', $product->getId())); + $this->assert404NotFound(); + $pTag = Product::CACHE_TAG . '_' . $product->getId(); + $hTags = $this->getResponse()->getHeader('X-Magento-Tags'); + $tags = $hTags && $hTags->getFieldValue() ? explode(',', $hTags->getFieldValue()) : []; + $this->assertContains( + $pTag, + $tags, + "Failed asserting that X-Magento-Tags: {$hTags->getFieldValue()} contains \"$pTag\"" + ); + } + /** * @param string|ProductInterface $product * @param array $data diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php index 4494ccf1eb3fe..3f9f788dc28c7 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php @@ -42,6 +42,12 @@ protected function setUp(): void if (defined('HHVM_VERSION')) { $this->markTestSkipped('Randomly fails due to known HHVM bug (DOMText mixed with DOMElement)'); } + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => + \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class + ] + ]); parent::setUp(); $this->registry = $this->_objectManager->get(Registry::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompositeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompositeTest.php index a558a99bd2f17..bc310f8bd65b5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompositeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompositeTest.php @@ -3,34 +3,51 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Helper\Product; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; use Magento\Customer\Controller\RegistryConstants; +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** * Test Composite */ -class CompositeTest extends \PHPUnit\Framework\TestCase +class CompositeTest extends TestCase { - /** - * @var Composite - */ - protected $helper; + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Composite */ + private $helper; + + /** @var Registry */ + private $registry; + + /** @var ProductRepositoryInterface */ + private $productRepository; /** - * @var Registry + * @inheritdoc */ - protected $registry; - protected function setUp(): void { - $this->helper = Bootstrap::getObjectManager()->get(\Magento\Catalog\Helper\Product\Composite::class); - $this->registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->helper = $this->objectManager->get(Composite::class); + $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('composite_configure_result_error_message'); @@ -42,40 +59,85 @@ protected function tearDown(): void /** * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void */ - public function testRenderConfigureResult() + public function testRenderConfigureResult(): void { - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = Bootstrap::getObjectManager()->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class - ); - /** @var $product \Magento\Catalog\Model\Product */ - $product = $productRepository->get('simple'); - - $configureResult = new \Magento\Framework\DataObject(); + $product = $this->productRepository->get('simple'); + /** @var DataObject $buyRequest */ + $buyRequest = $this->objectManager->create(DataObject::class); + $buyRequest->setData(['qty' => 1]); + /** @var DataObject $configureResult */ + $configureResult = $this->objectManager->create(DataObject::class); $configureResult->setOk(true) ->setProductId($product->getId()) + ->setBuyRequest($buyRequest) ->setCurrentCustomerId(1); - $this->helper->renderConfigureResult($configureResult); + $resultLayout = $this->helper->renderConfigureResult($configureResult); - $customerId = $this->registry->registry(RegistryConstants::CURRENT_CUSTOMER_ID); - $this->assertEquals(1, $customerId); - $errorMessage = $this->registry->registry('composite_configure_result_error_message'); - $this->assertNull($errorMessage); + /** @var Product $preparedProduct */ + $preparedProduct = $this->registry->registry('product'); + $preparedCurrentProduct = $this->registry->registry('current_product'); + $this->assertTrue($preparedProduct && $preparedCurrentProduct); + $this->assertEquals(1, $this->registry->registry(RegistryConstants::CURRENT_CUSTOMER_ID)); + $this->assertNotNull($preparedProduct->getPreconfiguredValues()); + $this->assertContains( + 'CATALOG_PRODUCT_COMPOSITE_CONFIGURE', + $resultLayout->getLayout()->getUpdate()->getHandles() + ); + $this->assertContains( + 'catalog_product_view_type_' . $product->getTypeId(), + $resultLayout->getLayout()->getUpdate()->getHandles() + ); } - public function testRenderConfigureResultNotOK() + /** + * @dataProvider renderConfigureResultExceptionProvider + * @param array $data + * @param string $expectedErrorMessage + * @return void + */ + public function testRenderConfigureResultException(array $data, string $expectedErrorMessage): void { - $configureResult = new \Magento\Framework\DataObject(); - $configureResult->setError(true) - ->setMessage('Test Message'); + /** @var DataObject $configureResult */ + $configureResult = $this->objectManager->create(DataObject::class); + $configureResult->setData($data); - $this->helper->renderConfigureResult($configureResult); + $resultLayout = $this->helper->renderConfigureResult($configureResult); + + $this->assertEquals( + $expectedErrorMessage, + $this->registry->registry('composite_configure_result_error_message') + ); + $this->assertContains( + 'CATALOG_PRODUCT_COMPOSITE_CONFIGURE_ERROR', + $resultLayout->getLayout()->getUpdate()->getHandles() + ); + } - $customerId = $this->registry->registry(RegistryConstants::CURRENT_CUSTOMER_ID); - $this->assertNull($customerId); - $errorMessage = $this->registry->registry('composite_configure_result_error_message'); - $this->assertEquals('Test Message', $errorMessage); + /** + * Create render configure result exception provider + * + * @return array + */ + public function renderConfigureResultExceptionProvider(): array + { + return [ + 'error_true' => [ + 'data' => [ + 'error' => true, + 'message' => 'Test Message' + ], + 'expected_error_message' => 'Test Message', + ], + 'without_product' => [ + 'data' => [ + 'ok' => true, + ], + 'expected_error_message' => 'The product that was requested doesn\'t exist.' + . ' Verify the product and try again.', + ], + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php index 506556dbe95b3..da6aa44df3e6a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php @@ -55,6 +55,12 @@ private function recreateCategory(): void */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class + => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + ] + ]); $this->categoryFactory = Bootstrap::getObjectManager()->get(CategoryFactory::class); $this->recreateCategory(); $this->attribute = $this->category->getAttributes()['custom_layout_update_file']->getBackend(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php index f9237e89817f1..1d846fc154fc0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php @@ -3,15 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate; +use Magento\Catalog\Model\CategoryFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Framework\Registry; use PHPUnit\Framework\TestCase; -use Magento\Catalog\Model\Category; -use Magento\Catalog\Model\CategoryFactory; -use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate; /** * @magentoDbIsolation enabled @@ -40,6 +45,16 @@ class DataProviderTest extends TestCase */ private $fakeFiles; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * Create subject instance. * @@ -58,22 +73,30 @@ private function createDataProvider(): DataProvider } /** - * {@inheritDoc} + * @inheritDoc */ protected function setUp(): void { - parent::setUp(); $objectManager = Bootstrap::getObjectManager(); + $objectManager->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class + => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + ] + ]); + parent::setUp(); $this->dataProvider = $this->createDataProvider(); $this->registry = $objectManager->get(Registry::class); $this->categoryFactory = $objectManager->get(CategoryFactory::class); $this->fakeFiles = $objectManager->get(CategoryLayoutUpdateManager::class); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); } /** * @return void */ - public function testGetMetaRequiredAttributes() + public function testGetMetaRequiredAttributes(): void { $requiredAttributes = [ 'general' => ['name'], @@ -221,4 +244,48 @@ public function testCustomLayoutMeta(): void sort($list); $this->assertEquals($expectedList, $list); } + + /** + * Check if existing category page layout will remain unaffected by category page layout default value setting + * + * @return void + */ + public function testExistingCategoryLayoutUnaffectedByDefaults(): void + { + /** @var Category $category */ + $category = $this->categoryFactory->create(); + $category->load(2); + + $this->registry->register('category', $category); + $meta = $this->dataProvider->getMeta(); + $categoryPageLayout = $meta["design"]["children"]["page_layout"]["arguments"]["data"]["config"]["default"]; + $this->registry->unregister('category'); + + $this->assertNull($categoryPageLayout); + } + + /** + * Check if category page layout default value setting will apply to the new category during it's creation + * + * @throws NoSuchEntityException + */ + public function testNewCategoryLayoutMatchesDefault(): void + { + $categoryDefaultPageLayout = $this->scopeConfig->getValue( + 'web/default_layouts/default_category_layout', + ScopeInterface::SCOPE_STORE, + $this->storeManager->getStore()->getId() + ); + + /** @var Category $category */ + $category = $this->categoryFactory->create(); + $category->setName('Net Test Category'); + + $this->registry->register('category', $category); + $meta = $this->dataProvider->getMeta(); + $categoryPageLayout = $meta["design"]["children"]["page_layout"]["arguments"]["data"]["config"]["default"]; + $this->registry->unregister('category'); + + $this->assertEquals($categoryDefaultPageLayout, $categoryPageLayout); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php index bfacdb85bbcce..7fd7627c738d6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php @@ -53,6 +53,12 @@ class CategoryRepositoryTest extends TestCase */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class + => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + ] + ]); $this->repositoryFactory = Bootstrap::getObjectManager()->get(CategoryRepositoryInterfaceFactory::class); $this->layoutManager = Bootstrap::getObjectManager()->get(CategoryLayoutUpdateManager::class); $this->productCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php index 13437554febd3..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; @@ -49,7 +51,7 @@ class CategoryTest extends TestCase */ protected $objectManager; - /** @var CategoryRepository */ + /** @var CategoryResource */ private $categoryResource; /** @var CategoryRepositoryInterface */ @@ -355,6 +357,17 @@ public function testDeleteChildren(): void $this->assertEquals($this->_model->getId(), null); } + /** + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/categories_no_products.php + */ + public function testChildrenCountAfterDeleteParentCategory(): void + { + $this->categoryRepository->deleteByIdentifier(3); + $this->assertEquals(8, $this->categoryResource->getChildrenCount(1)); + } + /** * @magentoDataFixture Magento/Catalog/_files/category.php */ @@ -408,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/Indexer/Product/Flat/Action/RelationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php index 51b1d4fdb7fe0..e3b5bc8d5fd0d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php @@ -15,6 +15,8 @@ /** * Test relation customization + * + * @magentoDbIsolation disabled */ class RelationTest extends \Magento\TestFramework\Indexer\TestCase { 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/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index f9d235493297f..2659f14c07c7a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -20,6 +20,7 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; /** @@ -79,6 +80,15 @@ class UpdateHandlerTest extends \PHPUnit\Framework\TestCase */ private $mediaAttributeId; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** + * @var int + */ + private $currentStoreId; + /** * @inheritdoc */ @@ -93,6 +103,8 @@ protected function setUp(): void $this->productResource = $this->objectManager->create(ProductResource::class); $this->mediaAttributeId = (int)$this->productResource->getAttribute('media_gallery')->getAttributeId(); $this->config = $this->objectManager->get(Config::class); + $this->storeManager = $this->objectManager->create(StoreManagerInterface::class); + $this->currentStoreId = $this->storeManager->getStore()->getId(); $this->mediaDirectory = $this->objectManager->get(Filesystem::class) ->getDirectoryWrite(DirectoryList::MEDIA); $this->mediaDirectory->writeFile($this->fileName, 'Test'); @@ -274,7 +286,7 @@ public function testExecuteWithImageToDelete(): void $this->updateHandler->execute($product); $productImages = $this->galleryResource->loadProductGalleryByAttributeId($product, $this->mediaAttributeId); $this->assertCount(0, $productImages); - $this->assertFileNotExists( + $this->assertFileDoesNotExist( $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $image) ); $defaultImages = $this->productResource->getAttributeRawValue( @@ -344,6 +356,7 @@ public function testExecuteWithTwoImagesOnStoreView(): void */ protected function tearDown(): void { + $this->storeManager->setCurrentStore($this->currentStoreId); parent::tearDown(); $this->mediaDirectory->getDriver()->deleteFile($this->mediaDirectory->getAbsolutePath($this->fileName)); $this->galleryResource->getConnection() @@ -377,4 +390,91 @@ private function updateProductGalleryImages(ProductInterface $product, array $im $product->setData('store_id', Store::DEFAULT_STORE_ID); $product->setData('media_gallery', ['images' => ['image' => array_merge($image, $imageData)]]); } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoDbIsolation disabled + * @return void + */ + public function testDeleteWithMultiWebsites(): void + { + $defaultWebsiteId = (int) $this->storeManager->getWebsite('base')->getId(); + $secondWebsiteId = (int) $this->storeManager->getWebsite('test')->getId(); + $defaultStoreId = (int) $this->storeManager->getStore('default')->getId(); + $secondStoreId = (int) $this->storeManager->getStore('fixture_second_store')->getId(); + $imageRoles = ['image', 'small_image', 'thumbnail']; + $globalScopeId = Store::DEFAULT_STORE_ID; + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $product = $this->getProduct($globalScopeId); + // Assert that product has images + $this->assertNotEmpty($product->getMediaGalleryEntries()); + $image = $product->getImage(); + $path = $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $image); + $this->assertFileExists($path); + // Assign product to default and second website and save changes + $product->setWebsiteIds([$defaultWebsiteId, $secondWebsiteId]); + $this->productRepository->save($product); + // Assert that product image has roles in global scope only + $imageRolesPerStore = $this->getProductStoreImageRoles($product); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['image']); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['small_image']); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['thumbnail']); + $this->assertArrayNotHasKey($defaultStoreId, $imageRolesPerStore); + $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); + // Assign roles to product image on second store and save changes + $this->storeManager->setCurrentStore($secondStoreId); + $product = $this->getProduct($secondStoreId); + $product->addData(array_fill_keys($imageRoles, $image)); + $this->productRepository->save($product); + // Assert that roles are assigned to product image for second store + $imageRolesPerStore = $this->getProductStoreImageRoles($product); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['image']); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['small_image']); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['thumbnail']); + $this->assertArrayNotHasKey($defaultStoreId, $imageRolesPerStore); + $this->assertEquals($image, $imageRolesPerStore[$secondStoreId]['image']); + $this->assertEquals($image, $imageRolesPerStore[$secondStoreId]['small_image']); + $this->assertEquals($image, $imageRolesPerStore[$secondStoreId]['thumbnail']); + // Delete existing images and save changes + $this->storeManager->setCurrentStore($globalScopeId); + $product = $this->getProduct($globalScopeId); + $product->setMediaGalleryEntries([]); + $this->productRepository->save($product); + $product = $this->getProduct($globalScopeId); + // Assert that image was not deleted as it has roles in second store + $this->assertNotEmpty($product->getMediaGalleryEntries()); + $this->assertFileExists($path); + // Unlink second website, delete existing images and save changes + $product->setWebsiteIds([$defaultWebsiteId]); + $product->setMediaGalleryEntries([]); + $this->productRepository->save($product); + $product = $this->getProduct($globalScopeId); + // Assert that image was deleted and product has no images + $this->assertEmpty($product->getMediaGalleryEntries()); + $this->assertFileDoesNotExist($path); + // Load image roles + $imageRolesPerStore = $this->getProductStoreImageRoles($product); + // Assert that image roles are reset on global scope and removed on second store + // as the product is no longer assigned to second website + $this->assertEquals('no_selection', $imageRolesPerStore[$globalScopeId]['image']); + $this->assertEquals('no_selection', $imageRolesPerStore[$globalScopeId]['small_image']); + $this->assertEquals('no_selection', $imageRolesPerStore[$globalScopeId]['thumbnail']); + $this->assertArrayNotHasKey($defaultStoreId, $imageRolesPerStore); + $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); + } + + /** + * @param Product $product + * @return array + */ + private function getProductStoreImageRoles(Product $product): array + { + $imageRolesPerStore = []; + $stores = array_keys($this->storeManager->getStores(true)); + foreach ($this->galleryResource->getProductImages($product, $stores) as $role) { + $imageRolesPerStore[$role['store_id']][$role['attribute_code']] = $role['filepath']; + } + return $imageRolesPerStore; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Price/SpecialPriceStorageTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Price/SpecialPriceStorageTest.php new file mode 100644 index 0000000000000..0d8b0a825d24c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Price/SpecialPriceStorageTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\SpecialPriceInterface; +use Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test special price storage model + */ +class SpecialPriceStorageTest extends TestCase +{ + /** + * @var SpecialPriceStorage + */ + private $model; + /** + * @var SpecialPriceInterfaceFactory + */ + private $specialPriceFactory; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->model = $objectManager->get(SpecialPriceStorage::class); + $this->specialPriceFactory = $objectManager->get(SpecialPriceInterfaceFactory::class); + } + + /** + * Test that price update validation works correctly + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testUpdateValidationResult() + { + $date = new \Datetime('+2 days'); + $date->setTime(0, 0); + /** @var SpecialPriceInterface $price */ + $price = $this->specialPriceFactory->create(); + $price->setSku('invalid') + ->setStoreId(0) + ->setPrice(5.0) + ->setPriceFrom($date->format('Y-m-d H:i:s')) + ->setPriceTo( + $date->modify('+1 day') + ->format('Y-m-d H:i:s') + ); + $result = $this->model->update([$price]); + $this->assertCount(1, $result); + $this->assertStringContainsString( + 'The product that was requested doesn\'t exist.', + (string) $result[0]->getMessage() + ); + $price->setSku('simple333'); + $result = $this->model->update([$price]); + $this->assertCount(0, $result); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php index 0fe3ef55455d2..f1d1352bcb05b 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php @@ -7,15 +7,22 @@ namespace Magento\Catalog\Model; -use Magento\Backend\Model\Auth; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Media\ConfigInterface; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Framework\Api\SearchCriteriaBuilder; +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\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\Bootstrap as TestBootstrap; -use Magento\Framework\Acl\Builder; +use PHPUnit\Framework\TestCase; /** * Provide tests for ProductRepository model. @@ -24,8 +31,19 @@ * @magentoAppIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ProductRepositoryTest extends \PHPUnit\Framework\TestCase +class ProductRepositoryTest extends TestCase { + private const STUB_STORE_ID = 1; + private const STUB_STORE_ID_GLOBAL = 0; + private const STUB_PRODUCT_NAME = 'Simple Product'; + private const STUB_UPDATED_PRODUCT_NAME = 'updated'; + private const STUB_PRODUCT_SKU = 'simple'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + /** * Test subject. * @@ -54,40 +72,74 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase private $layoutManager; /** - * Sets up common objects + * @var ConfigInterface + */ + private $mediaConfig; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var array + */ + private $productSkusToDelete = []; + + /** + * @inheritdoc */ protected function setUp(): void { - $this->productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); - $this->searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); - $this->productFactory = Bootstrap::getObjectManager()->get(ProductFactory::class); - $this->productResource = Bootstrap::getObjectManager()->get(ProductResource::class); - $this->layoutManager = Bootstrap::getObjectManager()->get(ProductLayoutUpdateManager::class); + parent::setUp(); + + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => + \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class + ] + ]); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $this->productFactory = $this->objectManager->get(ProductFactory::class); + $this->productResource = $this->objectManager->get(ProductResource::class); + $this->layoutManager = $this->objectManager->get(ProductLayoutUpdateManager::class); + $this->mediaConfig = $this->objectManager->get(ConfigInterface::class); + $this->mediaDirectory = $this->objectManager->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); } /** - * Create new subject instance. - * - * @return ProductRepositoryInterface + * @inheritdoc */ - private function createRepo(): ProductRepositoryInterface + protected function tearDown(): void { - return Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); + foreach ($this->productSkusToDelete as $productSku) { + try { + $this->productRepository->deleteById($productSku); + } catch (NoSuchEntityException $e) { + //Product already removed + } + } + + parent::tearDown(); } /** * Checks filtering by store_id * * @magentoDataFixture Magento/Catalog/Model/ResourceModel/_files/product_simple.php + * @return void */ - public function testFilterByStoreId() + public function testFilterByStoreId(): void { $searchCriteria = $this->searchCriteriaBuilder ->addFilter('store_id', '1', 'eq') ->create(); $list = $this->productRepository->getList($searchCriteria); $count = $list->getTotalCount(); - $this->assertGreaterThanOrEqual(1, $count); } @@ -99,13 +151,11 @@ public function testFilterByStoreId() * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @dataProvider skuDataProvider */ - public function testGetProduct(string $sku) : void + public function testGetProduct(string $sku): void { $expectedSku = 'simple'; $product = $this->productRepository->get($sku); - - self::assertNotEmpty($product); - self::assertEquals($expectedSku, $product->getSku()); + $this->assertEquals($expectedSku, $product->getSku()); } /** @@ -127,45 +177,29 @@ public function skuDataProvider(): array * * @magentoDataFixture Magento/Catalog/_files/product_simple_with_image.php * - * @throws \Magento\Framework\Exception\CouldNotSaveException - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\StateException + * @return void + * @throws CouldNotSaveException + * @throws InputException + * @throws StateException */ public function testSaveProductWithGalleryImage(): void { - /** @var $mediaConfig \Magento\Catalog\Model\Product\Media\Config */ - $mediaConfig = Bootstrap::getObjectManager() - ->get(\Magento\Catalog\Model\Product\Media\Config::class); - - /** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ - $mediaDirectory = Bootstrap::getObjectManager() - ->get(\Magento\Framework\Filesystem::class) - ->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); - - $product = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); - $product->load(1); - - $path = $mediaConfig->getBaseMediaPath() . '/magento_image.jpg'; - $absolutePath = $mediaDirectory->getAbsolutePath() . $path; + $product = $this->productRepository->get('simple'); + $path = $this->mediaConfig->getBaseMediaPath() . '/magento_image.jpg'; + $absolutePath = $this->mediaDirectory->getAbsolutePath() . $path; $product->addImageToMediaGallery( $absolutePath, [ - 'image', - 'small_image', + 'image', + 'small_image', ], false, false ); - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $productRepository->save($product); - + $this->productRepository->save($product); $gallery = $product->getData('media_gallery'); $this->assertArrayHasKey('images', $gallery); $images = array_values($gallery['images']); - $this->assertNotEmpty($gallery); $this->assertTrue(isset($images[0]['file'])); $this->assertStringStartsWith('/m/a/magento_image', $images[0]['file']); @@ -179,58 +213,121 @@ public function testSaveProductWithGalleryImage(): void * Test Product Repository can change(update) "sku" for given product. * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation enabled * @magentoAppArea adminhtml + * @return void */ - public function testUpdateProductSku() + public function testUpdateProductSku(): void { $newSku = 'simple-edited'; $productId = $this->productResource->getIdBySku('simple'); $initialProduct = $this->productFactory->create(); $this->productResource->load($initialProduct, $productId); - $initialProduct->setSku($newSku); $this->productRepository->save($initialProduct); - + $this->productSkusToDelete[] = $newSku; $updatedProduct = $this->productFactory->create(); $this->productResource->load($updatedProduct, $productId); - self::assertSame($newSku, $updatedProduct->getSku()); - - //clean up. - $this->productRepository->delete($updatedProduct); + $this->assertSame($newSku, $updatedProduct->getSku()); } /** * Test that custom layout file attribute is saved. * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @return void * @throws \Throwable - * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled */ public function testCustomLayout(): void { - //New valid value - $repo = $this->createRepo(); - $product = $repo->get('simple'); + $product = $this->productRepository->get('simple'); $newFile = 'test'; $this->layoutManager->setFakeFiles((int)$product->getId(), [$newFile]); $product->setCustomAttribute('custom_layout_update_file', $newFile); - $repo->save($product); - $repo = $this->createRepo(); - $product = $repo->get('simple'); + $this->productRepository->save($product); + $product = $this->productRepository->get('simple'); $this->assertEquals($newFile, $product->getCustomAttribute('custom_layout_update_file')->getValue()); - - //Setting non-existent value $newFile = 'does not exist'; $product->setCustomAttribute('custom_layout_update_file', $newFile); - $caughtException = false; - try { - $repo->save($product); - } catch (LocalizedException $exception) { - $caughtException = true; - } - $this->assertTrue($caughtException); + $this->expectException(LocalizedException::class); + $this->productRepository->save($product); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoAppArea adminhtml + * + * @return void + */ + public function testDeleteByIdSimpleProduct(): void + { + $productSku = 'simple-1'; + $result = $this->productRepository->deleteById($productSku); + $this->assertTrue($result); + $this->assertProductNotExist($productSku); + } + + /** + * Assert that product does not exist. + * + * @param string $sku + * @return void + */ + private function assertProductNotExist(string $sku): void + { + $this->expectExceptionObject(new NoSuchEntityException( + __("The product that was requested doesn't exist. Verify the product and try again.") + )); + $this->productRepository->get($sku); + } + + /** + * Tests product repository update + * + * @dataProvider productUpdateDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @param int $storeId + * @param int $checkStoreId + * @param string $expectedNameStore + * @param string $expectedNameCheckedStore + */ + public function testProductUpdate( + int $storeId, + int $checkStoreId, + string $expectedNameStore, + string $expectedNameCheckedStore + ): void { + $sku = self::STUB_PRODUCT_SKU; + + $product = $this->productRepository->get($sku, false, $storeId); + $product->setName(self::STUB_UPDATED_PRODUCT_NAME); + $this->productRepository->save($product); + $productNameStoreId = $this->productRepository->get($sku, false, $storeId)->getName(); + $productNameCheckedStoreId = $this->productRepository->get($sku, false, $checkStoreId)->getName(); + + $this->assertEquals($expectedNameStore, $productNameStoreId); + $this->assertEquals($expectedNameCheckedStore, $productNameCheckedStoreId); + } + + /** + * Product update data provider + * + * @return array + */ + public function productUpdateDataProvider(): array + { + return [ + 'Updating for global store' => [ + self::STUB_STORE_ID_GLOBAL, + self::STUB_STORE_ID, + self::STUB_UPDATED_PRODUCT_NAME, + self::STUB_UPDATED_PRODUCT_NAME, + ], + 'Updating for store' => [ + self::STUB_STORE_ID, + self::STUB_STORE_ID_GLOBAL, + self::STUB_UPDATED_PRODUCT_NAME, + self::STUB_PRODUCT_NAME, + ], + ]; } } 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/Model/ResourceModel/Attribute/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/CollectionTest.php new file mode 100644 index 0000000000000..607e5b6de4541 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/CollectionTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Attribute; + +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection + */ +class CollectionTest extends TestCase +{ + /** + * @var CollectionFactory . + */ + private $attributesCollectionFactory; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->attributesCollectionFactory = $objectManager->get(CollectionFactory::class); + } + + /** + * @magentoAppArea adminhtml + * @dataProvider attributesCollectionGetCurrentPageDataProvider + * + * @param array|null $condition + * @param int $currentPage + * @param int $expectedCurrentPage + * @return void + */ + public function testAttributesCollectionGetCurrentPage( + ?array $condition, + int $currentPage, + int $expectedCurrentPage + ): void { + $attributeCollection = $this->attributesCollectionFactory->create(); + $attributeCollection->setCurPage($currentPage)->setPageSize(20); + + if ($condition !== null) { + $attributeCollection->addFieldToFilter('is_global', $condition); + } + + $this->assertEquals($expectedCurrentPage, (int)$attributeCollection->getCurPage()); + } + + /** + * @return array[] + */ + public function attributesCollectionGetCurrentPageDataProvider(): array + { + return [ + [ + 'condition' => null, + 'currentPage' => 1, + 'expectedCurrentPage' => 1, + ], + [ + 'condition' => ['eq' => 0], + 'currentPage' => 1, + 'expectedCurrentPage' => 1, + ], + [ + 'condition' => ['eq' => 0], + 'currentPage' => 15, + 'expectedCurrentPage' => 1, + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php index 19e62d7a50606..263a1c41ac8df 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php @@ -5,21 +5,48 @@ */ namespace Magento\Catalog\Model\ResourceModel\Eav; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + /** * Test for \Magento\Catalog\Model\ResourceModel\Eav\Attribute. */ class AttributeTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Attribute */ - protected $_model; + private $model; + /** + * @var AttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var int|string + */ + private $catalogProductEntityType; + + /** + * @inheritDoc + */ protected function setUp(): void { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(Attribute::class); + $this->attributeRepository = $this->objectManager->get(AttributeRepositoryInterface::class); + $this->catalogProductEntityType = $this->objectManager->get(Config::class) + ->getEntityType('catalog_product') + ->getId(); } /** @@ -29,18 +56,28 @@ protected function setUp(): void */ public function testCRUD() { - $this->_model->setAttributeCode( - 'test' - )->setEntityTypeId( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Eav\Model\Config::class - )->getEntityType( - 'catalog_product' - )->getId() - )->setFrontendLabel( - 'test' - )->setIsUserDefined(1); - $crud = new \Magento\TestFramework\Entity($this->_model, ['frontend_label' => uniqid()]); + $this->model->setAttributeCode('test') + ->setEntityTypeId($this->catalogProductEntityType) + ->setFrontendLabel('test') + ->setIsUserDefined(1); + $crud = new \Magento\TestFramework\Entity($this->model, [AttributeInterface::FRONTEND_LABEL => uniqid()]); $crud->testCrud(); } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_attribute.php + * + * @return void + */ + public function testAttributeSaveWithChangedEntityType(): void + { + $this->expectException( + \Magento\Framework\Exception\LocalizedException::class + ); + $this->expectExceptionMessage('Do not change entity type.'); + + $attribute = $this->attributeRepository->get($this->catalogProductEntityType, 'test_attribute_code_333'); + $attribute->setEntityTypeId(1); + $attribute->save(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/CombinationWithDifferentTypePricesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationAbstract.php similarity index 55% rename from dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/CombinationWithDifferentTypePricesTest.php rename to dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationAbstract.php index 7d366811952ca..fce502b2dfea0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/CombinationWithDifferentTypePricesTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationAbstract.php @@ -5,12 +5,16 @@ */ declare(strict_types=1); -namespace Magento\Catalog\Pricing\Render; +namespace Magento\Catalog\Pricing\Render\PriceTypes; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface; +use Magento\Catalog\Model\Product\Option; use Magento\CatalogRule\Api\CatalogRuleRepositoryInterface; use Magento\CatalogRule\Api\Data\RuleInterface; use Magento\CatalogRule\Api\Data\RuleInterfaceFactory; @@ -19,45 +23,50 @@ use Magento\Customer\Model\Session; use Magento\Framework\Registry; use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Xpath; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\TestCase; /** - * Assertions related to check product price rendering with combination of different price types. + * Base class for combination of different price types tests. * - * @magentoDbIsolation disabled - * @magentoAppArea frontend * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CombinationWithDifferentTypePricesTest extends TestCase +abstract class CombinationAbstract extends TestCase { /** * @var ObjectManager */ - private $objectManager; + protected $objectManager; /** * @var Page */ - private $page; + protected $page; /** - * @var Registry + * @var IndexBuilder */ - private $registry; + protected $indexBuilder; /** - * @var IndexBuilder + * @var Session */ - private $indexBuilder; + protected $customerSession; /** - * @var Session + * @var StoreManagerInterface + */ + protected $storeManager; + + /** + * @var Registry */ - private $customerSession; + private $registry; /** * @var WebsiteRepositoryInterface @@ -89,6 +98,11 @@ class CombinationWithDifferentTypePricesTest extends TestCase */ private $productTierPriceExtensionFactory; + /** + * @var ProductCustomOptionInterfaceFactory + */ + private $productCustomOptionFactory; + /** * @inheritdoc */ @@ -96,17 +110,19 @@ protected function setUp(): void { parent::setUp(); $this->objectManager = Bootstrap::getObjectManager(); - $this->page = $this->objectManager->create(Page::class); + $this->page = $this->objectManager->get(PageFactory::class)->create(); $this->registry = $this->objectManager->get(Registry::class); $this->indexBuilder = $this->objectManager->get(IndexBuilder::class); $this->customerSession = $this->objectManager->get(Session::class); $this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); $this->catalogRuleFactory = $this->objectManager->get(RuleInterfaceFactory::class); $this->catalogRuleRepository = $this->objectManager->get(CatalogRuleRepositoryInterface::class); $this->productTierPriceFactory = $this->objectManager->get(ProductTierPriceInterfaceFactory::class); $this->productTierPriceExtensionFactory = $this->objectManager->get(ProductTierPriceExtensionFactory::class); - $this->productRepository->cleanCache(); + $this->productCustomOptionFactory = $this->objectManager->get(ProductCustomOptionInterfaceFactory::class); } /** @@ -116,29 +132,7 @@ protected function tearDown(): void { parent::tearDown(); $this->registry->unregister('product'); - } - - /** - * Assert that product price rendered with expected special and regular prices if - * product has special price which lower than regular and tier prices. - * - * @magentoDataFixture Magento/Catalog/_files/product_special_price.php - * - * @dataProvider tierPricesForAllCustomerGroupsDataProvider - * - * @param float $specialPrice - * @param float $regularPrice - * @param array $tierPrices - * @param array|null $tierMessageConfig - * @return void - */ - public function testRenderSpecialPriceInCombinationWithTierPrice( - float $specialPrice, - float $regularPrice, - array $tierPrices, - ?array $tierMessageConfig - ): void { - $this->assertRenderedPrices($specialPrice, $regularPrice, $tierPrices, $tierMessageConfig); + $this->registry->unregister('current_product'); } /** @@ -150,79 +144,48 @@ public function tierPricesForAllCustomerGroupsDataProvider(): array { return [ 'fixed_tier_price_with_qty_1' => [ - 5.99, - 10, - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 9], + 'special_price' => 5.99, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 9]], + 'message_config' => null, ], - null ], 'fixed_tier_price_with_qty_2' => [ - 5.99, - 10, - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'value' => 5], + 'special_price' => 5.99, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'value' => 5]], + 'message_config' => ['qty' => 2, 'price' => 5.00, 'percent' => 17], ], - ['qty' => 2, 'price' => 5.00, 'percent' => 17], ], 'percent_tier_price_with_qty_2' => [ - 5.99, - 10, - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'percent_value' => 70], + 'special_price' => 5.99, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'percent_value' => 70]], + 'message_config' => ['qty' => 2, 'price' => 3.00, 'percent' => 50], ], - ['qty' => 2, 'price' => 3.00, 'percent' => 50], ], 'fixed_tier_price_with_qty_1_is_lower_than_special' => [ - 5, - 10, - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 5], + 'special_price' => 5, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 5]], + 'message_config' => null, ], - null ], 'percent_tier_price_with_qty_1_is_lower_than_special' => [ - 3, - 10, - [ - ['customer_group_id' => Group::NOT_LOGGED_IN_ID, 'qty' => 1, 'percent_value' => 70], + 'special_price' => 3, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::NOT_LOGGED_IN_ID, 'qty' => 1, 'percent_value' => 70]], + 'message_config' => null, ], - null ], ]; } - /** - * Assert that product price rendered with expected special and regular prices if - * product has special price which lower than regular and tier prices and customer is logged. - * - * @magentoDataFixture Magento/Catalog/_files/product_special_price.php - * @magentoDataFixture Magento/Customer/_files/customer.php - * - * @magentoAppIsolation enabled - * - * @dataProvider tierPricesForLoggedCustomerGroupDataProvider - * - * @param float $specialPrice - * @param float $regularPrice - * @param array $tierPrices - * @param array|null $tierMessageConfig - * @return void - */ - public function testRenderSpecialPriceInCombinationWithTierPriceForLoggedInUser( - float $specialPrice, - float $regularPrice, - array $tierPrices, - ?array $tierMessageConfig - ): void { - try { - $this->customerSession->setCustomerId(1); - $this->assertRenderedPrices($specialPrice, $regularPrice, $tierPrices, $tierMessageConfig); - } finally { - $this->customerSession->setCustomerId(null); - } - } - /** * Data provider with tier prices which are for logged customers group. * @@ -232,52 +195,24 @@ public function tierPricesForLoggedCustomerGroupDataProvider(): array { return [ 'fixed_tier_price_with_qty_1' => [ - 5.99, - 10, - [ - ['customer_group_id' => 1, 'qty' => 1, 'value' => 9], + 'special_price' => 5.99, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => 1, 'qty' => 1, 'value' => 9]], + 'message_config' => null, ], - null ], 'percent_tier_price_with_qty_1' => [ - 5.99, - 10, - [ - ['customer_group_id' => 1, 'qty' => 1, 'percent_value' => 30], + 'special_price' => 5.99, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => 1, 'qty' => 1, 'percent_value' => 30]], + 'message_config' => null, ], - null ], ]; } - /** - * Assert that product price rendered with expected special and regular prices if - * product has catalog rule price with different type of prices. - * - * @magentoDataFixture Magento/Catalog/_files/product_special_price.php - * @magentoDataFixture Magento/CatalogRule/_files/delete_catalog_rule_data.php - * - * @dataProvider catalogRulesDataProvider - * - * @param float $specialPrice - * @param float $regularPrice - * @param array $catalogRules - * @param array $tierPrices - * @param array|null $tierMessageConfig - * @return void - */ - public function testRenderCatalogRulePriceInCombinationWithDifferentPriceTypes( - float $specialPrice, - float $regularPrice, - array $catalogRules, - array $tierPrices, - ?array $tierMessageConfig - ): void { - $this->createCatalogRulesForProduct($catalogRules); - $this->indexBuilder->reindexFull(); - $this->assertRenderedPrices($specialPrice, $regularPrice, $tierPrices, $tierMessageConfig); - } - /** * Data provider with expect special and regular price, catalog rule data and tier price. * @@ -287,84 +222,99 @@ public function catalogRulesDataProvider(): array { return [ 'fixed_catalog_rule_price_more_than_special_price' => [ - 5.99, - 10, - [ + 'special_price' => 5.99, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 2], ], - [], - null + 'tier_data' => ['prices' => [], 'message_config' => null], ], 'fixed_catalog_rule_price_lower_than_special_price' => [ - 2, - 10, - [ + 'special_price' => 2, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 8], ], - [], - null + 'tier_data' => ['prices' => [], 'message_config' => null], ], 'fixed_catalog_rule_price_more_than_tier_price' => [ - 4, - 10, - [ + 'special_price' => 4, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 6], ], - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'percent_value' => 70], + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'percent_value' => 70]], + 'message_config' => ['qty' => 2, 'price' => 3.00, 'percent' => 25], ], - ['qty' => 2, 'price' => 3.00, 'percent' => 25], ], 'fixed_catalog_rule_price_lower_than_tier_price' => [ - 2, - 10, - [ + 'special_price' => 2, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 7], ], - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 2], + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 2]], + 'message_config' => null, ], - null ], 'adjust_percent_catalog_rule_price_lower_than_special_price' => [ - 4.50, - 10, - [ + 'special_price' => 4.50, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 45, RuleInterface::SIMPLE_ACTION => 'to_percent'], ], - [], - null + 'tier_data' => ['prices' => [], 'message_config' => null], ], 'adjust_percent_catalog_rule_price_lower_than_tier_price' => [ - 3, - 10, - [ + 'special_price' => 3, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 30, RuleInterface::SIMPLE_ACTION => 'to_percent'], ], - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 3.50], + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 3.50]], + 'message_config' => null, ], - null ], 'percent_catalog_rule_price_lower_than_special_price' => [ - 2, - 10, - [ + 'special_price' => 2, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 2, RuleInterface::SIMPLE_ACTION => 'to_fixed'], ], - [], - null + 'tier_data' => ['prices' => [], 'message_config' => null], ], 'percent_catalog_rule_price_lower_than_tier_price' => [ - 1, - 10, - [ + 'special_price' => 1, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 1, RuleInterface::SIMPLE_ACTION => 'to_fixed'], ], - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 3], + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 3]], + 'message_config' => null, ], - null + ], + ]; + } + + /** + * Data provider with percent customizable option prices. + * + * @return array + */ + public function percentCustomOptionsDataProvider(): array + { + return [ + 'percent_option_for_product_without_special_price' => [ + 'option_price' => 5, + 'product_prices' => ['special_price' => null], + ], + 'percent_option_for_product_with_special_price' => [ + 'option_price' => 3, + 'product_prices' => ['special_price' => 5.99], ], ]; } @@ -377,7 +327,7 @@ public function catalogRulesDataProvider(): array * @param float $regularPrice * @return void */ - private function checkPrices(string $priceHtml, float $specialPrice, float $regularPrice): void + protected function checkPrices(string $priceHtml, float $specialPrice, float $regularPrice): void { $this->assertEquals( 1, @@ -403,7 +353,7 @@ private function checkPrices(string $priceHtml, float $specialPrice, float $regu * @param array $tierMessageConfig * @return void */ - private function checkTierPriceMessage(string $priceHtml, array $tierMessageConfig): void + protected function checkTierPriceMessage(string $priceHtml, array $tierMessageConfig): void { $this->assertEquals( 1, @@ -418,38 +368,40 @@ private function checkTierPriceMessage(string $priceHtml, array $tierMessageConf * @param ProductInterface $product * @return string */ - private function getPriceHtml(ProductInterface $product): string + protected function getPriceHtml(ProductInterface $product): string { - $this->registerProduct($product); - $this->page->addHandle([ - 'default', - 'catalog_product_view', - ]); - $this->page->getLayout()->generateXml(); - $priceHtml = ''; - $availableChildNames = [ - 'product.info.price', - 'product.price.tier' - ]; - foreach ($this->page->getLayout()->getChildNames('product.info.main') as $childName) { - if (in_array($childName, $availableChildNames, true)) { - $priceHtml .= $this->page->getLayout()->renderElement($childName, false); - } - } + $this->preparePageLayout($product); + $priceHtml = $this->page->getLayout()->renderElement('product.info.price', false); + $priceHtml .= $this->page->getLayout()->renderElement('product.price.tier', false); return $priceHtml; } + /** + * Render custom options price render template with product. + * + * @param ProductInterface $product + * @return string + */ + protected function getCustomOptionsPriceHtml(ProductInterface $product): string + { + $this->preparePageLayout($product); + + return $this->page->getLayout()->renderElement('product.info.options', false); + } + /** * Add product to the registry. * * @param ProductInterface $product * @return void */ - private function registerProduct(ProductInterface $product): void + protected function registerProduct(ProductInterface $product): void { $this->registry->unregister('product'); $this->registry->register('product', $product); + $this->registry->unregister('current_product'); + $this->registry->register('current_product', $product); } /** @@ -457,10 +409,14 @@ private function registerProduct(ProductInterface $product): void * * @param ProductInterface $product * @param array $tierPrices + * @param int $websiteId * @return ProductInterface */ - private function createTierPricesForProduct(ProductInterface $product, array $tierPrices): ProductInterface - { + protected function createTierPricesForProduct( + ProductInterface $product, + array $tierPrices, + int $websiteId + ): ProductInterface { if (empty($tierPrices)) { return $product; } @@ -468,7 +424,7 @@ private function createTierPricesForProduct(ProductInterface $product, array $ti $createdTierPrices = []; foreach ($tierPrices as $tierPrice) { $tierPriceExtensionAttribute = $this->productTierPriceExtensionFactory->create(); - $tierPriceExtensionAttribute->setWebsiteId(0); + $tierPriceExtensionAttribute->setWebsiteId($websiteId); if (isset($tierPrice['percent_value'])) { $tierPriceExtensionAttribute->setPercentageValue($tierPrice['percent_value']); @@ -487,10 +443,35 @@ private function createTierPricesForProduct(ProductInterface $product, array $ti } /** + * Add custom option to product with data. + * + * @param ProductInterface $product + * @return void + */ + protected function addOptionToProduct(ProductInterface $product): void + { + $optionData = [ + Option::KEY_PRODUCT_SKU => $product->getSku(), + Option::KEY_TITLE => 'Test option field title', + 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_SKU => 'test-option-field-title', + ]; + $option = $this->productCustomOptionFactory->create(['data' => $optionData]); + $option->setProductSku($product->getSku()); + $product->setOptions([$option]); + $product->setHasOptions(true); + } + + /** + * Returns xpath for special price. + * * @param float $specialPrice * @return string */ - private function getSpecialPriceXpath(float $specialPrice): string + protected function getSpecialPriceXpath(float $specialPrice): string { $pathsForSearch = [ "//div[contains(@class, 'price-box') and contains(@class, 'price-final_price')]", @@ -502,10 +483,12 @@ private function getSpecialPriceXpath(float $specialPrice): string } /** + * Returns xpath for regular price. + * * @param float $regularPrice * @return string */ - private function getRegularPriceXpath(float $regularPrice): string + protected function getRegularPriceXpath(float $regularPrice): string { $pathsForSearch = [ "//div[contains(@class, 'price-box') and contains(@class, 'price-final_price')]", @@ -518,28 +501,29 @@ private function getRegularPriceXpath(float $regularPrice): string } /** + * Returns xpath for regular price label. + * * @return string */ - private function getRegularPriceLabelXpath(): string + protected function getRegularPriceLabelXpath(): string { $pathsForSearch = [ "//div[contains(@class, 'price-box') and contains(@class, 'price-final_price')]", "//span[contains(@class, 'old-price')]", "//span[contains(@class, 'price-container')]", - "//span[text()='Regular Price']", + sprintf("//span[normalize-space(text())='%s']", __('Regular Price')), ]; return implode('', $pathsForSearch); } /** - * Return tier price message xpath. Message must contain expected quantity, - * price and discount percent. + * Return tier price message xpath. Message must contain expected quantity, price and discount percent. * * @param array $expectedMessage * @return string */ - private function getTierPriceMessageXpath(array $expectedMessage): string + protected function getTierPriceMessageXpath(array $expectedMessage): string { [$qty, $price, $percent] = array_values($expectedMessage); $liPaths = [ @@ -557,36 +541,60 @@ private function getTierPriceMessageXpath(array $expectedMessage): string /** * Process test with combination of special and tier price. * + * @param string $sku * @param float $specialPrice * @param float $regularPrice - * @param array $tierPrices - * @param array|null $tierMessageConfig + * @param array $tierData + * @param int $websiteId * @return void */ - private function assertRenderedPrices( + public function assertRenderedPrices( + string $sku, float $specialPrice, float $regularPrice, - array $tierPrices, - ?array $tierMessageConfig + array $tierData, + int $websiteId = 0 ): void { - $product = $this->productRepository->get('simple', false, null, true); - $product = $this->createTierPricesForProduct($product, $tierPrices); + $product = $this->getProduct($sku); + $product = $this->createTierPricesForProduct($product, $tierData['prices'], $websiteId); $priceHtml = $this->getPriceHtml($product); $this->checkPrices($priceHtml, $specialPrice, $regularPrice); - if (null !== $tierMessageConfig) { - $this->checkTierPriceMessage($priceHtml, $tierMessageConfig); + if (null !== $tierData['message_config']) { + $this->checkTierPriceMessage($priceHtml, $tierData['message_config']); } } + /** + * Process test with combination of special and custom option price. + * + * @param string $sku + * @param float $optionPrice + * @param array $productPrices + * @return void + */ + public function assertRenderedCustomOptionPrices( + string $sku, + float $optionPrice, + array $productPrices + ): void { + $product = $this->getProduct($sku); + $product->addData($productPrices); + $this->addOptionToProduct($product); + $this->productRepository->save($product); + $priceHtml = $this->getCustomOptionsPriceHtml($this->getProduct($sku)); + $this->assertStringContainsString(sprintf('data-price-amount="%s"', $optionPrice), $priceHtml); + } + /** * Create provided catalog rules. * * @param array $catalogRules + * @param string $websiteCode * @return void */ - private function createCatalogRulesForProduct(array $catalogRules): void + protected function createCatalogRulesForProduct(array $catalogRules, string $websiteCode): void { - $baseWebsite = $this->websiteRepository->get('base'); + $baseWebsite = $this->websiteRepository->get($websiteCode); $staticRuleData = [ RuleInterface::IS_ACTIVE => 1, RuleInterface::NAME => 'Test rule name.', @@ -605,4 +613,36 @@ private function createCatalogRulesForProduct(array $catalogRules): void $this->catalogRuleRepository->save($catalogRule); } } + + /** + * Loads product by sku. + * + * @param string $sku + * @return ProductInterface + */ + protected function getProduct(string $sku): ProductInterface + { + return $this->productRepository->get( + $sku, + false, + null, + true + ); + } + + /** + * Prepares product page layout. + * + * @param ProductInterface $product + * @return void + */ + private function preparePageLayout(ProductInterface $product): void + { + $this->registerProduct($product); + $this->page->addHandle([ + 'default', + 'catalog_product_view', + ]); + $this->page->getLayout()->generateXml(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationTest.php new file mode 100644 index 0000000000000..f30e0492fc23e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Pricing\Render\PriceTypes; + +/** + * Assertions related to check product price rendering with combination of different price types. + * + * @magentoDbIsolation disabled + * @magentoAppArea frontend + * @magentoAppIsolation enabled + */ +class CombinationTest extends CombinationAbstract +{ + /** + * Assert that product price rendered with expected special and regular prices if + * product has special price which lower than regular and tier prices. + * + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * + * @dataProvider tierPricesForAllCustomerGroupsDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $tierData + * @return void + */ + public function testRenderSpecialPriceInCombinationWithTierPrice( + float $specialPrice, + float $regularPrice, + array $tierData + ): void { + $this->assertRenderedPrices('simple', $specialPrice, $regularPrice, $tierData); + } + + /** + * Assert that product price rendered with expected special and regular prices if + * product has special price which lower than regular and tier prices and customer is logged. + * + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @magentoAppIsolation enabled + * + * @dataProvider tierPricesForLoggedCustomerGroupDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $tierData + * @return void + */ + public function testRenderSpecialPriceInCombinationWithTierPriceForLoggedInUser( + float $specialPrice, + float $regularPrice, + array $tierData + ): void { + try { + $this->customerSession->setCustomerId(1); + $this->assertRenderedPrices('simple', $specialPrice, $regularPrice, $tierData); + } finally { + $this->customerSession->setCustomerId(null); + } + } + + /** + * Assert that product price rendered with expected special and regular prices if + * product has catalog rule price with different type of prices. + * + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * @magentoDataFixture Magento/CatalogRule/_files/delete_catalog_rule_data.php + * + * @dataProvider catalogRulesDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $catalogRules + * @param array $tierData + * @return void + */ + public function testRenderCatalogRulePriceInCombinationWithDifferentPriceTypes( + float $specialPrice, + float $regularPrice, + array $catalogRules, + array $tierData + ): void { + $this->createCatalogRulesForProduct($catalogRules, 'base'); + $this->indexBuilder->reindexFull(); + $this->assertRenderedPrices('simple', $specialPrice, $regularPrice, $tierData); + } + + /** + * Assert that product price rendered with expected custom option price if product has special price. + * + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * + * @dataProvider percentCustomOptionsDataProvider + * + * @param float $optionPrice + * @param array $productPrices + * @return void + */ + public function testRenderSpecialPriceInCombinationWithCustomOptionPrice( + float $optionPrice, + array $productPrices + ): void { + $this->assertRenderedCustomOptionPrices('simple', $optionPrice, $productPrices); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/MultiWebsiteCombinationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/MultiWebsiteCombinationTest.php new file mode 100644 index 0000000000000..852acf63a3f64 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/MultiWebsiteCombinationTest.php @@ -0,0 +1,188 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Pricing\Render\PriceTypes; + +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Store\ExecuteInStoreContext; + +/** + * Assertions related to check product price rendering with combination of different price types on second website. + * + * @magentoDbIsolation disabled + * @magentoAppArea frontend + */ +class MultiWebsiteCombinationTest extends CombinationAbstract +{ + /** + * @var ExecuteInStoreContext + */ + private $executeInStoreContext; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + } + + /** + * Assert that product price rendered with expected special and regular prices if + * product has special price which lower than regular and tier prices on second website. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_price_on_second_website.php + * @dataProvider tierPricesForAllCustomerGroupsDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $tierData + * @return void + */ + public function testRenderSpecialPriceInCombinationWithTierPrice( + float $specialPrice, + float $regularPrice, + array $tierData + ): void { + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertRenderedPrices'], + 'second-website-price-product', + $specialPrice, + $regularPrice, + $tierData, + (int)$this->storeManager->getStore('fixture_second_store')->getWebsiteId() + ); + $this->assertRenderedPricesOnDefaultStore('second-website-price-product'); + } + + /** + * Assert that product price rendered with expected special and regular prices on second website if + * product has special price which lower than regular and tier prices and customer is logged. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_price_on_second_website.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @magentoAppIsolation enabled + * + * @dataProvider tierPricesForLoggedCustomerGroupDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $tierData + * @return void + */ + public function testRenderSpecialPriceInCombinationWithTierPriceForLoggedInUser( + float $specialPrice, + float $regularPrice, + array $tierData + ): void { + try { + $this->customerSession->setCustomerId(1); + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertRenderedPrices'], + 'second-website-price-product', + $specialPrice, + $regularPrice, + $tierData, + (int)$this->storeManager->getStore('fixture_second_store')->getWebsiteId() + ); + $this->assertRenderedPricesOnDefaultStore('second-website-price-product'); + } finally { + $this->customerSession->setCustomerId(null); + } + } + + /** + * Assert that product price rendered with expected special and regular prices if + * product has catalog rule price with different type of prices on second website. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_price_on_second_website.php + * @magentoDataFixture Magento/CatalogRule/_files/delete_catalog_rule_data.php + * + * @dataProvider catalogRulesDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $catalogRules + * @param array $tierData + * @return void + */ + public function testRenderCatalogRulePriceInCombinationWithDifferentPriceTypes( + float $specialPrice, + float $regularPrice, + array $catalogRules, + array $tierData + ): void { + $this->createCatalogRulesForProduct($catalogRules, 'test'); + $this->indexBuilder->reindexFull(); + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertRenderedPrices'], + 'second-website-price-product', + $specialPrice, + $regularPrice, + $tierData, + (int)$this->storeManager->getStore('fixture_second_store')->getWebsiteId() + ); + $this->assertRenderedPricesOnDefaultStore('second-website-price-product'); + } + + /** + * Assert that product price rendered with expected custom option price if product has special price. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_price_on_second_website.php + * + * @dataProvider percentCustomOptionsDataProvider + * + * @param float $optionPrice + * @param array $productPrices + * @return void + */ + public function testRenderSpecialPriceInCombinationWithCustomOptionPrice( + float $optionPrice, + array $productPrices + ): void { + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertRenderedCustomOptionPrices'], + 'second-website-price-product', + $optionPrice, + $productPrices + ); + $this->assertRenderedCustomOptionPricesOnDefaultStore('second-website-price-product'); + } + + /** + * Checks price data for product on default store. + * + * @param string $sku + * @return void + */ + private function assertRenderedPricesOnDefaultStore(string $sku): void + { + //Reset layout page to get new block html + $this->page = $this->objectManager->get(PageFactory::class)->create(); + $defaultStoreTierData = ['prices' => [], 'message_config' => null]; + $this->assertRenderedPrices($sku, 15, 20, $defaultStoreTierData); + } + + /** + * Checks custom option price data for product on default store. + * + * @param string $sku + * @return void + */ + private function assertRenderedCustomOptionPricesOnDefaultStore(string $sku): void + { + //Reset layout page to get new block html + $this->page = $this->objectManager->get(PageFactory::class)->create(); + $this->assertRenderedCustomOptionPrices($sku, 7.5, []); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php index a95a981cb8006..1115a48c79ef4 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php @@ -220,11 +220,13 @@ protected function getOptionValueByLabel(string $attributeCode, string $label): /** * Returns product for testing. * + * @param bool $forceReload * @return ProductInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException */ - protected function getProduct(): ProductInterface + protected function getProduct($forceReload = false): ProductInterface { - return $this->productRepository->get('simple', false, Store::DEFAULT_STORE_ID); + return $this->productRepository->get('simple', false, Store::DEFAULT_STORE_ID, $forceReload); } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php index fbf752cc9e239..b5005ba9fc76a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php @@ -33,7 +33,8 @@ public function testModifyMeta(): void public function testModifyData(): void { $expectedData = include __DIR__ . '/../_files/eav_expected_data_output.php'; - $this->callModifyDataAndAssert($this->getProduct(), $expectedData); + // force load: ProductRepositoryInterface::getList does not add stock item, prices, categories to product + $this->callModifyDataAndAssert($this->getProduct(true), $expectedData); } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php index 1c709ffcacec7..72d96334e0335 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -9,6 +9,7 @@ use Magento\Eav\Api\AttributeSetRepositoryInterface; use Magento\Eav\Model\AttributeSetRepository; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\TestFramework\Eav\Model\GetAttributeGroupByName; use Magento\TestFramework\Eav\Model\ResourceModel\GetEntityIdByAttributeId; @@ -34,6 +35,9 @@ class EavTest extends AbstractEavTest */ private $setRepository; + /** @var ScopeConfigInterface */ + private $config; + /** * @inheritdoc */ @@ -43,6 +47,7 @@ protected function setUp(): void $this->attributeGroupByName = $this->objectManager->get(GetAttributeGroupByName::class); $this->getEntityIdByAttributeId = $this->objectManager->get(GetEntityIdByAttributeId::class); $this->setRepository = $this->objectManager->get(AttributeSetRepositoryInterface::class); + $this->config = $this->objectManager->get(ScopeConfigInterface::class); } /** @@ -217,4 +222,92 @@ private function prepareAttributeSet(array $additional): void $set->organizeData(array_merge($data, $additional)); $this->setRepository->save($set); } + + /** + * @magentoDataFixture Magento/Catalog/_files/attribute_page_layout_default.php + * @dataProvider testModifyMetaNewProductPageLayoutDefaultProvider + * @return void + */ + public function testModifyMetaNewProductPageLayoutDefault($attributesMeta): void + { + $defaultLayout = $this->config->getValue('web/default_layouts/default_product_layout'); + if ($defaultLayout) { + $attributesMeta = array_merge($attributesMeta, ['default' => $defaultLayout]); + } + $expectedMeta = $this->addMetaNesting( + $attributesMeta, + 'design', + 'page_layout' + ); + $this->callModifyMetaAndAssert($this->getNewProduct(), $expectedMeta); + } + + /** + * @return array + */ + public function testModifyMetaNewProductPageLayoutDefaultProvider(): array + { + return [ + 'attributes_meta' => [ + [ + 'dataType' => 'select', + 'formElement' => 'select', + 'visible' => '1', + 'required' => false, + 'label' => 'Layout', + 'code' => 'page_layout', + 'source' => 'design', + 'scopeLabel' => '[STORE VIEW]', + 'globalScope' => false, + 'sortOrder' => '__placeholder__', + 'options' => + [ + 0 => + [ + 'value' => '', + 'label' => 'No layout updates', + '__disableTmpl' => true, + ], + 1 => + [ + 'label' => 'Empty', + 'value' => 'empty', + '__disableTmpl' => true, + ], + 2 => + [ + 'label' => '1 column', + 'value' => '1column', + '__disableTmpl' => true, + ], + 3 => + [ + 'label' => '2 columns with left bar', + 'value' => '2columns-left', + '__disableTmpl' => true, + ], + 4 => + [ + 'label' => '2 columns with right bar', + 'value' => '2columns-right', + '__disableTmpl' => true, + ], + 5 => + [ + 'label' => '3 columns', + 'value' => '3columns', + '__disableTmpl' => true, + ], + ], + 'componentType' => 'field', + 'disabled' => true, + 'validation' => + [ + 'required' => false, + ], + 'serviceDisabled' => true, + ] + ] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php index 38fcc4554d391..6645c1fe7751f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php @@ -58,6 +58,12 @@ class LayoutUpdateTest extends TestCase */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => + \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class + ] + ]); $this->locator = $this->getMockForAbstractClass(LocatorInterface::class); $store = Bootstrap::getObjectManager()->create(StoreInterface::class); $this->locator->method('getStore')->willReturn($store); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default.php new file mode 100644 index 0000000000000..c8222ac565dc7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; +use Magento\Catalog\Setup\CategorySetup; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +$attribute = $objectManager->create(AttributeFactory::class)->create(); +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); +$attribute->loadByCode($entityType, 'page_layout'); +$attribute->setData('default_value', '1column'); +$attributeRepository->save($attribute); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default_rollback.php new file mode 100644 index 0000000000000..f762574a2efd1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default_rollback.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; +use Magento\Catalog\Setup\CategorySetup; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +$attribute = $objectManager->create(AttributeFactory::class)->create(); +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); +$attribute->loadByCode($entityType, 'page_layout'); +$attribute->setData('default_value', null); +$attributeRepository->save($attribute); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_anchor.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_anchor.php new file mode 100644 index 0000000000000..34996dd75015f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_anchor.php @@ -0,0 +1,95 @@ +<?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\Api\Data\CategoryInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Api\CategoryLinkManagementInterface; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var StoreInterface $defaultWebsite */ +$defaultStoreView = $objectManager->get(StoreManagerInterface::class)->getDefaultStoreView(); + +/** @var CategoryInterface $categoryAnchor */ +$categoryAnchor = $objectManager->create(CategoryInterface::class); +$categoryAnchor->isObjectNew(true); +$categoryAnchor + ->setId(22) + ->setIsAnchor(true) + ->setStoreId($defaultStoreView->getId()) + ->setName('Category_Anchor') + ->setParentId(2) + ->setPath('1/2/22') + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true); +$categoryAnchor->save(); + +/** @var CategoryInterface $categoryDefault */ +$categoryDefault = $objectManager->create(CategoryInterface::class); +$categoryDefault->isObjectNew(true); +$categoryDefault + ->setId(11) + ->setIsAnchor(false) + ->setStoreId($defaultStoreView->getId()) + ->setName('Category_Default') + ->setParentId(22) + ->setPath('1/2/22/11') + ->setLevel(3) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true); +$categoryDefault->save(); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productTemplate = [ + 'type' => 'simple', + 'sku' => 'product_anchor_', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_BOTH, + 'price' => 1, + 'attribute_set' => 4, + 'website_ids' => [1], + 'category_ids' => [1], +]; + +/** @var CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = $objectManager->create(CategoryLinkManagementInterface::class); + +$products = [ + ['name' => 'Product1', 'categories'=> [11]], + ['name' => 'Product2', 'categories' => [22]], +]; + +foreach ($products as $product) { + $sku = $productTemplate['sku'] . $product['name']; + + /** @var Product $product */ + $newProduct = $objectManager->create(Product::class); + $newProduct + ->setTypeId($productTemplate['type']) + ->setAttributeSetId($productTemplate['attribute_set']) + ->setWebsiteIds($productTemplate['website_ids']) + ->setName($product['name']) + ->setSku($sku) + ->setUrlKey(microtime(false)) + ->setPrice($productTemplate['website_ids']) + ->setVisibility($productTemplate['visibility']) + ->setStatus($productTemplate['status']) + ->setStockData(['use_config_manage_stock' => 0]); + $productRepository->save($newProduct); + + $categoryLinkManagement->assignProductToCategories($sku, $product['categories']); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_anchor_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_anchor_rollback.php new file mode 100644 index 0000000000000..a966931de0a36 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_anchor_rollback.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductSearchResultsInterface; +use Magento\Catalog\Model\CategoryRepository; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var CategoryRepository $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepository::class); + +$categoryIds = [11, 22]; +foreach ($categoryIds as $categoryId) { + /** @var CategoryInterface $category */ + $category = $categoryRepository->get($categoryId); + if ($category->getId()) { + $categoryRepository->delete($category); + } +} + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, 'product_anchor_%', 'like'); + +/** @var ProductSearchResultsInterface $products */ +$products = $productRepository->getList($searchCriteriaBuilder->create()); +/** @var ProductInterface $product */ +foreach ($products->getItems() as $product) { + $productRepository->delete($product); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php index 29b4a05c4dcbe..6b85b27929c2c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php @@ -4,9 +4,15 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ -$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); +/** @var Attribute $attribute */ + +use Magento\Catalog\Model\Category\AttributeFactory; +use Magento\Catalog\Model\Category\Attribute; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var AttributeFactory $attributeFactory */ +$attributeFactory = Bootstrap::getObjectManager()->get(AttributeFactory::class); +$attribute = $attributeFactory->create(); $attribute->setAttributeCode('test_attribute_code_666') ->setEntityTypeId(3) ->setIsGlobal(1) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute_rollback.php index 34114703de344..2cae71c35b916 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute_rollback.php @@ -4,15 +4,22 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +/** @var Registry $registry */ + +use Magento\Catalog\Model\Category\AttributeFactory; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ -$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); +/** @var AttributeFactory $attributeFactory */ +$attributeFactory = $objectManager->get(AttributeFactory::class); +$attribute = $attributeFactory->create(); $attribute->loadByCode(3, 'test_attribute_code_666'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_enabled_for_store.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_enabled_for_store.php new file mode 100644 index 0000000000000..bf7fa97b8c284 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_enabled_for_store.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Model\Category; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +$defaultWebsite = $objectManager->get(StoreManagerInterface::class)->getWebsite(); +$groupId = $defaultWebsite->getDefaultGroupId(); + +// creating english store +/** @var Store $storeEnglish */ +$storeEnglish = $objectManager->create(Store::class); +$storeEnglish->setCode('english') + ->setWebsiteId($defaultWebsite->getId()) + ->setGroupId($groupId) + ->setName('Fixture For English Store') + ->setSortOrder(1) + ->setIsActive(1); +$storeEnglish->save(); + +// creating ukrainian store +/** @var Store $storeUkrainian */ +$storeUkrainian = $objectManager->create(Store::class); +$storeUkrainian->setCode('ukrainian') + ->setWebsiteId($defaultWebsite->getId()) + ->setGroupId($groupId) + ->setName('Fixture For Ukrainian Store') + ->setSortOrder(1) + ->setIsActive(1); +$storeUkrainian->save(); + +/** @var Category $categoryEnglish */ +$categoryEnglish = $objectManager->create(Category::class); +$categoryEnglish->isObjectNew(true); +$categoryEnglish + ->setId(33) + ->setStoreId($storeEnglish->getId()) + ->setName('Category_US') + ->setParentId(2) + ->setPath('1/2/33') + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(false) + ->setPosition(1) + ->setAvailableSortBy(['position']); +$categoryEnglish->save(); + +/** @var Category $categoryUkrainian */ +$categoryUkrainian = $objectManager->create(Category::class); +$categoryUkrainian->isObjectNew(true); +$categoryUkrainian + ->setId(44) + ->setStoreId($storeUkrainian->getId()) + ->setName('Category_UA') + ->setParentId(2) + ->setPath('1/2/44') + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->setAvailableSortBy(['position']); +$categoryUkrainian->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_enabled_for_store_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_enabled_for_store_rollback.php new file mode 100644 index 0000000000000..11c6f43751820 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_enabled_for_store_rollback.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Model\CategoryRepository; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\Registry; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var StoreRepositoryInterface $storeRepository */ +$storeRepository = $objectManager->get(StoreRepositoryInterface::class); + +$storeCodes = ['english', 'ukrainian']; +foreach ($storeCodes as $storeCode) { + /** @var StoreInterface $store */ + $store = $storeRepository->get($storeCode); + if ($store->getId()) { + $store->delete(); + } +} + +/** @var CategoryRepository $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepository::class); + +$categoryIds = [33, 44]; +foreach ($categoryIds as $categoryId) { + /** @var CategoryInterface $category */ + $category = $categoryRepository->get($categoryId); + if ($category->getId()) { + $categoryRepository->delete($category); + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product_rollback.php index d4f2c803187dc..0198b82df2629 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product_rollback.php @@ -22,3 +22,5 @@ if ($category->getId()) { $category->delete(); } +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_specific_fields.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_specific_fields.php new file mode 100644 index 0000000000000..d5c9e4bc8a5a4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_specific_fields.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\Model\Category; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$defaultWebsite = $storeManager->getWebsite(); +$defaultStoreId = $storeManager->getStore()->getId(); +$groupId = $defaultWebsite->getDefaultGroupId(); + +/** @var Category $category */ +$category = $objectManager->create(Category::class); +$category->isObjectNew(true); +$category + ->setId(10) + ->setStoreId($defaultStoreId) + ->setIncludeInMenu(false) + ->setName('Category_en') + ->setDescription('Category_en Description') + ->setDisplayMode(Category::DM_MIXED) + ->setAvailableSortBy(['name', 'price']) + ->setDefaultSortBy('price') + ->setUrlKey('category-en') + ->setMetaTitle('Category_en Meta Title') + ->setMetaKeywords('Category_en Meta Keywords') + ->setMetaDescription('Category_en Meta Description') + ->setParentId(2) + ->setPath('1/2/3') + ->setLevel(2) + ->setIsActive(true) + ->setPosition(1); +$category->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_specific_fields_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_specific_fields_rollback.php new file mode 100644 index 0000000000000..2b619a9ac58fa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_specific_fields_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); + +/** @var CategoryInterface $category */ +$category = $categoryRepository->get(10); +if ($category->getId()) { + $categoryRepository->delete($category); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php index 2acb7fe99e192..c2c3782c8cd23 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php @@ -5,13 +5,14 @@ */ declare(strict_types=1); -use Magento\Eav\Api\AttributeRepositoryInterface; -use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products.php'); +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; + $eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ @@ -28,11 +29,5 @@ /** @var AttributeRepositoryInterface $attributeRepository */ $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); $attributeRepository->save($attribute); + CacheCleaner::cleanAll(); -/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ -$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); -$indexerCollection->load(); -/** @var \Magento\Indexer\Model\Indexer $indexer */ -foreach ($indexerCollection->getItems() as $indexer) { - $indexer->reindexAll(); -} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products.php index dcdbed7562fdb..559a1109cd420 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products.php @@ -13,7 +13,7 @@ ->setName('Simple Product1') ->setSku('simple1') ->setTaxClassId('none') - ->setDescription('description') + ->setDescription('description uniqueword') ->setShortDescription('short description') ->setOptionsContainer('container1') ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_IN_CART) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php index 34dccc2284445..57b918fb5e663 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php @@ -9,6 +9,7 @@ use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Eav\Model\Entity\Attribute\Source\Boolean; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); @@ -19,32 +20,36 @@ /** @var $installer CategorySetup */ $installer = $objectManager->create(CategorySetup::class); -$attribute->setData( - [ - 'attribute_code' => 'boolean_attribute', - 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, - 'is_global' => 0, - 'is_user_defined' => 1, - 'frontend_input' => 'boolean', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 0, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 0, - 'frontend_label' => ['Boolean Attribute'], - 'backend_type' => 'int', - 'source_model' => Boolean::class - ] -); +try { + $attributeRepository->get(CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, 'boolean_attribute'); +} catch (NoSuchEntityException $e) { + $attribute->setData( + [ + 'attribute_code' => 'boolean_attribute', + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'boolean', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 0, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Boolean Attribute'], + 'backend_type' => 'int', + 'source_model' => Boolean::class + ] + ); -$attributeRepository->save($attribute); + $attributeRepository->save($attribute); -/* Assign attribute to attribute set */ -$installer->addAttributeToGroup('catalog_product', 'Default', 'Attributes', $attribute->getId()); + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'Attributes', $attribute->getId()); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer.php new file mode 100644 index 0000000000000..17bf50bc76352 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer.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\Model\Product\Compare\ListCompare; +use Magento\Catalog\Model\Product\Compare\ListCompareFactory; +use Magento\Customer\Model\Session; +use Magento\Customer\Model\Visitor; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Session $session */ +$session = $objectManager->get(Session::class); + +try { + $session->loginById(1); + /** @var Visitor $visitor */ + $visitor = $objectManager->get(Visitor::class); + $visitor->setVisitorId(1); + /** @var ListCompare $compareList */ + $compareList = $objectManager->get(ListCompareFactory::class)->create(); + $compareList->addProduct(6); +} finally { + $session->logout(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer_rollback.php new file mode 100644 index 0000000000000..dc948702b7f53 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer_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_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php index 514c6563622c9..a7e4f702e5630 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php @@ -95,7 +95,7 @@ ->setPrice(10) ->setWeight(1) ->setShortDescription("Short description") - ->setTaxClassId(0) + ->setTaxClassId(2) ->setTierPrices($tierPrices) ->setDescription('Description with <b>html tag</b>') ->setExtensionAttributes($productExtensionAttributesWebsiteIds) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_disabled.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_disabled.php new file mode 100644 index 0000000000000..85209b8569645 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_disabled.php @@ -0,0 +1,209 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductExtensionInterfaceFactory; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = $objectManager->get(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +$tierPrices = []; +/** @var \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory */ +$tierPriceFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); +/** @var $tpExtensionAttributes */ +$tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); +/** @var $productExtensionAttributes */ +$productExtensionAttributesFactory = $objectManager->get(ProductExtensionInterfaceFactory::class); + +$adminWebsite = $objectManager->get(\Magento\Store\Api\WebsiteRepositoryInterface::class)->get('admin'); +$tierPriceExtensionAttributes1 = $tpExtensionAttributesFactory->create() + ->setWebsiteId($adminWebsite->getId()); +$productExtensionAttributesWebsiteIds = $productExtensionAttributesFactory->create( + ['website_ids' => $adminWebsite->getId()] +); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 2, + 'value' => 8 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 5, + 'value' => 5 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'qty' => 3, + 'value' => 5 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'qty' => 3.2, + 'value' => 6, + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPriceExtensionAttributes2 = $tpExtensionAttributesFactory->create() + ->setWebsiteId($adminWebsite->getId()) + ->setPercentageValue(50); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'qty' => 10 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes2); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setTierPrices($tierPrices) + ->setDescription('Description with <b>html tag</b>') + ->setExtensionAttributes($productExtensionAttributesWebsiteIds) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + )->setCanSaveCustomOptions(true) + ->setHasOptions(true); + +$oldOptions = [ + [ + 'previous_group' => 'text', + 'title' => 'Test Field', + 'type' => 'field', + 'is_require' => 1, + 'sort_order' => 0, + 'price' => 1, + 'price_type' => 'fixed', + 'sku' => '1-text', + 'max_characters' => 100, + ], + [ + 'previous_group' => 'date', + 'title' => 'Test Date and Time', + 'type' => 'date_time', + 'is_require' => 1, + 'sort_order' => 0, + 'price' => 2, + 'price_type' => 'fixed', + 'sku' => '2-date', + ], + [ + 'previous_group' => 'select', + 'title' => 'Test Select', + 'type' => 'drop_down', + 'is_require' => 1, + 'sort_order' => 0, + 'values' => [ + [ + 'option_type_id' => null, + 'title' => 'Option 1', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '3-1-select', + ], + [ + 'option_type_id' => null, + 'title' => 'Option 2', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '3-2-select', + ], + ] + ], + [ + 'previous_group' => 'select', + 'title' => 'Test Radio', + 'type' => 'radio', + 'is_require' => 1, + 'sort_order' => 0, + 'values' => [ + [ + 'option_type_id' => null, + 'title' => 'Option 1', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '4-1-radio', + ], + [ + 'option_type_id' => null, + 'title' => 'Option 2', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '4-2-radio', + ], + ] + ] +]; + +$options = []; + +/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = $objectManager->create(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); + +foreach ($oldOptions as $option) { + /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option */ + $option = $customOptionFactory->create(['data' => $option]); + $option->setProductSku($product->getSku()); + + $options[] = $option; +} + +$product->setOptions($options); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository->save($product); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php index 4ed783100fa98..7c8ce4c63034d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php @@ -20,6 +20,9 @@ $entityTypeId = $entityModel->setType(\Magento\Catalog\Model\Product::ENTITY)->getTypeId(); $groupId = $installer->getDefaultAttributeGroupId($entityTypeId, $attributeSetId); +/** @var \Magento\Catalog\Model\Product $product */ +$product = $productRepository->get('simple', true); + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ $attribute = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $attribute->setAttributeCode( @@ -30,6 +33,8 @@ 'text' )->setFrontendLabel( 'custom_attributes_frontend_label' +)->setAttributeSetId( + $product->getDefaultAttributeSetId() )->setAttributeGroupId( $groupId )->setIsFilterable( @@ -40,8 +45,6 @@ $attribute->getBackendTypeByInput($attribute->getFrontendInput()) )->save(); -$product = $productRepository->get('simple', true); - $product->setCustomAttribute($attribute->getAttributeCode(), 'customAttributeValue'); $productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php index d8222d0ce5c49..0dbcb998da836 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php @@ -13,6 +13,7 @@ ->setSku('simple1') ->setPrice(10) ->setDescription('Description with <b>html tag</b>') + ->setTaxClassId(2) ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setCategoryIds([2]) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website.php new file mode 100644 index 0000000000000..fd746578fcbf7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Data; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_store_group_and_store.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +$configResource->saveConfig(Data::XML_PATH_PRICE_SCOPE, Store::PRICE_SCOPE_WEBSITE, 'default', 0); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); +/** @var SwitchPriceAttributeScopeOnConfigChange $observer */ +$observer = $objectManager->get(Observer::class); +$objectManager->get(SwitchPriceAttributeScopeOnConfigChange::class)->execute($observer); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); +$defaultWebsiteId = $websiteRepository->get('base')->getId(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$secondStoreId = $storeManager->getStore('fixture_second_store')->getId(); +/** @var $product \Magento\Catalog\Model\Product */ +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$defaultWebsiteId, $websiteId]) + ->setName('Second website price product') + ->setSku('second-website-price-product') + ->setPrice(20) + ->setSpecialPrice(15) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_in_stock' => 1 + ] + ); +$productRepository->save($product); + +try { + $currentStoreCode = $storeManager->getStore()->getCode(); + $storeManager->setCurrentStore('fixture_second_store'); + $product = $productRepository->get('second-website-price-product', false, $secondStoreId, true); + $product->setPrice(10) + ->setSpecialPrice(5.99); + $productRepository->save($product); +} finally { + $storeManager->setCurrentStore($currentStoreCode); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website_rollback.php new file mode 100644 index 0000000000000..ce8d54d02a9ab --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website_rollback.php @@ -0,0 +1,40 @@ +<?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\Helper\Data; +use Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\Event\Observer; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +$configResource->deleteConfig(Data::XML_PATH_PRICE_SCOPE, 'default', 0); +$observer = $objectManager->get(Observer::class); +$objectManager->get(SwitchPriceAttributeScopeOnConfigChange::class)->execute($observer); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +try { + $productRepository->deleteById('second-website-price-product'); +} catch (NoSuchEntityException $e) { + //product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture( + 'Magento/Store/_files/second_website_with_store_group_and_store_rollback.php' +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php index 5a1dd30c6b492..8c1de09d2d8e2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php @@ -4,6 +4,10 @@ * See COPYING.txt for license details. */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Framework/Search/_files/products_rollback.php'); + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Framework\Registry $registry */ $registry = $objectManager->get(\Magento\Framework\Registry::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_list.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_list.php index 93c4fa854c7f3..aa8bd2ca9c89b 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_list.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_list.php @@ -29,6 +29,7 @@ $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Model\Product::class); $product + ->setId(153) ->setTypeId('simple') ->setAttributeSetId(4) ->setWebsiteIds([1]) @@ -49,6 +50,7 @@ $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Model\Product::class); $product + ->setId(156) ->setTypeId('simple') ->setAttributeSetId(4) ->setWebsiteIds([1]) 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/products_with_different_price.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_different_price.php new file mode 100644 index 0000000000000..834cdd49dfdc4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_different_price.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\TestFramework\Helper\Bootstrap; + +$productPrices = [0, 0.01, 5, 9.99, 10]; +$productTemplate = [ + 'type' => 'simple', + 'name' => 'Product with price ', + 'sku' => 'search_product_price_', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_BOTH, + 'attribute_set' => 4, + 'website_ids' => [1], + 'category_ids' => [1], +]; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = $objectManager->get(CategoryLinkManagementInterface::class); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +foreach ($productPrices as $price) { + + $sku = $productTemplate['sku'] . $price; + $name = $productTemplate['name'] . $price; + + /** @var $product Product */ + $product = $objectManager->create(Product::class); + $product + ->setTypeId($productTemplate['type']) + ->setAttributeSetId($productTemplate['attribute_set']) + ->setWebsiteIds($productTemplate['website_ids']) + ->setName($name) + ->setSku($sku) + ->setPrice($price) + ->setVisibility($productTemplate['visibility']) + ->setStatus($productTemplate['status']) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $productRepository->save($product); + + $categoryLinkManagement->assignProductToCategories($sku, $productTemplate['category_ids']); +} + +$indexRegistry = Bootstrap::getObjectManager()->get(IndexerRegistry::class); +$fulltextIndexer = $indexRegistry->get(Fulltext::INDEXER_ID); +$fulltextIndexer->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_different_price_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_different_price_rollback.php new file mode 100644 index 0000000000000..c852a857febbd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_different_price_rollback.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductSearchResultsInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; + +/** + * Remove products with different price + */ +$objectManager = Bootstrap::getObjectManager(); +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, 'search_product_price_%', 'like'); + +/** @var ProductSearchResultsInterface $products */ +$products = $productRepository->getList($searchCriteriaBuilder->create()); +/** @var ProductInterface $product */ +foreach ($products->getItems() as $product) { + $productRepository->delete($product); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php index 7bee46bc2078f..29812aa942ab5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php @@ -7,6 +7,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; $eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); $attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); @@ -59,6 +60,7 @@ /* Assign attribute to attribute set */ $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); + CacheCleaner::cleanAll(); } $eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php new file mode 100644 index 0000000000000..c2ebfa4389ab2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php @@ -0,0 +1,162 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + /** @var $store \Magento\Store\Model\Store */ + $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); + $store = $store->load('test', 'code'); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => [ + Store::DEFAULT_STORE_ID => 'Option Admin Store', + Store::DISTRO_STORE_ID => 'Option Default Store', + $store->getId() => 'Option Test Store' + ], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] + ); + + $attributeRepository->save($attribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +} + +$eavConfig->clear(); + +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(10) + ->setAttributeSetId(4) + ->setName('Simple Product1') + ->setSku('simple1') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_IN_CART) + ->setPrice(10) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCategoryIds([]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('5.99') + ->save(); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(11) + ->setAttributeSetId(4) + ->setName('Simple Product2') + ->setSku('simple2') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_ON_GESTURE) + ->setPrice(20) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCategoryIds([]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('15.99') + ->save(); + +$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId( + 333 +)->setCreatedAt( + '2014-06-23 09:50:07' +)->setName( + 'Category 1' +)->setParentId( + 2 +)->setPath( + '1/2/333' +)->setLevel( + 2 +)->setAvailableSortBy( + ['position', 'name'] +)->setDefaultSortBy( + 'name' +)->setIsActive( + true +)->setPosition( + 1 +)->setPostedProducts( + [10 => 10, 11 => 11] +)->save(); + +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); + +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php new file mode 100644 index 0000000000000..6793051b5787b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var \Magento\Framework\Registry $registry */ +$registry = \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); + +foreach (['simple1', 'simple2'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed + } +} + +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} + +/** @var $category \Magento\Catalog\Model\Category */ +$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +$category->load(333); +if ($category->getId()) { + $category->delete(); +} + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php index 379bf33ac4e3d..4dd088e148d75 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -5,15 +5,9 @@ */ declare(strict_types=1); -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; -use Magento\Catalog\Setup\CategorySetup; -use Magento\Eav\Model\Config; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Indexer\Model\Indexer; -use Magento\Indexer\Model\Indexer\Collection as IndexerCollection; use Magento\TestFramework\Helper\Bootstrap; use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; use Magento\TestFramework\Eav\Model\GetAttributeSetByName; @@ -24,94 +18,119 @@ /** @var GetAttributeSetByName $getAttributeSetByName */ $getAttributeSetByName = $objectManager->get(GetAttributeSetByName::class); $attributeSet = $getAttributeSetByName->execute('second_attribute_set'); -/** @var Config $eavConfig */ -$eavConfig = $objectManager->get(Config::class); -/** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager->create(AttributeRepositoryInterface::class); -/** @var CategorySetup $installer */ -$installer = $objectManager->create(CategorySetup::class); +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + $eavConfig->clear(); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +$attribute1 = $eavConfig->getAttribute('catalog_product', ' second_test_configurable'); +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + if (!$attribute->getId()) { - /** @var $attribute Attribute */ - $attribute->setData([ - 'attribute_code' => 'test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 0, - 'is_visible_in_advanced_search' => 0, - 'is_comparable' => 1, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 1, - 'frontend_label' => ['Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - 'default_value' => 'option_0' - ]); + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default_value' => 'option_0' + ] + ); + $attributeRepository->save($attribute); /* Assign attribute to attribute set */ $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); } - // create a second attribute -/** @var Attribute $secondAttribute */ -$secondAttribute = $eavConfig->getAttribute('catalog_product', ' second_test_configurable'); -if (!$secondAttribute->getId()) { - $secondAttribute->setData([ - 'attribute_code' => 'second_test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 0, - 'is_visible_in_advanced_search' => 0, - 'is_comparable' => 1, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 1, - 'frontend_label' => ['Second Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - 'default' => ['option_0'] - ]); - $attributeRepository->save($secondAttribute); +if (!$attribute1->getId()) { + + /** @var $attribute1 \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute1 = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute1->setData( + [ + 'attribute_code' => 'second_test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Second Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] + ); + + $attributeRepository->save($attribute1); /* Assign attribute to attribute set */ $installer->addAttributeToGroup( 'catalog_product', $attributeSet->getId(), $attributeSet->getDefaultGroupId(), - $secondAttribute->getId() + $attribute1->getId() ); } $eavConfig->clear(); -/** @var ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $productRepository \Magento\Catalog\Api\ProductRepositoryInterface */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); $productsWithNewAttributeSet = ['simple', '12345', 'simple-4']; foreach ($productsWithNewAttributeSet as $sku) { @@ -125,14 +144,8 @@ 'is_in_stock' => 1] ); $productRepository->save($product); - } catch (NoSuchEntityException $e) { + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { } } - -/** @var IndexerCollection $indexerCollection */ -$indexerCollection = $objectManager->get(IndexerCollection::class)->load(); -/** @var Indexer $indexer */ -foreach ($indexerCollection->getItems() as $indexer) { - $indexer->reindexAll(); -} +CacheCleaner::cleanAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php index f291127fe855d..6e1b20da18f18 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php @@ -5,44 +5,43 @@ */ declare(strict_types=1); -use Magento\Eav\Api\Data\AttributeInterface; -use Magento\Eav\Model\Config; -use Magento\Eav\Model\Entity\Attribute\Set as AttributeSet; -use Magento\Eav\Model\Entity\Type; -use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection as AttributeSetCollection; -use Magento\Framework\App\ObjectManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/Eav/_files/empty_attribute_set_rollback.php'); Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/categories_rollback.php'); -/** @var ObjectManager $objectManager */ -$objectManager = Bootstrap::getObjectManager(); -$eavConfig = $objectManager->get(Config::class); +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); $attributesToDelete = ['test_configurable', 'second_test_configurable']; /** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); +$attributeRepository = Bootstrap::getObjectManager()->get(AttributeRepositoryInterface::class); foreach ($attributesToDelete as $attributeCode) { - /** @var AttributeInterface $attribute */ + /** @var \Magento\Eav\Api\Data\AttributeInterface $attribute */ $attribute = $attributeRepository->get('catalog_product', $attributeCode); $attributeRepository->delete($attribute); } +/** @var $product \Magento\Catalog\Model\Product */ +$objectManager = Bootstrap::getObjectManager(); + +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); // remove attribute set -$entityType = $objectManager->create(Type::class)->loadByCode('catalog_product'); -/** @var AttributeSetCollection $attributeSetCollection */ + +/** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attributeSetCollection */ $attributeSetCollection = $objectManager->create( - AttributeSetCollection::class + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection::class ); $attributeSetCollection->addFilter('attribute_set_name', 'second_attribute_set'); $attributeSetCollection->addFilter('entity_type_id', $entityType->getId()); -$attributeSetCollection->setOrder('attribute_set_id'); +$attributeSetCollection->setOrder('attribute_set_id'); // descending is default value $attributeSetCollection->setPageSize(1); $attributeSetCollection->load(); -/** @var AttributeSet $attributeSet */ +/** @var \Magento\Eav\Model\Entity\Attribute\Set $attributeSet */ $attributeSet = $attributeSetCollection->fetchItem(); $attributeSet->delete(); + +CacheCleaner::cleanAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php index 81aad017d9619..42df6330d0dcf 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php @@ -4,54 +4,51 @@ * See COPYING.txt for license details. */ -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Attribute\Source\Status; -use Magento\Catalog\Model\Product\Type as ProductType; -use Magento\Catalog\Model\Product\Visibility; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; -use Magento\Catalog\Setup\CategorySetup; -use Magento\Eav\Model\Config as EavConfig; -use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection as OptionCollection; -use Magento\Indexer\Model\Indexer; -use Magento\Indexer\Model\Indexer\Collection as IndexerCollection; use Magento\TestFramework\Helper\Bootstrap; use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +/** + * Create multiselect attribute + */ Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiselect_attribute.php'); + /** Create product with options and multiselect attribute */ -$objectManager = Bootstrap::getObjectManager(); -/** @var CategorySetup $installer */ -$installer = $objectManager->create(CategorySetup::class); +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Setup\CategorySetup::class +); -/** @var OptionCollection $options */ -$options = $objectManager->create(OptionCollection::class); -$eavConfig = $objectManager->get(EavConfig::class); +/** @var $options \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ +$options = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class +); +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -/** @var $attribute EavAttribute */ +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ $attribute = $eavConfig->getAttribute('catalog_product', 'multiselect_attribute'); $eavConfig->clear(); $attribute->setIsSearchable(1) ->setIsVisibleInAdvancedSearch(1) - ->setIsFilterable(false) - ->setIsFilterableInSearch(false) - ->setIsVisibleOnFront(0); + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) + ->setIsVisibleOnFront(1); /** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager->create(AttributeRepositoryInterface::class); +$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); $attributeRepository->save($attribute); $options->setAttributeFilter($attribute->getId()); $optionIds = $options->getAllIds(); -/** @var ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); -/** @var Product $product */ -$product = $objectManager->create(Product::class); -$product->setTypeId(ProductType::TYPE_SIMPLE) +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) ->setId($optionIds[0] * 10) ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) ->setWebsiteIds([1]) @@ -59,45 +56,39 @@ ->setSku('simple_ms_1') ->setPrice(10) ->setDescription('Hello " &" Bring the water bottle when you can!') - ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setMultiselectAttribute([$optionIds[1],$optionIds[2]]) - ->setStatus(Status::STATUS_ENABLED) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); $productRepository->save($product); -$product = $objectManager->create(Product::class); -$product->setTypeId(ProductType::TYPE_SIMPLE) +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) ->setId($optionIds[1] * 10) ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) ->setWebsiteIds([1]) ->setName('With Multiselect 2 and 3') ->setSku('simple_ms_2') ->setPrice(10) - ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setMultiselectAttribute([$optionIds[2], $optionIds[3]]) - ->setStatus(Status::STATUS_ENABLED) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); $productRepository->save($product); -$product = $objectManager->create(Product::class); -$product->setTypeId(ProductType::TYPE_SIMPLE) +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) ->setId($optionIds[2] * 10) ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) ->setWebsiteIds([1]) ->setName('With Multiselect 1 and 3') ->setSku('simple_ms_2') ->setPrice(10) - ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setMultiselectAttribute([$optionIds[2], $optionIds[3]]) - ->setStatus(Status::STATUS_ENABLED) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); $productRepository->save($product); -/** @var IndexerCollection $indexerCollection */ -$indexerCollection = $objectManager->get(IndexerCollection::class); -$indexerCollection->load(); -/** @var Indexer $indexer */ -foreach ($indexerCollection->getItems() as $indexer) { - $indexer->reindexAll(); -} +CacheCleaner::cleanAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php index 5bc32e97db955..0e8d1f6f1022e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php @@ -3,29 +3,37 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -use Magento\Framework\Indexer\IndexerRegistry; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductSearchResultsInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiselect_attribute_rollback.php'); + /** * Remove all products as strategy of isolation process */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$registry = $objectManager->get('Magento\Framework\Registry'); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product */ -$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Catalog\Model\Product') - ->getCollection(); -foreach ($productCollection as $product) { - $product->delete(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, 'simple_ms_%', 'like'); + +/** @var ProductSearchResultsInterface $products */ +$products = $productRepository->getList($searchCriteriaBuilder->create()); +/** @var ProductInterface $product */ +foreach ($products->getItems() as $product) { + $productRepository->delete($product); } $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); - -\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(IndexerRegistry::class) - ->get(Magento\CatalogInventory\Model\Indexer\Stock\Processor::INDEXER_ID) - ->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php index 3056bf6cc5384..dd7081eaf508b 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php @@ -8,7 +8,6 @@ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiselect_attribute_with_source_model.php'); -Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/ValidatorFileMock.php'); /** Create product with options and multiselect attribute */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php index 658b6d8e8908a..786a8f1d90a50 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php @@ -3,26 +3,36 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductSearchResultsInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture( 'Magento/Catalog/_files/multiselect_attribute_with_source_model_rollback.php' ); + /** * Remove all products as strategy of isolation process */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$registry = $objectManager->get('Magento\Framework\Registry'); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product */ -$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Catalog\Model\Product') - ->getCollection(); +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, 'simple_mssm_%', 'like'); -foreach ($productCollection as $product) { - $product->delete(); +/** @var ProductSearchResultsInterface $products */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$products = $productRepository->getList($searchCriteriaBuilder->create()); +/** @var ProductInterface $product */ +foreach ($products->getItems() as $product) { + $productRepository->delete($product); } $registry->unregister('isSecureArea'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores.php index 6d74d85c0c819..58994e51a7a9f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores.php @@ -3,40 +3,59 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\DefaultCategory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ProductFactory; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores.php'); -$website = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Website::class); -/** @var $website \Magento\Store\Model\Website */ -$websiteId = $website->load('test', 'code')->getId(); +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsiteId = $websiteRepository->get('base')->getId(); +$secondWebsiteId = $websiteRepository->get('test')->getId(); +$defaultCategoryId = $objectManager->get(DefaultCategory::class)->getId(); +$stockData = ['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]; -/** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setAttributeSetId(4) - ->setWebsiteIds([$websiteId]) +$product = $productFactory->create(); +$attributeSetId = $product->getDefaultAttributeSetId(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([$secondWebsiteId]) ->setName('Simple Product on second website') ->setSku('simple-2') ->setPrice(10) ->setDescription('Description with <b>html tag</b>') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setCategoryIds([2]) - ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([$defaultCategoryId]) + ->setStockData($stockData); +$productRepository->save($product); -/** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setAttributeSetId(4) - ->setWebsiteIds([1]) +$secondProduct = $productFactory->create(); +$secondProduct->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([$baseWebsiteId]) ->setName('Simple Product') ->setSku('simple-1') + ->setUrlKey('simple_product_uniq_key_1') ->setPrice(10) ->setDescription('Description with <b>html tag</b>') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setCategoryIds([2]) - ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([$defaultCategoryId]) + ->setStockData($stockData); +$productRepository->save($secondProduct); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores_rollback.php index dcd849279ae39..f152438fe93f6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores_rollback.php @@ -3,30 +3,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -use Magento\TestFramework\Workaround\Override\Fixture\Resolver; - -Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores_rollback.php'); +declare(strict_types=1); -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -/** @var \Magento\Framework\Registry $registry */ -$registry = $objectManager->get(\Magento\Framework\Registry::class); +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - try { foreach (['simple-2', 'simple-1'] as $sku) { - $product = $productRepository->get($sku, false, null, true); - $productRepository->delete($product); + $productRepository->deleteById($sku); } -} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { +} catch (NoSuchEntityException $exception) { //Product already removed } $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores_rollback.php'); 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 27e60d29805ff..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 @@ -3,20 +3,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\UrlRewrite\Model\UrlRewrite; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var \Magento\Catalog\Model\Product $product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->load(6); - -if ($product->getId()) { - $product->delete(); +try { + $productRepository->deleteById('simple2'); +} catch (NoSuchEntityException $e) { + //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/simple_product_cyrillic_symbols.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols.php new file mode 100644 index 0000000000000..235aedc66f3f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsiteId = $websiteRepository->get('base')->getId(); + +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsiteId]) + ->setName('Простий продукт') + ->setSku('Продукт') + ->setDescription('Повний опис продукту') + ->setShortDescription('Короткий опис') + ->setPrice(10) + ->setTaxClassId(0) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] + ); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols_rollback.php new file mode 100644 index 0000000000000..3b33077988f35 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('Продукт', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled.php new file mode 100644 index 0000000000000..60dcfc4ea0d24 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\DefaultCategory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$defaultWebsiteId = $websiteRepository->get('base')->getId(); +/** @var DefaultCategory $defaultCategory */ +$defaultCategory = $objectManager->get(DefaultCategory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +$product = $productFactory->create(); +$productData = [ + ProductInterface::TYPE_ID => Type::TYPE_SIMPLE, + ProductInterface::ATTRIBUTE_SET_ID => $product->getDefaultAttributeSetId(), + ProductInterface::SKU => 'product_disabled', + ProductInterface::NAME => 'Product with category', + ProductInterface::PRICE => 10, + ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, + ProductInterface::STATUS => Status::STATUS_DISABLED, + 'website_ids' => [$defaultWebsiteId], + 'stock_data' => [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ], + 'category_ids' => [$defaultCategory->getId()], +]; +$product->setData($productData); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled_rollback.php new file mode 100644 index 0000000000000..afd874f1b38b1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('product_disabled'); +} catch (NoSuchEntityException $e) { + // product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty.php new file mode 100644 index 0000000000000..e8666ad9a13cd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty.php @@ -0,0 +1,44 @@ +<?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\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductInterfaceFactory ProductInterfaceFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepositoryFactory */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); + +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setStatus(Status::STATUS_ENABLED) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product min and max sale qty') + ->setSku('simple_product_min_max_sale_qty') + ->setPrice(10) + ->setWeight(1) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + 'min_sale_qty' => 5, + 'max_sale_qty' => 20, + ] + ) + ->setCanSaveCustomOptions(true) + ->setHasOptions(true); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty_rollback.php new file mode 100644 index 0000000000000..bc06240d2e9a3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('simple_product_min_max_sale_qty'); +} catch (NoSuchEntityException $e) { + //product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments.php new file mode 100644 index 0000000000000..bf425e2e57874 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments.php @@ -0,0 +1,44 @@ +<?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\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductInterfaceFactory ProductInterfaceFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepositoryFactory */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); + +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setStatus(Status::STATUS_ENABLED) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product with qty increments') + ->setSku('simple_product_with_qty_increments') + ->setPrice(10) + ->setWeight(1) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + 'enable_qty_increments' => 1, + 'qty_increments' => 3, + ] + ) + ->setCanSaveCustomOptions(true) + ->setHasOptions(true); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments_rollback.php new file mode 100644 index 0000000000000..d6cd1212daeb8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('simple_product_with_qty_increments'); +} catch (NoSuchEntityException $e) { + //product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups.php new file mode 100644 index 0000000000000..36550b1696ced --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$objectManager = Bootstrap::getObjectManager(); +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$tierPriceFactory = $objectManager->get(ProductTierPriceInterfaceFactory::class); +$tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); +$product = $productRepository->get('simple', false, null, true); +$adminWebsite = $objectManager->get(WebsiteRepositoryInterface::class)->get('admin'); +$tierPriceExtensionAttributes = $tpExtensionAttributesFactory->create()->setWebsiteId($adminWebsite->getId()); +$pricesForCustomerGroupsInput = [ + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 9.25 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 3, + 'value'=> 8.25 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 5, + 'value'=> 7.25 + ], + [ + 'customer_group_id' => 2, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 9 + ], + [ + 'customer_group_id' => 2, + 'percentage_value'=> null, + 'qty'=> 3, + 'value'=> 8 + ], + [ + 'customer_group_id' => 2, + 'percentage_value'=> null, + 'qty'=> 5, + 'value'=> 7 + ] +]; +$productTierPrices = []; +foreach ($pricesForCustomerGroupsInput as $price) { + $productTierPrices[] = $tierPriceFactory->create( + [ + 'data' => $price + ] + )->setExtensionAttributes($tierPriceExtensionAttributes); +} +$product->setTierPrices($productTierPrices); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups_rollback.php new file mode 100644 index 0000000000000..328c1e229da5c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups_rollback.php @@ -0,0 +1,10 @@ +<?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/Catalog/_files/product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product.php new file mode 100644 index 0000000000000..10a06c3b8a239 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\DefaultCategory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$defaultWebsiteId = $websiteRepository->get('base')->getId(); +/** @var DefaultCategory $defaultCategory */ +$defaultCategory = $objectManager->get(DefaultCategory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +$product = $productFactory->create(); +$productData = [ + ProductInterface::TYPE_ID => Type::TYPE_SIMPLE, + ProductInterface::ATTRIBUTE_SET_ID => $product->getDefaultAttributeSetId(), + ProductInterface::SKU => 'taxable_product', + ProductInterface::NAME => 'Taxable Product', + ProductInterface::PRICE => 10, + ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, + ProductInterface::STATUS => Status::STATUS_ENABLED, + 'website_ids' => [$defaultWebsiteId], + 'stock_data' => [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ], + 'category_ids' => [$defaultCategory->getId()], + 'tax_class_id' => 2, //Taxable Goods +]; +$product->setData($productData); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product_rollback.php new file mode 100644 index 0000000000000..9d58556fc987e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('taxable_product'); +} catch (NoSuchEntityException $e) { + // product already deleted +} + +$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/AbstractProductExportImportTestCase.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php index cfd07f57a4cd8..c2f571097f8e9 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php @@ -9,6 +9,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Export\Adapter\AbstractAdapter; use Magento\Store\Model\Store; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; /** * Abstract class for testing product export and import scenarios @@ -68,6 +70,11 @@ abstract class AbstractProductExportImportTestCase extends \PHPUnit\Framework\Te */ private $writer; + /** + * @var string + */ + private $csvFile; + /** * @inheritdoc */ @@ -87,6 +94,11 @@ protected function setUp(): void protected function tearDown(): void { $this->executeFixtures($this->fixtures, true); + + if ($this->csvFile !== null) { + $directoryWrite = $this->fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $directoryWrite->delete($this->csvFile); + } } /** @@ -104,6 +116,7 @@ protected function tearDown(): void */ public function testImportExport(array $fixtures, array $skus, array $skippedAttributes = []): void { + $this->csvFile = null; $this->fixtures = $fixtures; $this->executeFixtures($fixtures); $this->modifyData($skus); @@ -242,6 +255,7 @@ protected function executeImportDeleteTest(array $skus, string $csvFile = null): */ protected function executeFixtures(array $fixtures, bool $rollback = false) { + Resolver::getInstance()->setCurrentFixtureType(DataFixture::ANNOTATION); foreach ($fixtures as $fixture) { $fixturePath = $this->resolveFixturePath($fixture, $rollback); include $fixturePath; @@ -378,6 +392,7 @@ protected function executeImportReplaceTest( private function exportProducts(\Magento\CatalogImportExport\Model\Export\Product $exportProduct = null) { $csvfile = uniqid('importexport_') . '.csv'; + $this->csvFile = $csvfile; $exportProduct = $exportProduct ?: $this->objectManager->create( \Magento\CatalogImportExport\Model\Export\Product::class 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 4d08d71793cbb..a9699ea4a8050 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -295,6 +295,59 @@ public function testSaveStockItemQty() unset($stockItems, $stockItem); } + /** + * Test that is_in_stock set to 0 when item quantity is 0 + * + * @magentoDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoAppIsolation enabled + * + * @return void + */ + public function testSaveIsInStockByZeroQty(): void + { + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Api\ProductRepositoryInterface::class + ); + $id1 = $productRepository->get('simple1')->getId(); + $id2 = $productRepository->get('simple2')->getId(); + $id3 = $productRepository->get('simple3')->getId(); + $existingProductIds = [$id1, $id2, $id3]; + + $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/products_to_import_zero_qty.csv', + 'directory' => $directory + ] + ); + $errors = $this->_model->setParameters( + ['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, 'entity' => 'catalog_product'] + )->setSource( + $source + )->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + + $this->_model->importData(); + + /** @var $stockItmBeforeImport \Magento\CatalogInventory\Model\Stock\Item */ + foreach ($existingProductIds as $productId) { + /** @var $stockRegistry StockRegistry */ + $stockRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + StockRegistry::class + ); + + $stockItemAfterImport = $stockRegistry->getStockItem($productId, 1); + + $this->assertEquals(0, $stockItemAfterImport->getIsInStock()); + unset($stockItemAfterImport); + } + } + /** * Test if stock state properly changed after import * @@ -1762,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); @@ -1802,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); @@ -1951,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( @@ -2168,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); } @@ -2182,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); } @@ -3116,6 +3190,12 @@ public function testEmptyAttributeValueShouldBeIgnoredAfterUpdateProductByImport */ public function testCheckDoubleImportOfProducts() { + $this->importedProducts = [ + 'simple1', + 'simple2', + 'simple3', + ]; + /** @var SearchCriteria $searchCriteria */ $searchCriteria = $this->searchCriteriaBuilder->create(); @@ -3127,4 +3207,98 @@ public function testCheckDoubleImportOfProducts() $productsAfterSecondImport = $this->productRepository->getList($searchCriteria)->getItems(); $this->assertCount(3, $productsAfterSecondImport); } + + /** + * Checks that product related links added for all bunches properly after products import + */ + public function testImportProductsWithLinksInDifferentBunches() + { + $this->importedProducts = [ + 'simple1', + 'simple2', + 'simple3', + 'simple4', + 'simple5', + 'simple6', + ]; + $importExportData = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + $importExportData->expects($this->atLeastOnce()) + ->method('getBunchSize') + ->willReturn(5); + $this->_model = $this->objectManager->create( + \Magento\CatalogImportExport\Model\Import\Product::class, + ['importExportData' => $importExportData] + ); + $linksData = [ + 'related' => [ + 'simple1' => '2', + 'simple2' => '1' + ] + ]; + $pathToFile = __DIR__ . '/_files/products_to_import_with_related.csv'; + $filesystem = $this->objectManager->create(Filesystem::class); + + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + Csv::class, + [ + 'file' => $pathToFile, + 'directory' => $directory + ] + ); + $errors = $this->_model->setSource($source) + ->setParameters( + [ + 'behavior' => Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product' + ] + ) + ->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + $this->_model->importData(); + + $resource = $this->objectManager->get(ProductResource::class); + $productId = $resource->getIdBySku('simple6'); + /** @var Product $product */ + $product = $this->objectManager->create(Product::class); + $product->load($productId); + $productLinks = [ + 'related' => $product->getRelatedProducts() + ]; + $importedProductLinks = []; + foreach ($productLinks as $linkType => $linkedProducts) { + foreach ($linkedProducts as $linkedProductData) { + $importedProductLinks[$linkType][$linkedProductData->getSku()] = $linkedProductData->getPosition(); + } + } + $this->assertEquals($linksData, $importedProductLinks); + } + + /** + * Tests that image name does not have to be prefixed by slash + * + * @magentoDataFixture mediaImportImageFixture + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + */ + public function testUpdateImageByNameNotPrefixedWithSlash() + { + $expectedLabelForDefaultStoreView = 'image label updated'; + $expectedImageFile = '/m/a/magento_image.jpg'; + $secondStoreCode = 'fixturestore'; + $productSku = 'simple'; + $this->importDataForMediaTest('import_image_name_without_slash.csv'); + $product = $this->getProductBySku($productSku); + $imageItems = $product->getMediaGalleryImages()->getItems(); + $this->assertCount(1, $imageItems); + $imageItem = array_shift($imageItems); + $this->assertEquals($expectedImageFile, $imageItem->getFile()); + $this->assertEquals($expectedLabelForDefaultStoreView, $imageItem->getLabel()); + $product = $this->getProductBySku($productSku, $secondStoreCode); + $imageItems = $product->getMediaGalleryImages()->getItems(); + $this->assertCount(0, $imageItems); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv new file mode 100644 index 0000000000000..415501daf89d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv @@ -0,0 +1,3 @@ +"sku","store_view_code","base_image","base_image_label","hide_from_product_page" +"simple",,"m/a/magento_image.jpg","image label updated", +"simple","fixturestore",,,"m/a/magento_image.jpg" diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_related.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_related.csv new file mode 100644 index 0000000000000..3627cdc24ec41 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_related.csv @@ -0,0 +1,7 @@ +sku,product_type,store_view_code,name,price,qty,attribute_set_code,related_skus,related_position +simple1,simple,,simple 1,25,10,Default,, +simple2,simple,,simple 2,34,10,Default,, +simple3,simple,,simple 3,58,10,Default,"simple1,simple2","1,2" +simple4,simple,,simple 4,67,10,Default,"simple1,simple2","2,1" +simple5,simple,,simple 5,58,10,Default,"simple1,simple2","1,2" +simple6,simple,,simple 6,67,10,Default,"simple1,simple2","2,1" \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_zero_qty.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_zero_qty.csv new file mode 100644 index 0000000000000..632d60cf7daa0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_zero_qty.csv @@ -0,0 +1,4 @@ +sku,qty,is_in_stock +simple1,0,1 +simple2,0,1 +simple3,0,1 diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Block/ResultTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Block/ResultTest.php index 76a4ff9714ebd..6d679a5aea7d4 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Block/ResultTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Block/ResultTest.php @@ -12,6 +12,7 @@ use Magento\Framework\View\LayoutInterface; use Magento\Search\Model\QueryFactory; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Search\ViewModel\ConfigProvider; class ResultTest extends \PHPUnit\Framework\TestCase { @@ -25,6 +26,11 @@ class ResultTest extends \PHPUnit\Framework\TestCase */ private $layout; + /** + * @var ConfigProvider + */ + private $configProvider; + /** * @inheritdoc */ @@ -32,9 +38,15 @@ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->configProvider = $this->objectManager->get(ConfigProvider::class); } - public function testSetListOrders() + /** + * Set list orders test + * + * @return void + */ + public function testSetListOrders(): void { $this->layout->addBlock(Text::class, 'head'); // The tested block is using head block @@ -62,6 +74,7 @@ public function testEscapeSearchText(string $searchValue, string $expectedOutput $searchResultBlock = $this->layout->createBlock(Result::class); /** @var Template $searchBlock */ $searchBlock = $this->layout->createBlock(Template::class); + $searchBlock->setData(['configProvider' => $this->configProvider]); $searchBlock->setTemplate('Magento_Search::form.mini.phtml'); /** @var RequestInterface $request */ $request = $this->objectManager->get(RequestInterface::class); 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/Model/CategoryUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php index d87a7ffd48c09..f8837f8d9c5d6 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php @@ -9,18 +9,23 @@ use Magento\Catalog\Api\CategoryLinkManagementInterface; use Magento\Catalog\Api\CategoryRepositoryInterface; -use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\CategoryFactory; -use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; +use Magento\Catalog\Model\ResourceModel\CategoryFactory as CategoryResourceFactory; use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; use Magento\CatalogUrlRewrite\Model\ResourceModel\Category\Product; -use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; use Magento\UrlRewrite\Model\OptionProvider; use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use PHPUnit\Framework\TestCase; /** * Class for category url rewrites tests @@ -29,22 +34,34 @@ * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CategoryUrlRewriteTest extends AbstractUrlRewriteTest +class CategoryUrlRewriteTest extends TestCase { + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CategoryFactory */ + private $categoryFactory; + + /** @var UrlRewriteCollectionFactory */ + private $urlRewriteCollectionFactory; + /** @var CategoryRepositoryInterface */ private $categoryRepository; - /** @var CategoryResource */ - private $categoryResource; + /** @var CategoryResourceFactory */ + private $categoryResourceFactory; /** @var CategoryLinkManagementInterface */ - private $categoryLinkManagement; + private $categoryLinkManagment; - /** @var CategoryFactory */ - private $categoryFactory; + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var StoreRepositoryInterface */ + private $storeRepository; - /** @var string */ - private $suffix; + /** @var ScopeConfigInterface */ + private $config; /** * @inheritdoc @@ -53,19 +70,18 @@ protected function setUp(): void { parent::setUp(); - $this->categoryRepository = $this->objectManager->create(CategoryRepositoryInterface::class); - $this->categoryResource = $this->objectManager->get(CategoryResource::class); - $this->categoryLinkManagement = $this->objectManager->create(CategoryLinkManagementInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); $this->categoryFactory = $this->objectManager->get(CategoryFactory::class); - $this->suffix = $this->config->getValue( - CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, - ScopeInterface::SCOPE_STORE - ); + $this->urlRewriteCollectionFactory = $this->objectManager->get(UrlRewriteCollectionFactory::class); + $this->categoryRepository = $this->objectManager->create(CategoryRepositoryInterface::class); + $this->categoryResourceFactory = $this->objectManager->get(CategoryResourceFactory::class); + $this->categoryLinkManagment = $this->objectManager->create(CategoryLinkManagementInterface::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->storeRepository = $this->objectManager->create(StoreRepositoryInterface::class); + $this->config = $this->objectManager->get(ScopeConfigInterface::class); } /** - * Test url rewrite after category save - * * @magentoDataFixture Magento/Catalog/_files/category_with_position.php * @dataProvider categoryProvider * @param array $data @@ -73,18 +89,25 @@ protected function setUp(): void */ public function testUrlRewriteOnCategorySave(array $data): void { - $categoryModel = $this->saveCategory($data['data']); + $categoryModel = $this->categoryFactory->create(); + $categoryModel->isObjectNew(true); + $categoryModel->setData($data['data']); + $categoryResource = $this->categoryResourceFactory->create(); + $categoryResource->save($categoryModel); $this->assertNotNull($categoryModel->getId(), 'The category was not created'); - $urlRewriteCollection = $this->getEntityRewriteCollection($categoryModel->getId()); - $this->assertRewrites( - $urlRewriteCollection, - $this->prepareData($data['expected_data'], (int)$categoryModel->getId()) - ); + $urlRewriteCollection = $this->getCategoryRewriteCollection($categoryModel->getId()); + foreach ($urlRewriteCollection as $item) { + foreach ($data['expected_data'] as $field => $expectedItem) { + $this->assertEquals( + sprintf($expectedItem, $categoryModel->getId()), + $item[$field], + 'The expected data does not match actual value' + ); + } + } } /** - * Provider. categoryProvider - * * @return array */ public function categoryProvider(): array @@ -100,10 +123,8 @@ public function categoryProvider(): array 'is_active' => true, ], 'expected_data' => [ - [ - 'request_path' => 'test-category%suffix%', - 'target_path' => 'catalog/category/view/id/%id%', - ], + 'request_path' => 'test-category.html', + 'target_path' => 'catalog/category/view/id/%s', ], ], ], @@ -117,10 +138,8 @@ public function categoryProvider(): array 'is_active' => true, ], 'expected_data' => [ - [ - 'request_path' => 'category-1/test-sub-category%suffix%', - 'target_path' => 'catalog/category/view/id/%id%', - ], + 'request_path' => 'category-1/test-sub-category.html', + 'target_path' => 'catalog/category/view/id/%s', ], ], ], @@ -128,8 +147,6 @@ public function categoryProvider(): array } /** - * Test category product url rewrite - * * @magentoDataFixture Magento/Catalog/_files/category_tree.php * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php * @dataProvider productRewriteProvider @@ -139,16 +156,12 @@ public function categoryProvider(): array public function testCategoryProductUrlRewrite(array $data): void { $category = $this->categoryRepository->get(402); - $this->categoryLinkManagement->assignProductToCategories('simple2', [$category->getId()]); - $productRewriteCollection = $this->getCategoryProductRewriteCollection( - array_keys($category->getParentCategories()) - ); - $this->assertRewrites($productRewriteCollection, $this->prepareData($data)); + $this->categoryLinkManagment->assignProductToCategories('simple2', [$category->getId()]); + $productRewriteCollection = $this->getProductRewriteCollection(array_keys($category->getParentCategories())); + $this->assertRewrites($productRewriteCollection, $data); } /** - * Provider. productRewriteProvider - * * @return array */ public function productRewriteProvider(): array @@ -157,15 +170,15 @@ public function productRewriteProvider(): array [ [ [ - 'request_path' => 'category-1/category-1-1/category-1-1-1/simple-product2%suffix%', + 'request_path' => 'category-1/category-1-1/category-1-1-1/simple-product2.html', 'target_path' => 'catalog/product/view/id/6/category/402', ], [ - 'request_path' => 'category-1/simple-product2%suffix%', + 'request_path' => 'category-1/simple-product2.html', 'target_path' => 'catalog/product/view/id/6/category/400', ], [ - 'request_path' => 'category-1/category-1-1/simple-product2%suffix%', + 'request_path' => 'category-1/category-1-1/simple-product2.html', 'target_path' => 'catalog/product/view/id/6/category/401', ], ], @@ -174,8 +187,6 @@ public function productRewriteProvider(): array } /** - * Test url rewrites after category save with existing url key - * * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_products.php * @magentoAppIsolation enabled * @dataProvider existingUrlProvider @@ -186,12 +197,13 @@ public function testUrlRewriteOnCategorySaveWithExistingUrlKey(array $data): voi { $this->expectException(UrlAlreadyExistsException::class); $this->expectExceptionMessage((string)__('URL key for specified store already exists.')); - $this->saveCategory($data); + $category = $this->categoryFactory->create(); + $category->setData($data); + $categoryResource = $this->categoryResourceFactory->create(); + $categoryResource->save($category); } /** - * Provider. existingUrlProvider - * * @return array */ public function existingUrlProvider(): array @@ -239,8 +251,6 @@ public function existingUrlProvider(): array } /** - * Test url rewrites after category move - * * @magentoDataFixture Magento/Catalog/_files/category_product.php * @magentoDataFixture Magento/Catalog/_files/catalog_category_with_slash.php * @dataProvider categoryMoveProvider @@ -252,12 +262,10 @@ public function testUrlRewriteOnCategoryMove(array $data): void $categoryId = $data['data']['id']; $category = $this->categoryRepository->get($categoryId); $category->move($data['data']['pid'], $data['data']['aid']); - $productRewriteCollection = $this->getCategoryProductRewriteCollection( - array_keys($category->getParentCategories()) - ); - $categoryRewriteCollection = $this->getEntityRewriteCollection($categoryId); - $this->assertRewrites($categoryRewriteCollection, $this->prepareData($data['expected_data']['category'])); - $this->assertRewrites($productRewriteCollection, $this->prepareData($data['expected_data']['product'])); + $productRewriteCollection = $this->getProductRewriteCollection(array_keys($category->getParentCategories())); + $categoryRewriteCollection = $this->getCategoryRewriteCollection($categoryId); + $this->assertRewrites($categoryRewriteCollection, $data['expected_data']['category']); + $this->assertRewrites($productRewriteCollection, $data['expected_data']['product']); } /** @@ -277,21 +285,21 @@ public function categoryMoveProvider(): array 'category' => [ [ 'request_path' => 'category-1.html', - 'target_path' => 'category-with-slash-symbol/category-1%suffix%', + 'target_path' => 'category-with-slash-symbol/category-1.html', 'redirect_type' => OptionProvider::PERMANENT, ], [ - 'request_path' => 'category-with-slash-symbol/category-1%suffix%', + 'request_path' => 'category-with-slash-symbol/category-1.html', 'target_path' => 'catalog/category/view/id/333', ], ], 'product' => [ [ - 'request_path' => 'category-with-slash-symbol/simple-product-three%suffix%', + 'request_path' => 'category-with-slash-symbol/simple-product-three.html', 'target_path' => 'catalog/product/view/id/333/category/3331', ], [ - 'request_path' => 'category-with-slash-symbol/category-1/simple-product-three%suffix%', + 'request_path' => 'category-with-slash-symbol/category-1/simple-product-three.html', 'target_path' => 'catalog/product/view/id/333/category/333', ], ], @@ -302,14 +310,14 @@ public function categoryMoveProvider(): array } /** - * Test url rewrites after category delete * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoAppArea adminhtml * @return void */ public function testUrlRewritesAfterCategoryDelete(): void { $categoryId = 333; - $categoryItemIds = $this->getEntityRewriteCollection($categoryId)->getAllIds(); + $categoryItemIds = $this->getCategoryRewriteCollection($categoryId)->getAllIds(); $this->categoryRepository->deleteByIdentifier($categoryId); $this->assertEmpty( array_intersect($this->getAllRewriteIds(), $categoryItemIds), @@ -318,8 +326,6 @@ public function testUrlRewritesAfterCategoryDelete(): void } /** - * Test url rewrites after category with products delete - * * @magentoAppArea adminhtml * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_product_ids.php * @return void @@ -328,8 +334,8 @@ public function testUrlRewritesAfterCategoryWithProductsDelete(): void { $category = $this->categoryRepository->get(3); $childIds = explode(',', $category->getAllChildren()); - $productRewriteIds = $this->getCategoryProductRewriteCollection($childIds)->getAllIds(); - $categoryItemIds = $this->getEntityRewriteCollection($childIds)->getAllIds(); + $productRewriteIds = $this->getProductRewriteCollection($childIds)->getAllIds(); + $categoryItemIds = $this->getCategoryRewriteCollection($childIds)->getAllIds(); $this->categoryRepository->deleteByIdentifier($category->getId()); $allIds = $this->getAllRewriteIds(); $this->assertEmpty( @@ -343,8 +349,6 @@ public function testUrlRewritesAfterCategoryWithProductsDelete(): void } /** - * Test category url rewrite per Store Views - * * @magentoDataFixture Magento/Store/_files/second_store.php * @magentoDataFixture Magento/Catalog/_files/category.php * @return void @@ -360,12 +364,11 @@ public function testCategoryUrlRewritePerStoreViews(): void $categoryId = 333; $category = $this->categoryRepository->get($categoryId); $urlKeyFirstStore = $category->getUrlKey(); - $this->saveCategory( - ['store_id' => $secondStoreId, 'url_key' => $urlKeySecondStore], - $category - ); - $urlRewriteItems = $this->getEntityRewriteCollection($categoryId)->getItems(); - $this->assertTrue(count($urlRewriteItems) == 2); + $category->setStoreId($secondStoreId); + $category->setUrlKey($urlKeySecondStore); + $categoryResource = $this->categoryResourceFactory->create(); + $categoryResource->save($category); + $urlRewriteItems = $this->getCategoryRewriteCollection($categoryId)->getItems(); foreach ($urlRewriteItems as $item) { $item->getData('store_id') == $secondStoreId ? $this->assertEquals($urlKeySecondStore . $urlSuffix, $item->getRequestPath()) @@ -374,103 +377,74 @@ public function testCategoryUrlRewritePerStoreViews(): void } /** - * Test category url rewrite while reassign store view + * Get products url rewrites collection referred to categories * - * @magentoAppArea adminhtml - * @magentoDataFixture Magento/Store/_files/second_store_group_with_second_website.php - * @magentoDataFixture Magento/Catalog/_files/category.php - * @return void + * @param string|array $categoryId + * @return UrlRewriteCollection */ - public function testCategoryUrlRewriteMovingToOtherStoreView(): void + private function getProductRewriteCollection($categoryId): UrlRewriteCollection { - $categoryId = 333; - $store = $this->storeRepository->get('default'); - $storeId = $store->getId(); - $urlRewrites = [ - ['category-1-updated.html', 'category-1.html'], - ['category-1-most-recent.html', 'category-1-updated.html'], - ]; - foreach ($urlRewrites as $rewrite) { - /** @var \Magento\UrlRewrite\Model\UrlRewrite $urlRewrite */ - $urlRewrite = $this->objectManager->create(\Magento\UrlRewrite\Model\UrlRewrite::class); - $urlRewrite->setEntityType(\Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($categoryId) - ->setRequestPath($rewrite[0]) - ->setTargetPath($rewrite[1]) - ->setRedirectType(\Magento\UrlRewrite\Model\OptionProvider::PERMANENT) - ->setStoreId($storeId); - $urlRewrite->save(); - } - - /** @var WebsiteRepositoryInterface $websiteRepo */ - $websiteRepo = $this->objectManager->get(WebsiteRepositoryInterface::class); - $website = $websiteRepo->get('test'); - $group = $website->getDefaultGroup(); - $group->setRootCategoryId(2); - $group->save(); - $groupId = $group->getId(); - $store->setStoreGroupId($groupId); - $store->save(); + $condition = is_array($categoryId) ? ['in' => $categoryId] : $categoryId; + $productRewriteCollection = $this->urlRewriteCollectionFactory->create(); + $productRewriteCollection + ->join( + ['p' => Product::TABLE_NAME], + 'main_table.url_rewrite_id = p.url_rewrite_id', + 'category_id' + ) + ->addFieldToFilter('category_id', $condition) + ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => DataProductUrlRewriteDatabaseMap::ENTITY_TYPE]); - $urlRewriteItems = $this->getEntityRewriteCollection($categoryId)->getItems(); - $this->assertTrue(count($urlRewriteItems) === 3); - $expectedRewriteRequestPaths = ['category-1.html', 'category-1-updated.html', 'category-1-most-recent.html']; - foreach ($urlRewriteItems as $item) { - $this->assertTrue(in_array($item->getRequestPath(), $expectedRewriteRequestPaths)); - } + return $productRewriteCollection; } /** - * @inheritdoc + * Retrieve all rewrite ids + * + * @return array */ - protected function getUrlSuffix(): string + private function getAllRewriteIds(): array { - return $this->suffix; - } + $urlRewriteCollection = $this->urlRewriteCollectionFactory->create(); - /** - * @inheritdoc - */ - protected function getEntityType(): string - { - return DataCategoryUrlRewriteDatabaseMap::ENTITY_TYPE; + return $urlRewriteCollection->getAllIds(); } /** - * Save product with data using resource model directly + * Get category url rewrites collection * - * @param array $data - * @param CategoryInterface|null $category - * @return CategoryInterface + * @param string|array $categoryId + * @return UrlRewriteCollection */ - private function saveCategory(array $data, $category = null): CategoryInterface + private function getCategoryRewriteCollection($categoryId): UrlRewriteCollection { - $category = $category ?: $this->categoryFactory->create(); - $category->addData($data); - $this->categoryResource->save($category); + $condition = is_array($categoryId) ? ['in' => $categoryId] : $categoryId; + $categoryRewriteCollection = $this->urlRewriteCollectionFactory->create(); + $categoryRewriteCollection->addFieldToFilter(UrlRewrite::ENTITY_ID, $condition) + ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => DataCategoryUrlRewriteDatabaseMap::ENTITY_TYPE]); - return $category; + return $categoryRewriteCollection; } /** - * Get products url rewrites collection referred to categories + * Check that actual data contains of expected values * - * @param string|array $categoryId - * @return UrlRewriteCollection + * @param UrlRewriteCollection $collection + * @param array $expectedData + * @return void */ - private function getCategoryProductRewriteCollection($categoryId): UrlRewriteCollection + private function assertRewrites(UrlRewriteCollection $collection, array $expectedData): void { - $condition = is_array($categoryId) ? ['in' => $categoryId] : $categoryId; - $productRewriteCollection = $this->urlRewriteCollectionFactory->create(); - $productRewriteCollection - ->join( - ['p' => $this->categoryResource->getTable(Product::TABLE_NAME)], - 'main_table.url_rewrite_id = p.url_rewrite_id', - 'category_id' - ) - ->addFieldToFilter('category_id', $condition) - ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => DataProductUrlRewriteDatabaseMap::ENTITY_TYPE]); - - return $productRewriteCollection; + $collectionItems = $collection->toArray()['items']; + foreach ($collectionItems as $item) { + $found = false; + foreach ($expectedData as $expectedItem) { + $found = array_intersect_assoc($item, $expectedItem) == $expectedItem; + if ($found) { + break; + } + } + $this->assertTrue($found, 'The actual data does not contains of expected values'); + } } } 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/Block/Cart/CrosssellTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Block/Cart/CrosssellTest.php new file mode 100644 index 0000000000000..7a898b4310b3f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Block/Cart/CrosssellTest.php @@ -0,0 +1,353 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Block\Cart; + +use Magento\Catalog\Block\Product\ProductList\AbstractLinksTest; +use Magento\Catalog\ViewModel\Product\Listing\PreparePostData; +use Magento\Checkout\Model\Session; +use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Store\ExecuteInStoreContext; + +/** + * Check the correct behavior of cross-sell products in the shopping cart + * + * @see \Magento\Checkout\Block\Cart\Crosssell + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * @magentoAppArea frontend + */ +class CrosssellTest extends AbstractLinksTest +{ + private const MAX_ITEM_COUNT = 4; + + /** @var Session */ + private $checkoutSession; + + /** @var string */ + private $addToCartButtonXpath = "//div[contains(@class, 'actions-primary')]/button[@type='button']"; + + /** @var string */ + private $addToCartSubmitXpath = "//div[contains(@class, 'actions-primary')]" + . "/form[@data-product-sku='%s']/button[@type='submit']"; + + /** @var string */ + private $addToLinksXpath = "//div[contains(@class, 'actions-secondary')]"; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->block = $this->layout->createBlock(Crosssell::class); + $this->linkType = 'crosssell'; + $this->titleName = (string)__('More Choices:'); + $this->checkoutSession = $this->objectManager->get(Session::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + } + + /** + * Checks for a simple cross-sell product when block code is generated + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testSimpleCrosssellProduct(): void + { + $relatedProduct = $this->productRepository->get('simple-1'); + $this->linkProducts('simple', ['simple-1' => ['position' => 2]]); + $this->setCheckoutSessionQuote('test_order_with_simple_product_without_address'); + $this->prepareBlock(); + $html = $this->block->toHtml(); + + $this->assertNotEmpty($html); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($this->titleXpath, $this->linkType, $this->titleName), $html), + 'Expected title is incorrect or missing!' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($this->addToCartSubmitXpath, $relatedProduct->getSku()), $html), + 'Expected add to cart button is incorrect or missing!' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($this->addToLinksXpath, $html), + 'Expected add to links is incorrect or missing!' + ); + } + + /** + * Checks for a cross-sell product with required option when block code is generated + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Catalog/_files/product_virtual_with_options.php + * @return void + */ + public function testCrosssellProductWithRequiredOption(): void + { + $this->linkProducts('simple', ['virtual' => ['position' => 1]]); + $this->setCheckoutSessionQuote('test_order_with_simple_product_without_address'); + $this->prepareBlock(); + $html = $this->block->toHtml(); + + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($this->addToCartButtonXpath, $html), + 'Expected add to cart button is incorrect or missing!' + ); + } + + /** + * Test the display of cross-sell products in the block + * + * @dataProvider displayLinkedProductsProvider + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @param array $data + * @return void + */ + public function testDisplayCrosssellProducts(array $data): void + { + $this->updateProducts($data['updateProducts']); + $this->linkProducts('simple', $this->existingProducts); + $items = $this->getBlockItems('test_order_with_simple_product_without_address'); + + $this->assertEquals( + $data['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected cross-sell products do not match actual cross-sell products!' + ); + } + + /** + * Test the position and max count of cross-sell products in the block + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testPositionCrosssellProducts(): void + { + $positionData = array_merge_recursive( + $this->getPositionData(), + [ + 'productLinks' => [ + 'simple-1' => ['position' => 5], + 'simple2' => ['position' => 4], + ], + 'expectedProductLinks' => [ + 'simple2', + ], + ] + ); + $this->linkProducts('simple', $positionData['productLinks']); + $items = $this->getBlockItems('test_order_with_simple_product_without_address'); + + $this->assertCount( + self::MAX_ITEM_COUNT, + $items, + 'Expected quantity of cross-sell products do not match the actual quantity!' + ); + $this->assertEquals( + $positionData['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected cross-sell products do not match actual cross-sell products!' + ); + } + + /** + * Test the position and max count of cross-sell products in the block + * when set last added product in checkout session + * + * @dataProvider positionWithLastAddedProductProvider + * @magentoDataFixture Magento/Sales/_files/quote_with_multiple_products.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @param array $positionData + * @param array $expectedProductLinks + * @return void + */ + public function testPositionCrosssellProductsWithLastAddedProduct( + array $positionData, + array $expectedProductLinks + ): void { + foreach ($positionData as $sku => $productLinks) { + $this->linkProducts($sku, $productLinks); + } + $this->checkoutSession->setLastAddedProductId($this->productRepository->get('simple-tableRate-1')->getId()); + $items = $this->getBlockItems('tableRate'); + + $this->assertCount( + self::MAX_ITEM_COUNT, + $items, + 'Expected quantity of cross-sell products do not match the actual quantity!' + ); + $this->assertEquals( + $expectedProductLinks, + $this->getActualLinks($items), + 'Expected cross-sell products do not match actual cross-sell products!' + ); + } + + /** + * Provide test data to verify the position of linked products of the last added product. + * + * @return array + */ + public function positionWithLastAddedProductProvider(): array + { + return [ + 'less_four_linked_products_to_last_added_product' => [ + 'positionData' => [ + 'simple-tableRate-1' => [ + 'simple-249' => ['position' => 2], + 'simple-156' => ['position' => 1], + ], + 'simple-tableRate-2' => [ + 'simple-1' => ['position' => 2], + 'simple2' => ['position' => 1], + 'wrong-simple' => ['position' => 3], + ], + ], + 'expectedProductLinks' => [ + 'simple-156', + 'simple-249', + 'simple2', + 'simple-1', + ], + ], + 'four_linked_products_to_last_added_product' => [ + 'positionData' => [ + 'simple-tableRate-1' => [ + 'wrong-simple' => ['position' => 3], + 'simple-249' => ['position' => 1], + 'simple-156' => ['position' => 2], + 'simple2' => ['position' => 4], + ], + 'simple-tableRate-2' => [ + 'simple-1' => ['position' => 1], + ], + ], + 'expectedProductLinks' => [ + 'simple-249', + 'simple-156', + 'wrong-simple', + 'simple2', + ], + ], + ]; + } + + /** + * Test the display of cross-sell products in the block on different websites + * + * @dataProvider multipleWebsitesLinkedProductsProvider + * @magentoDataFixture Magento/Catalog/_files/products_with_websites_and_stores.php + * @magentoDataFixture Magento/Sales/_files/quote_with_multiple_products.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @param array $data + * @return void + */ + public function testMultipleWebsitesCrosssellProducts(array $data): void + { + $this->updateProducts($this->prepareProductsWebsiteIds()); + $productLinks = array_merge($this->existingProducts, $data['productLinks']); + $this->linkProducts('simple-tableRate-1', $productLinks); + $items = $this->executeInStoreContext->execute($data['storeCode'], [$this, 'getBlockItems'], 'tableRate'); + + $this->assertEquals( + $data['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected cross-sell products do not match actual cross-sell products!' + ); + } + + /** + * Test the invisibility of cross-sell products in the block which added to cart + * + * @magentoDataFixture Magento/Sales/_files/quote_with_multiple_products.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testInvisibilityCrosssellProductAddedToCart(): void + { + $productLinks = [ + 'simple-1' => ['position' => 1], + 'simple-tableRate-2' => ['position' => 2], + ]; + $this->linkProducts('simple-tableRate-1', $productLinks); + $items = $this->getBlockItems('tableRate'); + + $this->assertEquals( + ['simple-1'], + $this->getActualLinks($items), + 'Expected cross-sell products do not match actual cross-sell products!' + ); + } + + /** + * Get products of block when quote in checkout session + * + * @param string $reservedOrderId + * @return array + */ + public function getBlockItems(string $reservedOrderId): array + { + $this->setCheckoutSessionQuote($reservedOrderId); + + return $this->block->getItems(); + } + + /** + * @inheritdoc + */ + protected function prepareBlock(): void + { + parent::prepareBlock(); + + $this->block->setViewModel($this->objectManager->get(PreparePostData::class)); + } + + /** + * @inheritdoc + */ + protected function prepareProductsWebsiteIds(): array + { + $productsWebsiteIds = parent::prepareProductsWebsiteIds(); + $simple = $productsWebsiteIds['simple-1']; + unset($productsWebsiteIds['simple-1']); + + return array_merge($productsWebsiteIds, ['simple-tableRate-1' => $simple]); + } + + /** + * Set quoteId in checkoutSession object. + * + * @param string $reservedOrderId + * @return void + */ + private function setCheckoutSessionQuote(string $reservedOrderId): void + { + $this->checkoutSession->clearQuote(); + $quote = $this->objectManager->get(GetQuoteByReservedOrderId::class)->execute($reservedOrderId); + if ($quote !== null) { + $this->checkoutSession->setQuoteId($quote->getId()); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/AddTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/AddTest.php new file mode 100644 index 0000000000000..424fa13d74890 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/AddTest.php @@ -0,0 +1,231 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Controller\Cart; + +use Laminas\Stdlib\Parameters; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\SessionFactory as CheckoutSessionFactory; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Escaper; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Class add product to cart controller. + * + * @see \Magento\Checkout\Controller\Cart\Add + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class AddTest extends AbstractController +{ + /** @var SerializerInterface */ + private $json; + + /** @var CheckoutSessionFactory */ + private $checkoutSessionFactory; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** @var Escaper */ + private $escaper; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->json = $this->_objectManager->get(SerializerInterface::class); + $this->checkoutSessionFactory = $this->_objectManager->get(CheckoutSessionFactory::class); + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->executeInStoreContext = $this->_objectManager->get(ExecuteInStoreContext::class); + $this->escaper = $this->_objectManager->get(Escaper::class); + } + + /** + * Test with simple product and activated redirect to cart + * + * @magentoDataFixture Magento/Catalog/_files/products.php + * @magentoConfigFixture current_store checkout/cart/redirect_to_cart 1 + * + * @return void + */ + public function testMessageAtAddToCartWithRedirect(): void + { + $this->prepareReferer(); + $checkoutSession = $this->checkoutSessionFactory->create(); + $postData = [ + 'qty' => '1', + 'product' => '1', + 'custom_price' => 1, + 'isAjax' => 1, + ]; + $this->dispatchAddToCartRequest($postData); + $this->assertEquals( + $this->json->serialize(['backUrl' => 'http://localhost/checkout/cart/']), + $this->getResponse()->getBody() + ); + $this->assertSessionMessages( + $this->containsEqual((string)__('You added %1 to your shopping cart.', 'Simple Product')), + MessageInterface::TYPE_SUCCESS + ); + $this->assertCount(1, $checkoutSession->getQuote()->getItemsCollection()); + } + + /** + * Test with simple product and deactivated redirect to cart + * + * @magentoDataFixture Magento/Catalog/_files/products.php + * @magentoConfigFixture current_store checkout/cart/redirect_to_cart 0 + * + * @return void + */ + public function testMessageAtAddToCartWithoutRedirect(): void + { + $this->prepareReferer(); + $checkoutSession = $this->checkoutSessionFactory->create(); + $postData = [ + 'qty' => '1', + 'product' => '1', + 'custom_price' => 1, + 'isAjax' => 1, + ]; + $this->dispatchAddToCartRequest($postData); + $this->assertFalse($this->getResponse()->isRedirect()); + $this->assertEquals('[]', $this->getResponse()->getBody()); + $message = (string)__( + 'You added %1 to your <a href="%2">shopping cart</a>.', + 'Simple Product', + 'http://localhost/checkout/cart/' + ); + $this->assertSessionMessages( + $this->containsEqual("\n" . $message), + MessageInterface::TYPE_SUCCESS + ); + $this->assertCount(1, $checkoutSession->getQuote()->getItemsCollection()); + } + + /** + * @dataProvider wrongParamsDataProvider + * + * @param array $params + * @return void + */ + public function testWithWrongParams(array $params): void + { + $this->prepareReferer(); + $this->dispatchAddToCartRequest($params); + $this->assertRedirect($this->stringContains('http://localhost/test')); + } + + /** + * @return array + */ + public function wrongParamsDataProvider(): array + { + return [ + 'empty_params' => ['params' => []], + 'with_not_existing_product_id' => ['params' => ['product' => 989]], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * + * @return void + */ + public function testAddProductFromUnavailableWebsite(): void + { + $this->prepareReferer(); + $product = $this->productRepository->get('simple-1'); + $postData = ['product' => $product->getId()]; + $this->executeInStoreContext->execute('fixture_second_store', [$this, 'dispatchAddToCartRequest'], $postData); + $this->assertRedirect($this->stringContains('http://localhost/test')); + $message = $this->escaper->escapeHtml( + (string)__('The product wasn\'t found. Verify the product and try again.') + ); + $this->assertSessionMessages($this->containsEqual($message), MessageInterface::TYPE_ERROR); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * + * @return void + */ + public function testAddProductWithUnavailableQty(): void + { + $product = $this->productRepository->get('simple-1'); + $postData = ['product' => $product->getId(), 'qty' => '1000']; + $this->dispatchAddToCartRequest($postData); + $message = (string)__('The requested qty is not available'); + $this->assertSessionMessages($this->containsEqual($message), MessageInterface::TYPE_ERROR); + $this->assertRedirect($this->stringContains($product->getProductUrl())); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/products_related_multiple.php + * + * @return void + */ + public function testAddProductWithRelated(): void + { + $this->prepareReferer(); + $checkoutSession = $this->checkoutSessionFactory->create(); + $product = $this->productRepository->get('simple_with_cross'); + $params = [ + 'product' => $product->getId(), + 'related_product' => implode(',', $product->getRelatedProductIds()), + ]; + $this->dispatchAddToCartRequest($params); + $this->assertCount(3, $checkoutSession->getQuote()->getItemsCollection()); + $message = (string)__( + 'You added %1 to your <a href="%2">shopping cart</a>.', + $product->getName(), + 'http://localhost/checkout/cart/' + ); + $this->assertSessionMessages( + $this->containsEqual("\n" . $message), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Dispatch add product to cart request. + * + * @param array $postData + * @return void + */ + public function dispatchAddToCartRequest(array $postData = []): void + { + $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('checkout/cart/add'); + } + + /** + * Prepare referer to test. + * + * @return void + */ + private function prepareReferer(): void + { + $parameters = $this->_objectManager->create(Parameters::class); + $parameters->set('HTTP_REFERER', 'http://localhost/test'); + $this->getRequest()->setServer($parameters); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php index a9714a17ffe4f..fd89229bb73be 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php @@ -352,77 +352,6 @@ public function addAddProductDataProvider() ]; } - /** - * Test for \Magento\Checkout\Controller\Cart\Add::execute() with simple product and activated redirect to cart - * - * @magentoDataFixture Magento/Catalog/_files/products.php - * @magentoConfigFixture current_store checkout/cart/redirect_to_cart 1 - * @magentoAppIsolation enabled - */ - public function testMessageAtAddToCartWithRedirect() - { - $formKey = $this->_objectManager->get(FormKey::class); - $postData = [ - 'qty' => '1', - 'product' => '1', - 'custom_price' => 1, - 'form_key' => $formKey->getFormKey(), - 'isAjax' => 1 - ]; - \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); - $this->getRequest()->setPostValue($postData); - $this->getRequest()->setMethod('POST'); - - $this->dispatch('checkout/cart/add'); - - $this->assertEquals( - '{"backUrl":"http:\/\/localhost\/index.php\/checkout\/cart\/"}', - $this->getResponse()->getBody() - ); - - $this->assertSessionMessages( - $this->containsEqual( - 'You added Simple Product to your shopping cart.' - ), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS - ); - } - - /** - * Test for \Magento\Checkout\Controller\Cart\Add::execute() with simple product and deactivated redirect to cart - * - * @magentoDataFixture Magento/Catalog/_files/products.php - * @magentoConfigFixture current_store checkout/cart/redirect_to_cart 0 - * @magentoAppIsolation enabled - */ - public function testMessageAtAddToCartWithoutRedirect() - { - $formKey = $this->_objectManager->get(FormKey::class); - $postData = [ - 'qty' => '1', - 'product' => '1', - 'custom_price' => 1, - 'form_key' => $formKey->getFormKey(), - 'isAjax' => 1 - ]; - \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); - $this->getRequest()->setPostValue($postData); - $this->getRequest()->setMethod('POST'); - - $this->dispatch('checkout/cart/add'); - - $this->assertFalse($this->getResponse()->isRedirect()); - $this->assertEquals('[]', $this->getResponse()->getBody()); - - $this->assertSessionMessages( - $this->containsEqual( - "\n" . 'You added Simple Product to your ' . - '<a href="http://localhost/index.php/checkout/cart/">shopping cart</a>.' - ), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS - ); - } - /** * @covers \Magento\Checkout\Controller\Cart\Addgroup::execute() * diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Model/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Model/CartTest.php index f534904e9db6b..c8f3bc891a413 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Model/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Model/CartTest.php @@ -3,51 +3,163 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Checkout\Model; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Checkout\Model\SessionFactory as CheckoutSessionFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use PHPUnit\Framework\TestCase; -class CartTest extends \PHPUnit\Framework\TestCase +/** + * Test for checkout cart model. + * + * @see \Magento\Checkout\Model\Cart + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CartTest extends TestCase { + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CartFactory */ + private $cartFactory; + + /** @var ProductInterfaceFactory */ + private $productFactory; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** @var CheckoutSession */ + private $checkoutSession; + + /** @var CartInterface */ + private $quote; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + /** - * @var Cart + * @inheritdoc */ - private $cart; + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->cartFactory = $this->objectManager->get(CartFactory::class); + $this->productFactory = $this->objectManager->get(ProductInterfaceFactory::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + $this->checkoutSession = $this->objectManager->get(CheckoutSessionFactory::class)->create(); + $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + } /** - * @var ProductRepositoryInterface + * @inheritdoc */ - private $productRepository; - - protected function setUp(): void + protected function tearDown(): void { - $this->cart = Bootstrap::getObjectManager()->create(Cart::class); - $this->productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + + parent::tearDown(); } /** - * @magentoDataFixture Magento/Checkout/_files/simple_product.php * @magentoDataFixture Magento/Checkout/_files/set_product_min_in_cart.php - * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * + * @return void */ - public function testAddProductWithLowerQty() + public function testAddProductWithLowerQty(): void { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('The fewest you may purchase is 3'); + $cart = $this->cartFactory->create(); + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage((string)__('The fewest you may purchase is %1', 3)); $product = $this->productRepository->get('simple'); - $this->cart->addProduct($product->getId(), ['qty' => 1]); + $cart->addProduct($product->getId(), ['qty' => 1]); } /** - * @magentoDataFixture Magento/Checkout/_files/simple_product.php * @magentoDataFixture Magento/Checkout/_files/set_product_min_in_cart.php - * @magentoDbIsolation enabled + * + * @return void + */ + public function testAddProductWithNoQty(): void + { + $cart = $this->cartFactory->create(); + $product = $this->productRepository->get('simple'); + $cart->addProduct($product->getId(), [])->save(); + $this->quote = $cart->getQuote(); + $this->assertCount(1, $cart->getItems()); + $this->assertEquals($product->getId(), $this->checkoutSession->getLastAddedProductId()); + } + + /** + * @return void + */ + public function testAddNotExistingProduct(): void + { + $product = $this->productFactory->create(); + $this->expectExceptionObject( + new LocalizedException(__('The product wasn\'t found. Verify the product and try again.')) + ); + $this->cartFactory->create()->addProduct($product); + } + + /** + * @return void + */ + public function testAddNotExistingProductId(): void + { + $this->expectExceptionObject( + new LocalizedException(__('The product wasn\'t found. Verify the product and try again.')) + ); + $this->cartFactory->create()->addProduct(989); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * + * @return void + */ + public function testAddProductFromUnavailableWebsite(): void + { + $product = $this->productRepository->get('simple'); + $this->expectExceptionObject( + new LocalizedException(__('The product wasn\'t found. Verify the product and try again.')) + ); + $this->executeInStoreContext + ->execute('fixture_second_store', [$this->cartFactory->create(), 'addProduct'], $product->getId()); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * + * @return void */ - public function testAddProductWithNoQty() + public function testAddProductWithInvalidRequest(): void { $product = $this->productRepository->get('simple'); - $this->cart->addProduct($product->getId(), []); + $message = __('We found an invalid request for adding product to quote.'); + $this->expectExceptionObject(new LocalizedException($message)); + $this->cartFactory->create()->addProduct($product->getId(), ''); } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php index 41bf18619332a..44d900a8f2aca 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Checkout\Model; use Magento\Catalog\Api\Data\ProductTierPriceInterface; @@ -10,22 +12,24 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\Session as CustomerSession; -use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Model\Quote; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; /** * Checkout Session model test. * + * @see \Magento\Checkout\Model\Session + * @magentoDbIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class SessionTest extends \PHPUnit\Framework\TestCase +class SessionTest extends TestCase { /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; @@ -45,40 +49,78 @@ class SessionTest extends \PHPUnit\Framework\TestCase private $checkoutSession; /** - * @return void + * @var GetQuoteByReservedOrderId + */ + private $getQuoteByReservedOrderId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + + /** + * @var CartInterface + */ + private $quote; + + /** + * @inheritdoc */ protected function setUp(): void { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); $this->customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class); $this->customerSession = $this->objectManager->get(CustomerSession::class); - $this->checkoutSession = $this->objectManager->create(Session::class); + $this->checkoutSession = $this->objectManager->get(Session::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + $this->customerSession->setCustomerId(null); + $this->checkoutSession->clearQuote(); + $this->checkoutSession->setCustomerData(null); + + parent::tearDown(); } /** * Tests that quote items and totals are correct when product becomes unavailable. * - * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Sales/_files/quote.php * @magentoAppIsolation enabled + * + * @return void */ - public function testGetQuoteWithUnavailableProduct() + public function testGetQuoteWithUnavailableProduct(): void { $reservedOrderId = 'test01'; $quoteGrandTotal = 10; - - $quote = $this->getQuote($reservedOrderId); + $quote = $this->getQuoteByReservedOrderId->execute($reservedOrderId); $this->assertEquals(1, $quote->getItemsCount()); $this->assertCount(1, $quote->getItems()); $this->assertEquals($quoteGrandTotal, $quote->getShippingAddress()->getBaseGrandTotal()); - - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple'); + $product = $this->productRepository->get('simple'); $product->setStatus(Status::STATUS_DISABLED); - $productRepository->save($product); + $this->productRepository->save($product); $this->checkoutSession->setQuoteId($quote->getId()); $quote = $this->checkoutSession->getQuote(); - $this->assertEquals(0, $quote->getItemsCount()); $this->assertEmpty($quote->getItems()); $this->assertEquals(0, $quote->getShippingAddress()->getBaseGrandTotal()); @@ -90,15 +132,15 @@ public function testGetQuoteWithUnavailableProduct() * Expected result - quote object should be loaded and customer data should be set to it. * * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php + * + * @return void */ - public function testGetQuoteNotInitializedCustomerSet() + public function testGetQuoteNotInitializedCustomerSet(): void { $customer = $this->customerRepository->getById(1); $this->checkoutSession->setCustomerData($customer); - - /** Execute SUT */ $quote = $this->checkoutSession->getQuote(); - $this->_validateCustomerDataInQuote($quote); + $this->validateCustomerDataInQuote($quote); } /** @@ -107,36 +149,29 @@ public function testGetQuoteNotInitializedCustomerSet() * Expected result - quote object should be loaded and customer data should be set to it. * * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php - * @magentoAppIsolation enabled + * + * @return void */ - public function testGetQuoteNotInitializedCustomerLoggedIn() + public function testGetQuoteNotInitializedCustomerLoggedIn(): void { $customer = $this->customerRepository->getById(1); $this->customerSession->setCustomerDataObject($customer); - - /** Execute SUT */ $quote = $this->checkoutSession->getQuote(); - $this->_validateCustomerDataInQuote($quote); + $this->validateCustomerDataInQuote($quote); } /** * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php - * @magentoAppIsolation enabled + * + * @return void */ - public function testGetQuoteWithMismatchingSession() + public function testGetQuoteWithMismatchingSession(): void { - /** @var Quote $quote */ - $quote = Bootstrap::getObjectManager()->create(Quote::class); - /** @var \Magento\Quote\Model\ResourceModel\Quote $quoteResource */ - $quoteResource = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\ResourceModel\Quote::class); - $quoteResource->load($quote, 'test01', 'reserved_order_id'); - - // Customer on quote is not logged in + $quote = $this->getQuoteByReservedOrderId->execute('test01'); $this->checkoutSession->setQuoteId($quote->getId()); - - $sessionQuote = $this->checkoutSession->getQuote(); - $this->assertEmpty($sessionQuote->getCustomerId()); - $this->assertNotEquals($quote->getId(), $sessionQuote->getId()); + $this->quote = $this->checkoutSession->getQuote(); + $this->assertEmpty($this->quote->getCustomerId()); + $this->assertNotEquals($quote->getId(), $this->quote->getId()); } /** @@ -150,96 +185,106 @@ public function testGetQuoteWithMismatchingSession() * Quote which is set to checkout session should contain customer data * * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoAppIsolation enabled + * + * @return void */ - public function testLoadCustomerQuoteCustomerWithoutQuote() + public function testLoadCustomerQuoteCustomerWithoutQuote(): void { - $quote = $this->checkoutSession->getQuote(); - $this->assertEmpty($quote->getCustomerId(), 'Precondition failed: Customer data must not be set to quote'); - $this->assertEmpty($quote->getCustomerEmail(), 'Precondition failed: Customer data must not be set to quote'); - + $this->quote = $this->checkoutSession->getQuote(); + $this->assertEmpty( + $this->quote->getCustomerId(), + 'Precondition failed: Customer data must not be set to quote' + ); + $this->assertEmpty( + $this->quote->getCustomerEmail(), + 'Precondition failed: Customer data must not be set to quote' + ); + self::assertEquals( + '1', + $this->quote->getCustomerIsGuest(), + 'Precondition failed: Customer must be as guest in quote' + ); $customer = $this->customerRepository->getById(1); $this->customerSession->setCustomerDataObject($customer); - - /** Ensure that customer data is still unavailable before SUT invocation */ - $quote = $this->checkoutSession->getQuote(); - $this->assertEmpty($quote->getCustomerEmail(), 'Precondition failed: Customer data must not be set to quote'); - - /** Execute SUT */ + $this->quote = $this->checkoutSession->getQuote(); + $this->assertEmpty( + $this->quote->getCustomerEmail(), + 'Precondition failed: Customer data must not be set to quote' + ); $this->checkoutSession->loadCustomerQuote(); - $quote = $this->checkoutSession->getQuote(); - $this->_validateCustomerDataInQuote($quote); + $this->quote = $this->checkoutSession->getQuote(); + $this->validateCustomerDataInQuote($this->quote); } /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Sales/_files/quote.php + * + * @return void */ - public function testGetQuoteWithProductWithTierPrice() + public function testGetQuoteWithProductWithTierPrice(): void { $reservedOrderId = 'test01'; $customerGroupId = 1; $tierPriceQty = 1; $tierPriceValue = 9; - - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple'); - $tierPrice = $this->objectManager->create(ProductTierPriceInterface::class) + $product = $this->productRepository->get('simple'); + $tierPrice = $this->objectManager->get(ProductTierPriceInterface::class) ->setCustomerGroupId($customerGroupId) ->setQty($tierPriceQty) ->setValue($tierPriceValue); $product->setTierPrices([$tierPrice]); - $productRepository->save($product); - - $quote = $this->getQuote($reservedOrderId); + $this->productRepository->save($product); + $quote = $this->getQuoteByReservedOrderId->execute($reservedOrderId); $this->checkoutSession->setQuoteId($quote->getId()); - $quote = $this->checkoutSession->getQuote(); $item = $quote->getItems()[0]; - /** @var \Magento\Catalog\Model\Product $quoteProduct */ $quoteProduct = $item->getProduct(); $this->assertEquals(10, $quoteProduct->getTierPrice($tierPriceQty)); - $customer = $this->customerRepository->getById(1); $this->customerSession->setCustomerDataAsLoggedIn($customer); - $quote = $this->checkoutSession->getQuote(); $item = $quote->getItems()[0]; - /** @var \Magento\Catalog\Model\Product $quoteProduct */ $quoteProduct = $item->getProduct(); $this->assertEquals($tierPriceValue, $quoteProduct->getTierPrice(1)); } /** - * Returns quote by reserved order id. + * Test covers case when quote is not yet initialized and customer is guest * - * @param string $reservedOrderId - * @return CartInterface + * Expected result - quote object should be loaded with customer as guest */ - private function getQuote(string $reservedOrderId): CartInterface + public function testGetQuoteNotInitializedGuest() { - $filterBuilder = $this->objectManager->create(FilterBuilder::class); - $filter = $filterBuilder->setField('reserved_order_id') - ->setConditionType('=') - ->setValue($reservedOrderId) - ->create(); - $searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); - $searchCriteria = $searchCriteriaBuilder->addFilters([$filter]) - ->create(); - $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); - $searchResult = $quoteRepository->getList($searchCriteria); - /** @var CartInterface[] $items */ - $items = $searchResult->getItems(); + $quote = $this->checkoutSession->getQuote(); + self::assertEquals('1', $quote->getCustomerIsGuest()); + } - return \array_values($items)[0]; + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testMergeGuestQuoteWithCustomerQuote(): void + { + $guestQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $customerQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($guestQuote->getId()); + $this->customerSession->setCustomerId(1); + $updatedQuote = $this->checkoutSession->loadCustomerQuote()->getQuote(); + $this->assertNull($this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address')); + $this->assertEquals($customerQuote->getId(), $updatedQuote->getId()); + $this->assertCount(2, $updatedQuote->getItems()); } /** * Ensure that quote has customer data specified in customer fixture. * - * @param \Magento\Quote\Model\Quote $quote + * @param CartInterface $quote + * @return void */ - protected function _validateCustomerDataInQuote($quote) + private function validateCustomerDataInQuote(CartInterface $quote): void { $customerIdFromFixture = 1; $customerEmailFromFixture = 'customer@example.com'; @@ -259,5 +304,10 @@ protected function _validateCustomerDataInQuote($quote) $quote->getCustomerFirstname(), 'Customer first name was not set to Quote correctly.' ); + self::assertEquals( + '0', + $quote->getCustomerIsGuest(), + 'Customer should not be as guest in Quote.' + ); } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/ViewModel/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/ViewModel/CartTest.php new file mode 100644 index 0000000000000..7fb57ca0f4090 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/ViewModel/CartTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\ViewModel; + +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for clear shopping cart config + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CartTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Cart + */ + private $cart; + + /** + * @var MutableScopeConfigInterface + */ + private $mutableScopeConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = $this->objectManager = Bootstrap::getObjectManager(); + $this->cart = $objectManager->get(Cart::class); + $this->mutableScopeConfig = $objectManager->get(MutableScopeConfigInterface::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testConfigClearShoppingCartEnabledWithWebsiteScopes() + { + // Assert not active by default + $this->assertFalse($this->cart->isClearShoppingCartEnabled()); + + // Enable Clear Shopping Cart in default website scope + $this->setClearShoppingCartEnabled( + true, + ScopeInterface::SCOPE_WEBSITE + ); + + // Assert now active in default website scope + $this->assertTrue($this->cart->isClearShoppingCartEnabled()); + + $defaultStore = $this->storeManager->getStore(); + $defaultWebsite = $defaultStore->getWebsite(); + $defaultWebsiteCode = $defaultWebsite->getCode(); + + $secondStore = $this->storeManager->getStore('fixture_second_store'); + $secondWebsite = $secondStore->getWebsite(); + $secondWebsiteCode = $secondWebsite->getCode(); + + // Change current store context to that of second website + $this->storeManager->setCurrentStore($secondStore); + + // Assert not active by default in second website + $this->assertFalse($this->cart->isClearShoppingCartEnabled()); + + // Enable Clear Shopping Cart in second website scope + $this->setClearShoppingCartEnabled( + true, + ScopeInterface::SCOPE_WEBSITE, + $secondWebsiteCode + ); + + // Assert now active in second website scope + $this->assertTrue($this->cart->isClearShoppingCartEnabled()); + + // Disable Clear Shopping Cart in default website scope + $this->setClearShoppingCartEnabled( + false, + ScopeInterface::SCOPE_WEBSITE, + $defaultWebsiteCode + ); + + // Assert still active in second website + $this->assertTrue($this->cart->isClearShoppingCartEnabled()); + } + + /** + * Set clear shopping cart enabled. + * + * @param bool $isActive + * @param string $scope + * @param string|null $scopeCode + */ + private function setClearShoppingCartEnabled(bool $isActive, string $scope, $scopeCode = null) + { + $this->mutableScopeConfig->setValue( + 'checkout/cart/enable_clear_shopping_cart', + $isActive ? '1' : '0', + $scope, + $scopeCode + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website.php new file mode 100644 index 0000000000000..a705335a3f68b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website.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\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\Store\Model\StoreManagerInterface; +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/products_with_websites_and_stores.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 StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var CartInterface $quote */ +$quote = $objectManager->get(CartInterfaceFactory::class)->create(); +$quote->setStoreId($storeManager->getStore('fixture_second_store')->getId()) + ->setIsActive(true) + ->setIsMultiShipping(0) + ->setCustomer($customer) + ->setCheckoutMethod(Onepage::METHOD_CUSTOMER) + ->setReservedOrderId('test_order_on_second_website') + ->addProduct($productRepository->get('simple-2'), 1); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website_rollback.php new file mode 100644 index 0000000000000..2cc43f9171f4b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website_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\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_on_second_website'); +if ($quote !== null) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance() + ->requireDataFixture('Magento/Catalog/_files/products_with_websites_and_stores_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); 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/customer_quote_with_items_simple_product_options.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php new file mode 100644 index 0000000000000..f0ba56f7179aa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile; +use Magento\Catalog\Model\Product\Option\Value; +use Magento\TestFramework\Catalog\Model\Product\Option\Type\File\ValidatorFileMock; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\DataObject; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_with_options.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_with_uk_address.php'); + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +/** @var Quote $quote */ +$quote = $objectManager->get(QuoteFactory::class)->create(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); + +$customer = $customerRepository->get('customer_uk_address@test.com'); +$product = $productRepository->get('simple'); +$options = []; +$dropDownValues = []; +$iDate = 1; +/** @var Option $option */ +foreach ($product->getOptions() as $option) { + switch ($option->getGroupByType()) { + case ProductCustomOptionInterface::OPTION_GROUP_SELECT: + if ($option->getType() == ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN) { + $dropDownValues = $option->getValues(); + $value = null; + } elseif ($option->getType() == ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX) { + $value = array_keys($option->getValues()); + } else { + $value = (string)key($option->getValues()); + } + break; + case ProductCustomOptionInterface::OPTION_GROUP_DATE: + $value = [ + 'year' => 2013 + $iDate, + 'month' => 1 + $iDate, + 'day' => 1 + $iDate, + 'hour' => 10 + $iDate, + 'minute' => 30 + $iDate, + ]; + $iDate++; + break; + case ProductCustomOptionInterface::OPTION_GROUP_FILE: + $value = 'test.jpg'; + break; + default: + $value = 'test'; + break; + } + $options[$option->getId()] = $value; +} + +$itemsOptions = []; +/** @var Value $dropDownValue */ +foreach ($dropDownValues as $dropDownId => $dropDownValue) { + $options[$dropDownValue->getOption()->getId()] = $dropDownId; + $itemsOptions[$dropDownValue->getTitle()] = $options; +} + +$validatorFileMock = $objectManager->get(ValidatorFileMock::class)->getInstance(); +$objectManager->addSharedInstance($validatorFileMock, ValidatorFile::class); + +$quote->setStoreId($storeManager->getStore()->getId()) + ->assignCustomer($customer) + ->setReservedOrderId('customer_quote_product_custom_options'); + +/** @var DataObject $request */ +$requestInfo = $objectManager->create(DataObject::class); + +foreach ($itemsOptions as $itemOptions) { + $requestInfo->setData(['qty' => 1, 'options' => $itemOptions]); + $product = clone $product; + $quote->addProduct($product, $requestInfo); +} + +$quoteRepository->save($quote); +$objectManager->removeSharedInstance(ValidatorFile::class); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options_rollback.php new file mode 100644 index 0000000000000..5877e9a5ef975 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$quote = $objectManager->get(GetQuoteByReservedOrderId::class)->execute('customer_quote_product_custom_options'); +if ($quote !== null) { + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $objectManager->get(CartRepositoryInterface::class); + $quoteRepository->delete($quote); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_with_uk_address_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_with_options_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/Checkout/_files/quote_with_bundle_product_with_dynamic_price.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price.php new file mode 100644 index 0000000000000..b6d51dfe0fdc7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\Product\Type; +use Magento\Bundle\Model\Selection; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Checkout\Model\Cart; +use Magento\Checkout\Model\Session; +use Magento\Framework\DataObject; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Bundle/_files/bundle_product_with_dynamic_price.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +/** @var Product $product */ +$product = $productRepository->get('bundle_product_with_dynamic_price'); + +/** @var $typeInstance Type */ +//Load options +$typeInstance = $product->getTypeInstance(); +$typeInstance->setStoreFilter($product->getStoreId(), $product); +$optionCollection = $typeInstance->getOptionsCollection($product); + +$bundleOptions = []; +$bundleOptionsQty = []; +/** @var $option Option */ +foreach ($optionCollection as $option) { + $selectionCollection = $typeInstance->getSelectionsCollection([$option->getId()], $product); + /** @var $selection Selection */ + $selection = $selectionCollection->getFirstItem(); + $bundleOptions[$option->getId()] = $selection->getSelectionId(); + $bundleOptionsQty[$option->getId()] = 1; +} + +$requestInfo = new DataObject( + ['qty' => 1, 'bundle_option' => $bundleOptions, 'bundle_option_qty' => $bundleOptionsQty] +); + +/** @var Cart $cart */ +$cart = $objectManager->create(Cart::class); +$cart->addProduct($product, $requestInfo); +$cart->getQuote()->setReservedOrderId('quote_with_bundle_product_with_dynamic_price'); +$cart->save(); + +$objectManager->removeSharedInstance(Session::class); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price_rollback.php new file mode 100644 index 0000000000000..8507b6f1a2619 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Registry; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$quote = $objectManager->create(Quote::class); +$quote->load('quote_with_bundle_product_with_dynamic_price', 'reserved_order_id')->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Bundle/_files/bundle_product_with_dynamic_price_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address.php new file mode 100644 index 0000000000000..1e3813f3970bc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address.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/second_product_simple.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(true) + ->setIsMultiShipping(0) + ->setCustomer($customer) + ->setCheckoutMethod(Onepage::METHOD_CUSTOMER) + ->setReservedOrderId('test_order_with_customer_without_address') + ->addProduct($productRepository->get('simple2'), 1); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address_rollback.php new file mode 100644 index 0000000000000..bd06a8da059dd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address_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_without_address'); +if ($quote !== null) { + $quoteRepository->delete($quote); +} + +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/Checkout/_files/quote_with_items_and_custom_options_saved.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_items_and_custom_options_saved.php index 3abe6b21f110e..2293f0662c699 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_items_and_custom_options_saved.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_items_and_custom_options_saved.php @@ -4,7 +4,12 @@ * See COPYING.txt for license details. */ -use Magento\Checkout\_files\ValidatorFileMock; +use Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile; +use Magento\Framework\DataObject; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\QuoteRepository; +use Magento\TestFramework\Catalog\Model\Product\Option\Type\File\ValidatorFileMock; use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; @@ -12,7 +17,6 @@ Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_address.php'); Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_with_options.php'); -Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/ValidatorFileMock.php'); $objectManager = Bootstrap::getObjectManager(); /** @var QuoteFactory $quoteFactory */ @@ -45,18 +49,18 @@ $options[$option->getId()] = $value; } -$requestInfo = new \Magento\Framework\DataObject(['qty' => 1, 'options' => $options]); -$validatorFile = (new ValidatorFileMock())->getInstance(); -$objectManager->addSharedInstance($validatorFile, \Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile::class); +$requestInfo = new DataObject(['qty' => 1, 'options' => $options]); +$validatorFile = $objectManager->get(ValidatorFileMock::class)->getInstance(); +$objectManager->addSharedInstance($validatorFile, ValidatorFile::class); $quote->setReservedOrderId('test_order_item_with_items_and_custom_options'); $quote->addProduct($product, $requestInfo); $quote->collectTotals(); -$objectManager->get(\Magento\Quote\Model\QuoteRepository::class)->save($quote); +$objectManager->get(QuoteRepository::class)->save($quote); -/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +/** @var QuoteIdMask $quoteIdMask */ $quoteIdMask = Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->setQuoteId($quote->getId()); $quoteIdMask->setDataChanges(true); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product.php new file mode 100644 index 0000000000000..2321aa1d4bc71 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product.php @@ -0,0 +1,51 @@ +<?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/simple_products_not_visible_individually.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @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('test_order_with_not_visible_product') + ->setEmail($customer->getEmail()) + ->addProduct($productRepository->get('simple_not_visible_1'), 1); + +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product_rollback.php new file mode 100644 index 0000000000000..4ed8bf5e11735 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product_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('test_order_with_not_visible_product'); +if ($quote) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance() + ->requireDataFixture('Magento/Catalog/_files/simple_products_not_visible_individually_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/quote_with_taxable_product_and_customer.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer.php new file mode 100644 index 0000000000000..c32d299d427b5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer.php @@ -0,0 +1,51 @@ +<?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/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 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('test_order_with_taxable_product') + ->setEmail($customer->getEmail()) + ->addProduct($productRepository->get('taxable_product'), 1); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer_rollback.php new file mode 100644 index 0000000000000..0051023e48060 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer_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\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_taxable_product'); +if ($quote) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/taxable_simple_product_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/CheckoutAgreements/_files/agreement_active_with_html_content.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php index 66b452d234366..ee99ec96bbf2c 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + $agreement->setData([ 'name' => 'Checkout Agreement (active)', 'content' => 'Checkout agreement content: <b>HTML</b>', @@ -15,4 +28,4 @@ 'is_html' => true, 'stores' => [0, 1], ]); -$agreement->save(); +$agreementResource->save($agreement); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php index da65dcae7d8f4..10879d3d91306 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php @@ -3,10 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('Checkout Agreement (active)', 'name'); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + +$agreementResource->load($agreement, 'Checkout Agreement (active)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php index e60c754d66a3c..29b01163df514 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + $agreement->setData([ 'name' => 'Checkout Agreement (inactive)', 'content' => 'Checkout agreement content: TEXT', @@ -15,4 +28,4 @@ 'is_html' => false, 'stores' => [0, 1], ]); -$agreement->save(); +$agreementResource->save($agreement); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php index 39ba6cf30be26..3fda82782ebc5 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php @@ -4,10 +4,22 @@ * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('Checkout Agreement (inactive)', 'name'); +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + +$agreementResource->load($agreement, 'Checkout Agreement (inactive)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php index 3be16338110a1..8d15bf6e9b74f 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + $agreement->setData([ 'name' => 'First Checkout Agreement (active)', 'content' => 'Checkout agreement content: TEXT', @@ -16,8 +29,9 @@ 'mode' => 1, 'stores' => [0, 1], ]); -$agreement->save(); -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); +$agreementResource->save($agreement); + +$agreement = $objectManager->create(Agreement::class); $agreement->setData([ 'name' => 'Second Checkout Agreement (active)', 'content' => 'Checkout agreement content: TEXT', @@ -28,4 +42,5 @@ 'mode' => 1, 'stores' => [0, 1], ]); -$agreement->save(); + +$agreementResource->save($agreement); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php index 9c594c0c22b65..f43f7a5ba9a51 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php @@ -4,15 +4,28 @@ * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('First Checkout Agreement (active)', 'name'); +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + +$agreementResource->load($agreement, 'First Checkout Agreement (active)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('Second Checkout Agreement (active)', 'name'); + +$agreement = $objectManager->create(Agreement::class); +$agreementResource->load($agreement, 'Second Checkout Agreement (active)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } 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 cb6ea60a5efdc..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 { @@ -66,11 +69,18 @@ class PageDesignTest extends AbstractBackendController */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + ] + ]); parent::setUp(); $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); $this->pageRetriever = Bootstrap::getObjectManager()->get(GetPageByIdentifierInterface::class); $this->scopeConfig = Bootstrap::getObjectManager()->get(ScopeConfigInterface::class); + $this->pagesToDelete = []; } /** @@ -80,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 + } + } + } } /** @@ -144,6 +183,7 @@ public function testSaveDesign(): void self::equalTo($sessionMessages), MessageInterface::TYPE_ERROR ); + $this->pagesToDelete = [$id]; } /** @@ -175,6 +215,7 @@ public function testSaveDesignWithDefaults(): void $this->assertNotEmpty($page->getId()); $this->assertNotNull($page->getPageLayout()); $this->assertEquals($defaultLayout, $page->getPageLayout()); + $this->pagesToDelete = [$id]; } /** @@ -221,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/Controller/PageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php index 8d82602b3ac1c..4d9178f1a0659 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php @@ -4,17 +4,38 @@ * See COPYING.txt for license details. */ -/** - * Test class for \Magento\Cms\Controller\Page. - */ namespace Magento\Cms\Controller; use Magento\Cms\Api\GetPageByIdentifierInterface; +use Magento\Cms\Model\Page\CustomLayoutManagerInterface; use Magento\Framework\View\LayoutInterface; -use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Cms\Model\CustomLayoutManager; +use Magento\TestFramework\TestCase\AbstractController; -class PageTest extends \Magento\TestFramework\TestCase\AbstractController +/** + * Test for \Magento\Cms\Controller\Page\View class. + */ +class PageTest extends AbstractController { + /** + * @var GetPageByIdentifierInterface + */ + private $pageRetriever; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->_objectManager->configure([ + 'preferences' => [ + CustomLayoutManagerInterface::class => CustomLayoutManager::class, + ] + ]); + $this->pageRetriever = $this->_objectManager->get(GetPageByIdentifierInterface::class); + } + public function testViewAction() { $this->dispatch('/enable-cookies'); @@ -37,9 +58,7 @@ public function testViewRedirectWithTrailingSlash() public function testAddBreadcrumbs() { $this->dispatch('/enable-cookies'); - $layout = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - ); + $layout = $this->_objectManager->get(LayoutInterface::class); $breadcrumbsBlock = $layout->getBlock('breadcrumbs'); $this->assertStringContainsString($breadcrumbsBlock->toHtml(), $this->getResponse()->getBody()); } @@ -76,12 +95,10 @@ public static function cmsPageWithSystemRouteFixture() */ public function testCustomHandles(): void { - /** @var GetPageByIdentifierInterface $pageFinder */ - $pageFinder = Bootstrap::getObjectManager()->get(GetPageByIdentifierInterface::class); - $page = $pageFinder->execute('test_custom_layout_page_3', 0); - $this->dispatch('/cms/page/view/page_id/' .$page->getId()); + $page = $this->pageRetriever->execute('test_custom_layout_page_3', 0); + $this->dispatch('/cms/page/view/page_id/' . $page->getId()); /** @var LayoutInterface $layout */ - $layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + $layout = $this->_objectManager->get(LayoutInterface::class); $handles = $layout->getUpdate()->getHandles(); $this->assertContains('cms_page_view_selectable_test_custom_layout_page_3_test_selected', $handles); } @@ -97,8 +114,37 @@ public function testHomePageCustomHandles(): void { $this->dispatch('/'); /** @var LayoutInterface $layout */ - $layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + $layout = $this->_objectManager->get(LayoutInterface::class); $handles = $layout->getUpdate()->getHandles(); $this->assertContains('cms_page_view_selectable_home_page_custom_layout', $handles); } + + /** + * Tests page renders even with unavailable custom page layout. + * + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @dataProvider pageLayoutDataProvider + * @param string $pageIdentifier + * @return void + */ + public function testPageWithCustomLayout(string $pageIdentifier): void + { + $page = $this->pageRetriever->execute($pageIdentifier, 0); + $this->dispatch('/cms/page/view/page_id/' . $page->getId()); + $this->assertStringContainsString( + '<main id="maincontent" class="page-main">', + $this->getResponse()->getBody() + ); + } + + /** + * @return array + */ + public function pageLayoutDataProvider(): array + { + return [ + 'Page with 1column layout' => ['page-with-1column-layout'], + 'Page with unavailable layout' => ['page-with-unavailable-layout'] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php index ae431f5c4cf1a..2fa0bf3a5bc13 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php @@ -14,15 +14,27 @@ $data = [ [ 'title' => 'simplePage', - 'is_active' => 1 + 'is_active' => 1, ], [ 'title' => 'simplePage01', - 'is_active' => 1 + 'is_active' => 1, ], [ 'title' => '01simplePage', - 'is_active' => 1 + 'is_active' => 1, + ], + [ + 'title' => 'Page with 1column layout', + 'is_active' => 1, + 'content' => '<h1>Test Page Content</h1>', + 'page_layout' => '1column', + ], + [ + 'title' => 'Page with unavailable layout', + 'content' => '<h1>Test Page Content</h1>', + 'is_active' => 1, + 'page_layout' => 'unavailable-layout', ], ]; diff --git a/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php index 261cdba589653..00bec67bcfefc 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php @@ -16,7 +16,18 @@ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); -$searchCriteria = $searchCriteriaBuilder->addFilter('title', ['simplePage', 'simplePage01', '01simplePage'], 'in') +$searchCriteria = $searchCriteriaBuilder + ->addFilter( + 'title', + [ + 'simplePage', + 'simplePage01', + '01simplePage', + 'Page with 1column layout', + 'Page with unavailable layout', + ], + 'in' + ) ->create(); $result = $pageRepository->getList($searchCriteria); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php index 0036c1722fd52..5542b779cde47 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php @@ -21,6 +21,8 @@ /** * Test the repository. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CustomLayoutRepositoryTest extends TestCase { @@ -50,6 +52,12 @@ class CustomLayoutRepositoryTest extends TestCase protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); + $objectManager->configure([ + 'preferences' => [ + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + ] + ]); $this->fakeManager = $objectManager->get(CustomLayoutManager::class); $this->repo = $objectManager->create(CustomLayoutRepositoryInterface::class, ['manager' => $this->fakeManager]); $this->pageFactory = $objectManager->get(PageFactory::class); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php index 17188238c5126..5197daa759e04 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php @@ -48,6 +48,12 @@ class DataProviderTest extends TestCase protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); + $objectManager->configure([ + 'preferences' => [ + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + ] + ]); $this->repo = $objectManager->get(GetPageByIdentifierInterface::class); $this->filesFaker = $objectManager->get(CustomLayoutManager::class); $this->request = $objectManager->get(HttpRequest::class); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php index 88d84eb4dc80a..53e514083d6ba 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php @@ -34,6 +34,12 @@ class PageRepositoryTest extends TestCase */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + ] + ]); $this->repo = Bootstrap::getObjectManager()->get(PageRepositoryInterface::class); $this->retriever = Bootstrap::getObjectManager()->get(GetPageByIdentifierInterface::class); } 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/Cms/Model/Wysiwyg/Images/StorageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php index 5685f9f140a6d..cb96ca2a14cac 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php @@ -6,7 +6,13 @@ */ namespace Magento\Cms\Model\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage\Collection; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\DataObject; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; /** * Test methods of class Storage @@ -29,22 +35,27 @@ class StorageTest extends \PHPUnit\Framework\TestCase private $objectManager; /** - * @var \Magento\Framework\Filesystem + * @var Filesystem */ private $filesystem; /** - * @var \Magento\Cms\Model\Wysiwyg\Images\Storage + * @var Storage */ private $storage; + /** + * @var DriverInterface + */ + private $driver; + /** * @inheritdoc */ // phpcs:disable public static function setUpBeforeClass(): void { - self::$_baseDir = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + self::$_baseDir = Bootstrap::getObjectManager()->get( \Magento\Cms\Helper\Wysiwyg\Images::class )->getCurrentPath() . 'MagentoCmsModelWysiwygImagesStorageTest'; if (!file_exists(self::$_baseDir)) { @@ -60,8 +71,8 @@ public static function setUpBeforeClass(): void // phpcs:ignore public static function tearDownAfterClass(): void { - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Filesystem\Driver\File::class + Bootstrap::getObjectManager()->create( + File::class )->deleteDirectory( self::$_baseDir ); @@ -72,9 +83,10 @@ public static function tearDownAfterClass(): void */ protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->filesystem = $this->objectManager->get(\Magento\Framework\Filesystem::class); - $this->storage = $this->objectManager->create(\Magento\Cms\Model\Wysiwyg\Images\Storage::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->storage = $this->objectManager->create(Storage::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); } /** @@ -83,16 +95,31 @@ protected function setUp(): void */ public function testGetFilesCollection(): void { - \Magento\TestFramework\Helper\Bootstrap::getInstance() + Bootstrap::getInstance() ->loadArea(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); - $collection = $this->storage->getFilesCollection(self::$_baseDir, 'media'); - $this->assertInstanceOf(\Magento\Cms\Model\Wysiwyg\Images\Storage\Collection::class, $collection); + $fileName = 'magento_image.jpg'; + $imagePath = realpath(__DIR__ . '/../../../../Catalog/_files/' . $fileName); + $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $modifiableFilePath = $mediaDirectory->getAbsolutePath('MagentoCmsModelWysiwygImagesStorageTest/' . $fileName); + $this->driver->copy( + $imagePath, + $modifiableFilePath + ); + $this->storage->resizeFile($modifiableFilePath); + $collection = $this->storage->getFilesCollection(self::$_baseDir, 'image'); + $this->assertInstanceOf(Collection::class, $collection); foreach ($collection as $item) { - $this->assertInstanceOf(\Magento\Framework\DataObject::class, $item); - $this->assertStringEndsWith('/1.swf', $item->getUrl()); - $this->assertStringMatchesFormat( - 'http://%s/static/%s/adminhtml/%s/%s/Magento_Cms/images/placeholder_thumbnail.jpg', - $item->getThumbUrl() + $this->assertInstanceOf(DataObject::class, $item); + $this->assertStringEndsWith('/' . $fileName, $item->getUrl()); + $this->assertEquals( + '/pub/media/.thumbsMagentoCmsModelWysiwygImagesStorageTest/magento_image.jpg', + parse_url($item->getThumbUrl(), PHP_URL_PATH), + "Check if Thumbnail URL is equal to the generated URL" + ); + $this->assertEquals( + 'image/jpeg', + $item->getMimeType(), + "Check if Mime Type is equal to the image in the file system" ); return; } @@ -121,7 +148,7 @@ public function testDeleteDirectory(): void $this->storage->createDirectory($dir, $path); $this->assertFileExists($fullPath); $this->storage->deleteDirectory($fullPath); - $this->assertFileNotExists($fullPath); + $this->assertFileDoesNotExist($fullPath); } /** @@ -142,7 +169,7 @@ public function testDeleteDirectoryWithExcludedDirPath(): void public function testUploadFile(): void { $fileName = 'magento_small_image.jpg'; - $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); $filePath = $tmpDirectory->getAbsolutePath($fileName); // phpcs:disable $fixtureDir = realpath(__DIR__ . '/../../../../Catalog/_files'); @@ -167,10 +194,12 @@ public function testUploadFile(): void public function testUploadFileWithExcludedDirPath(): void { $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('We can\'t upload the file to current folder right now. Please try another folder.'); + $this->expectExceptionMessage( + 'We can\'t upload the file to current folder right now. Please try another folder.' + ); $fileName = 'magento_small_image.jpg'; - $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); $filePath = $tmpDirectory->getAbsolutePath($fileName); // phpcs:disable $fixtureDir = realpath(__DIR__ . '/../../../../Catalog/_files'); @@ -202,7 +231,7 @@ public function testUploadFileWithWrongExtension(string $fileName, string $fileT $this->expectException(\Magento\Framework\Exception\LocalizedException::class); $this->expectExceptionMessage('File validation failed.'); - $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); $filePath = $tmpDirectory->getAbsolutePath($fileName); // phpcs:disable $fixtureDir = realpath(__DIR__ . '/../../../_files'); @@ -249,7 +278,7 @@ public function testUploadFileWithWrongFile(): void $this->expectExceptionMessage('File validation failed.'); $fileName = 'file.gif'; - $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); $filePath = $tmpDirectory->getAbsolutePath($fileName); // phpcs:disable $file = fopen($filePath, "wb"); @@ -295,6 +324,58 @@ public function testGetThumbnailUrl(string $directory, string $filename, string $this->storage->deleteFile($path); } + /** + * Verify thumbnail generation for diferent sizes + * + * @param array $sizes + * @param bool $resized + * @dataProvider getThumbnailsSizes + */ + public function testResizeFile(array $sizes, bool $resized): void + { + $root = $this->storage->getCmsWysiwygImages()->getStorageRoot(); + $path = $root . '/' . 'testfile.png'; + $this->generateImage($path, $sizes['width'], $sizes['height']); + $this->storage->resizeFile($path); + + $thumbPath = $this->storage->getThumbnailPath($path); + list($imageWidth, $imageHeight) = getimagesize($thumbPath); + + $this->assertEquals( + $resized ? $this->storage->getResizeWidth() : $sizes['width'], + $imageWidth + ); + $this->assertLessThanOrEqual( + $resized ? $this->storage->getResizeHeight() : $sizes['height'], + $imageHeight + ); + + $this->storage->deleteFile($path); + } + + /** + * Provide sizes for resizeFile test + */ + public function getThumbnailsSizes(): array + { + return [ + [ + [ + 'width' => 1024, + 'height' => 768, + ], + true + ], + [ + [ + 'width' => 20, + 'height' => 20, + ], + false + ] + ]; + } + /** * Provide scenarios for testing getThumbnailUrl() * diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/block.php b/dev/tests/integration/testsuite/Magento/Cms/_files/block.php index 070fd9ae2a0b3..4625c1fe3313b 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/block.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/block.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Model\Block; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $block Block + * @var $blockRepository BlockRepositoryInterface + */ +$block = $objectManager->create(Block::class); +$blockRepository = $objectManager->create(BlockRepositoryInterface::class); -/** @var $block \Magento\Cms\Model\Block */ -$block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Cms\Model\Block::class); $block->setTitle( 'CMS Block Title' )->setIdentifier( @@ -20,8 +33,10 @@ 1 )->setStores( [ - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Store\Model\StoreManagerInterface::class + Bootstrap::getObjectManager()->get( + StoreManagerInterface::class )->getStore()->getId() ] -)->save(); +); + +$blockRepository->save($block); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/block_default_store.php b/dev/tests/integration/testsuite/Magento/Cms/_files/block_default_store.php index de4e852f807bc..825103d76ecff 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/block_default_store.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/block_default_store.php @@ -5,12 +5,20 @@ */ declare(strict_types=1); +use Magento\Cms\Api\BlockRepositoryInterface; use Magento\Cms\Model\Block; use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; -/** @var $block Block */ -$block = Bootstrap::getObjectManager()->create(Block::class); +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $block Block + * @var $blockRepository BlockRepositoryInterface + */ +$block = $objectManager->create(Block::class); +$blockRepository = $objectManager->create(BlockRepositoryInterface::class); + $block->setTitle( 'CMS Block Title' )->setIdentifier( @@ -24,4 +32,6 @@ 1 )->setStores( [Store::DEFAULT_STORE_ID] -)->save(); +); + +$blockRepository->save($block); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/home_with_custom_handle.php b/dev/tests/integration/testsuite/Magento/Cms/_files/home_with_custom_handle.php index 2556e0318222d..a4dd0c5fd4e56 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/home_with_custom_handle.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/home_with_custom_handle.php @@ -5,6 +5,7 @@ */ declare(strict_types=1); +use Magento\Cms\Model\ResourceModel\Page as PageResource; use Magento\Cms\Model\Page as PageModel; use Magento\Cms\Model\PageFactory as PageModelFactory; use Magento\TestFramework\Cms\Model\CustomLayoutManager; @@ -20,11 +21,16 @@ $customLayoutName = 'page_custom_layout'; -/** @var PageModel $page */ +/** + * @var PageModel $page + * @var PageResource $pageResource + */ $page = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); -$page->load('home'); +$pageResource = $objectManager->create(PageResource::class); + +$pageResource->load($page, 'home'); $cmsPageId = (int)$page->getId(); $fakeManager->fakeAvailableFiles($cmsPageId, [$customLayoutName]); $page->setData('layout_update_selected', $customLayoutName); -$page->save(); +$pageResource->save($page); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/noroute.php b/dev/tests/integration/testsuite/Magento/Cms/_files/noroute.php index 4c56132a12c01..6fb93a266036c 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/noroute.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/noroute.php @@ -3,6 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Cms\Model\Page::class); -$block->load('no-route', 'identifier'); -$block->setIsActive(0)->save(); + +declare(strict_types=1); + +use Magento\Cms\Model\Page; +use Magento\Cms\Model\ResourceModel\Page as PageResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var Page $page + * @var PageResource $pageResource + */ +$page = $objectManager->create(Page::class); +$pageResource = $objectManager->create(PageResource::class); + +$pageResource->load($page, 'no-route', 'identifier'); +$page->setIsActive(0); +$pageResource->save($page); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php index b2742ecd380f3..3581fdc34f8e5 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php @@ -4,8 +4,21 @@ * See COPYING.txt for license details. */ -/** @var $page \Magento\Cms\Model\Page */ -$page = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Cms\Model\Page::class); +declare(strict_types=1); + +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $page Page + * @var $pageRepository PageRepositoryInterface + */ +$page = $objectManager->create(Page::class); +$pageRepository = $objectManager->create(PageRepositoryInterface::class); + $page->setTitle('Cms Page 100') ->setIdentifier('page100') ->setStores([0]) @@ -15,10 +28,10 @@ ->setMetaTitle('Cms Meta title for page100') ->setMetaKeywords('Cms Meta Keywords for page100') ->setMetaDescription('Cms Meta Description for page100') - ->setPageLayout('1column') - ->save(); + ->setPageLayout('1column'); +$pageRepository->save($page); -$page = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Cms\Model\Page::class); +$page = $objectManager->create(Page::class); $page->setTitle('Cms Page Design Blank') ->setIdentifier('page_design_blank') ->setStores([0]) @@ -29,5 +42,5 @@ ->setMetaKeywords('Cms Meta Keywords for Blank page') ->setMetaDescription('Cms Meta Description for Blank page') ->setPageLayout('1column') - ->setCustomTheme('Magento/blank') - ->save(); + ->setCustomTheme('Magento/blank'); +$pageRepository->save($page); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php index 9734ed3abaeed..0dacb4d5576b0 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php @@ -7,16 +7,27 @@ declare(strict_types=1); use Magento\Cms\Model\Page as PageModel; +use Magento\Cms\Model\ResourceModel\Page as PageResource; use Magento\Cms\Model\PageFactory as PageModelFactory; use Magento\TestFramework\Cms\Model\CustomLayoutManager; use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); +$objectManager->configure([ + 'preferences' => [ + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + ] +]); $pageFactory = $objectManager->get(PageModelFactory::class); + /** @var CustomLayoutManager $fakeManager */ $fakeManager = $objectManager->get(CustomLayoutManager::class); $layoutRepo = $objectManager->create(PageModel\CustomLayoutRepositoryInterface::class, ['manager' => $fakeManager]); +/** @var PageResource $pageRepository */ +$pageResource = $objectManager->create(PageResource::class); + /** @var PageModel $page */ $page = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); $page->setIdentifier('test_custom_layout_page_1'); @@ -25,14 +36,16 @@ $page->setLayoutUpdateXml('<container />'); $page->setIsActive(true); $page->setStoreId(0); -$page->save(); +$pageResource->save($page); + /** @var PageModel $page2 */ $page2 = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); $page2->setIdentifier('test_custom_layout_page_2'); $page2->setTitle('Test Page 2'); $page->setIsActive(true); $page->setStoreId(0); -$page2->save(); +$pageResource->save($page2); + /** @var PageModel $page3 */ $page3 = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); $page3->setIdentifier('test_custom_layout_page_3'); @@ -41,7 +54,7 @@ $page3->setIsActive(1); $page3->setContent('<h1>Test Page</h1>'); $page3->setPageLayout('1column'); -$page3->save(); +$pageResource->save($page3); $fakeManager->fakeAvailableFiles((int)$page3->getId(), ['test_selected']); $page3->setData('layout_update_selected', 'test_selected'); -$page3->save(); +$pageResource->save($page3); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php index 3217b94d7392b..684b1d4356d20 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php @@ -8,25 +8,33 @@ use Magento\Cms\Model\Page as PageModel; use Magento\Cms\Model\PageFactory as PageModelFactory; +use Magento\Cms\Model\ResourceModel\Page as PageResource; use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); $pageFactory = $objectManager->get(PageModelFactory::class); -/** @var PageModel $page */ + +/** + * @var PageModel $page + * @var PageResource $pageResource + */ $page = $pageFactory->create(); -$page->load('test_custom_layout_page_1', PageModel::IDENTIFIER); +$pageResource = $objectManager->create(PageResource::class); +$pageResource->load($page, 'test_custom_layout_page_1', PageModel::IDENTIFIER); if ($page->getId()) { - $page->delete(); + $pageResource->delete($page); } + /** @var PageModel $page2 */ $page2 = $pageFactory->create(); -$page2->load('test_custom_layout_page_2', PageModel::IDENTIFIER); +$pageResource->load($page2, 'test_custom_layout_page_2', PageModel::IDENTIFIER); if ($page2->getId()) { - $page2->delete(); + $pageResource->delete($page2); } + /** @var PageModel $page3 */ $page3 = $pageFactory->create(); -$page3->load('test_custom_layout_page_3', PageModel::IDENTIFIER); +$pageResource->load($page3, 'test_custom_layout_page_3', PageModel::IDENTIFIER); if ($page3->getId()) { - $page3->delete(); + $pageResource->delete($page3); } diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php b/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php index 16e4a4e521fa3..fdb042fbb18fa 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php @@ -5,29 +5,37 @@ */ declare(strict_types=1); +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page; use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store.php'); -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$objectManager = Bootstrap::getObjectManager(); + /** @var StoreRepositoryInterface $storeRepository */ $storeRepository = $objectManager->get(StoreRepositoryInterface::class); $store = $storeRepository->get('fixture_second_store'); -/** @var $page \Magento\Cms\Model\Page */ -$page = $objectManager->create(\Magento\Cms\Model\Page::class); + +/** @var PageRepositoryInterface $pageRepository */ +$pageRepository = $objectManager->create(PageRepositoryInterface::class); + +/** @var $page Page */ +$page = $objectManager->create(Page::class); $page->setTitle('First test page') ->setIdentifier('page1') ->setStores([1]) ->setIsActive(1) - ->setPageLayout('1column') - ->save(); + ->setPageLayout('1column'); +$pageRepository->save($page); -/** @var $page \Magento\Cms\Model\Page */ -$page = $objectManager->create(\Magento\Cms\Model\Page::class); +/** @var $page Page */ +$page = $objectManager->create(Page::class); $page->setTitle('Second test page') ->setIdentifier('page1') ->setStores([$store->getId()]) ->setIsActive(1) - ->setPageLayout('1column') - ->save(); + ->setPageLayout('1column'); +$pageRepository->save($page); diff --git a/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php b/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php index a5934dd98e2a6..660d59f3264ec 100644 --- a/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php +++ b/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php @@ -7,19 +7,30 @@ namespace Magento\CmsUrlRewrite\Plugin\Cms\Model\Store; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator; +use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\ObjectManagerInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; +use PHPUnit\Framework\TestCase; /** - * Test for plugin which is listening store resource model and on save replace cms page url rewrites + * Test for plugin which is listening store resource model and on save replace cms page url rewrites. * * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ViewTest extends \PHPUnit\Framework\TestCase +class ViewTest extends TestCase { /** * @var UrlFinderInterface @@ -36,6 +47,26 @@ class ViewTest extends \PHPUnit\Framework\TestCase */ private $storeFactory; + /** + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var UrlRewriteFactory + */ + private $urlRewriteFactory; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var CmsPageUrlPathGenerator + */ + private $cmsPageUrlPathGenerator; + /** * @inheritdoc */ @@ -44,31 +75,40 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->urlFinder = $this->objectManager->create(UrlFinderInterface::class); $this->storeFactory = $this->objectManager->create(StoreFactory::class); + $this->urlPersist = $this->objectManager->create(UrlPersistInterface::class); + $this->urlRewriteFactory = $this->objectManager->create(UrlRewriteFactory::class); + $this->pageRepository = $this->objectManager->create(PageRepositoryInterface::class); + $this->cmsPageUrlPathGenerator = $this->objectManager->create(CmsPageUrlPathGenerator::class); } /** * Test of replacing cms page url rewrites on create and delete store * + * @magentoDataFixture Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php * @magentoDataFixture Magento/Cms/_files/pages.php */ - public function testUrlRewritesChangesAfterStoreSave() + public function testUrlRewritesChangesAfterStoreSave(): void { $storeId = $this->createStore(); - $this->assertUrlRewritesCount($storeId, 1); + $this->assertUrlRewritesCount($storeId, 'page100', 1); + $this->editUrlRewrite($storeId, 'page100'); + $this->saveStore($storeId); + $this->assertUrlRewritesCount($storeId, 'page100-test', 1); $this->deleteStore($storeId); - $this->assertUrlRewritesCount($storeId, 0); + $this->assertUrlRewritesCount($storeId, 'page100', 0); } /** - * Assert url rewrites count by store id + * Assert url rewrites count by store id and request path * * @param int $storeId + * @param string $requestPath * @param int $expectedCount */ - private function assertUrlRewritesCount(int $storeId, int $expectedCount): void + private function assertUrlRewritesCount(int $storeId, string $requestPath, int $expectedCount): void { $data = [ - UrlRewrite::REQUEST_PATH => 'page100', + UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::STORE_ID => $storeId ]; $urlRewrites = $this->urlFinder->findAllByData($data); @@ -77,8 +117,6 @@ private function assertUrlRewritesCount(int $storeId, int $expectedCount): void /** * Create test store - * - * @return int */ private function createStore(): int { @@ -95,7 +133,6 @@ private function createStore(): int * Delete test store * * @param int $storeId - * @return void */ private function deleteStore(int $storeId): void { @@ -105,4 +142,49 @@ private function deleteStore(int $storeId): void $store->delete(); } } + + /** + * Edit url rewrite + * + * @param int $storeId + * @param string $pageIdentifier + */ + private function editUrlRewrite(int $storeId, string $pageIdentifier): void + { + $filter = $this->objectManager->create(Filter::class); + $filter->setField('identifier')->setValue($pageIdentifier); + $filterGroup = $this->objectManager->create(FilterGroup::class); + $filterGroup->setFilters([$filter]); + $searchCriteria = $this->objectManager->create(SearchCriteriaInterface::class); + $searchCriteria->setFilterGroups([$filterGroup]); + $pageSearchResults = $this->pageRepository->getList($searchCriteria); + $pages = $pageSearchResults->getItems(); + /** @var PageInterface $page */ + $cmsPage = array_values($pages)[0]; + + $urlRewrite = $this->urlRewriteFactory->create()->setStoreId($storeId) + ->setEntityType(CmsPageUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($cmsPage->getId()) + ->setRequestPath($cmsPage->getIdentifier() . '-test') + ->setTargetPath($this->cmsPageUrlPathGenerator->getCanonicalUrlPath($cmsPage)) + ->setIsAutogenerated(0) + ->setRedirectType(0); + + $this->urlPersist->replace([$urlRewrite]); + } + + /** + * Edit test store + * + * @param int $storeId + * @return void + */ + private function saveStore(int $storeId): void + { + $store = $this->storeFactory->create(); + $store->load($storeId); + if ($store !== null) { + $store->save(); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php index e7f714250f2c8..4906014ad1903 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php @@ -6,24 +6,23 @@ namespace Magento\Config\Console\Command; +use Magento\Config\Model\Config\Structure; use Magento\Framework\App\DeploymentConfig\FileReader; use Magento\Framework\App\DeploymentConfig\Writer; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Console\Cli; use Magento\Framework\Filesystem; -use Magento\Framework\ObjectManagerInterface; use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; -class ConfigShowCommandTest extends \PHPUnit\Framework\TestCase +/** + * Test for \Magento\Config\Console\Command\ConfigShowCommand. + */ +class ConfigShowCommandTest extends TestCase { - /** - * @var ObjectManagerInterface - */ - private $objectManager; - /** * @var CommandTester */ @@ -64,16 +63,22 @@ class ConfigShowCommandTest extends \PHPUnit\Framework\TestCase */ private $envConfig; + /** + * @var Structure + */ + private $structure; + /** * @inheritdoc */ protected function setUp(): void { - $this->objectManager = Bootstrap::getObjectManager(); - $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); - $this->filesystem = $this->objectManager->get(Filesystem::class); - $this->reader = $this->objectManager->get(FileReader::class); - $this->writer = $this->objectManager->get(Writer::class); + $objectManager = Bootstrap::getObjectManager(); + $this->configFilePool = $objectManager->get(ConfigFilePool::class); + $this->filesystem = $objectManager->get(Filesystem::class); + $this->reader = $objectManager->get(FileReader::class); + $this->writer = $objectManager->get(Writer::class); + $this->structure = $objectManager->get(Structure::class); $this->config = $this->loadConfig(); $this->envConfig = $this->loadEnvConfig(); @@ -89,21 +94,27 @@ protected function setUp(): void $_ENV['CONFIG__WEBSITES__BASE__WEB__TEST2__TEST_VALUE_4'] = 'value4.env.website_base.test'; $_ENV['CONFIG__STORES__DEFAULT__WEB__TEST2__TEST_VALUE_4'] = 'value4.env.store_default.test'; - $command = $this->objectManager->create(ConfigShowCommand::class); + $command = $objectManager->create(ConfigShowCommand::class); $this->commandTester = new CommandTester($command); } /** + * Test execute config show command + * * @param string $scope * @param string $scopeCode * @param int $resultCode * @param array $configs + * @return void + * * @magentoDbIsolation enabled * @magentoDataFixture Magento/Config/_files/config_data.php * @dataProvider executeDataProvider */ - public function testExecute($scope, $scopeCode, $resultCode, array $configs) + public function testExecute($scope, $scopeCode, $resultCode, array $configs): void { + $this->setConfigPaths(); + foreach ($configs as $inputPath => $configValue) { $arguments = [ ConfigShowCommand::INPUT_ARGUMENT_PATH => $inputPath @@ -130,6 +141,41 @@ public function testExecute($scope, $scopeCode, $resultCode, array $configs) } } + /** + * Set config paths to structure + * + * @return void + */ + private function setConfigPaths(): void + { + $reflection = new \ReflectionClass(Structure::class); + $mappedPaths = $reflection->getProperty('mappedPaths'); + $mappedPaths->setAccessible(true); + $mappedPaths->setValue($this->structure, $this->getConfigPaths()); + } + + /** + * Returns config paths + * + * @return array + */ + private function getConfigPaths(): array + { + $configs = [ + 'web/test/test_value_1', + 'web/test/test_value_2', + 'web/test2/test_value_3', + 'web/test2/test_value_4', + 'carriers/fedex/account', + 'paypal/fetch_reports/ftp_password', + 'web/test', + 'web/test2', + 'web', + ]; + + return array_flip($configs); + } + /** * @return array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -240,7 +286,7 @@ public function executeDataProvider() Cli::RETURN_FAILURE, [ 'web/test/test_wrong_value' => [ - 'Configuration for path: "web/test/test_wrong_value" doesn\'t exist' + 'The "web/test/test_wrong_value" path doesn\'t exist. Verify and try again.' ], ] ], @@ -250,7 +296,7 @@ public function executeDataProvider() Cli::RETURN_FAILURE, [ 'web/test/test_wrong_value' => [ - 'Configuration for path: "web/test/test_wrong_value" doesn\'t exist' + 'The "web/test/test_wrong_value" path doesn\'t exist. Verify and try again.' ], ] ], diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php index 1b7a504959d54..eedb93099b8c3 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php @@ -5,12 +5,18 @@ */ namespace Magento\Config\Model; +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Config\Model\ResourceModel\Config\Data\Collection; +use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Encryption\EncryptorInterface; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** * @magentoAppArea adminhtml */ -class ConfigTest extends \PHPUnit\Framework\TestCase +class ConfigTest extends TestCase { /** * @covers \Magento\Config\Model\Config::save @@ -22,25 +28,25 @@ class ConfigTest extends \PHPUnit\Framework\TestCase public function testSaveWithSingleStoreModeEnabled($groups) { Bootstrap::getObjectManager()->get( - \Magento\Framework\Config\ScopeInterface::class + ScopeInterface::class )->setCurrentScope( - \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE + FrontNameResolver::AREA_CODE ); - /** @var $_configDataObject \Magento\Config\Model\Config */ - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + /** @var $_configDataObject Config */ + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configData = $_configDataObject->setSection('dev')->setWebsite('base')->load(); $this->assertEmpty($_configData); - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configDataObject->setSection('dev')->setGroups($groups)->save(); - /** @var $_configDataObject \Magento\Config\Model\Config */ - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + /** @var $_configDataObject Config */ + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configData = $_configDataObject->setSection('dev')->load(); $this->assertArrayHasKey('dev/debug/template_hints_admin', $_configData); $this->assertArrayHasKey('dev/debug/template_hints_blocks', $_configData); - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configData = $_configDataObject->setSection('dev')->setWebsite('base')->load(); $this->assertArrayNotHasKey('dev/debug/template_hints_admin', $_configData); $this->assertArrayNotHasKey('dev/debug/template_hints_blocks', $_configData); @@ -63,16 +69,16 @@ public function testSave($section, $groups, $expected) { $objectManager = Bootstrap::getObjectManager(); - /** @var $_configDataObject \Magento\Config\Model\Config */ - $_configDataObject = $objectManager->create(\Magento\Config\Model\Config::class); + /** @var $_configDataObject Config */ + $_configDataObject = $objectManager->create(Config::class); $_configDataObject->setSection($section)->setWebsite('base')->setGroups($groups)->save(); foreach ($expected as $group => $expectedData) { - $_configDataObject = $objectManager->create(\Magento\Config\Model\Config::class); + $_configDataObject = $objectManager->create(Config::class); $_configData = $_configDataObject->setSection($group)->setWebsite('base')->load(); if (array_key_exists('payment/payflow_link/pwd', $_configData)) { $_configData['payment/payflow_link/pwd'] = $objectManager->get( - \Magento\Framework\Encryption\EncryptorInterface::class + EncryptorInterface::class )->decrypt( $_configData['payment/payflow_link/pwd'] ); @@ -85,4 +91,102 @@ public function saveDataProvider() { return require __DIR__ . '/_files/config_section.php'; } + + /** + * @param string $website + * @param string $section + * @param array $override + * @param array $inherit + * @param array $expected + * @dataProvider saveWebsiteScopeDataProvider + */ + public function testSaveUseDefault( + string $website, + string $section, + array $override, + array $inherit, + array $expected + ): void { + $objectManager = Bootstrap::getObjectManager(); + /** @var Config $config*/ + $configFactory = $objectManager->create(ConfigFactory::class); + $config = $configFactory->create() + ->setSection($section) + ->setWebsite($website) + ->setGroups($override['groups']) + ->save(); + + $paths = array_keys($expected); + + $this->assertEquals( + $expected, + $this->getConfigValues($config->getScope(), $config->getScopeId(), $paths) + ); + + $config = $configFactory->create() + ->setSection($section) + ->setWebsite($website) + ->setGroups($inherit['groups']) + ->save(); + + $this->assertEmpty( + $this->getConfigValues($config->getScope(), $config->getScopeId(), $paths) + ); + } + + /** + * @return array + */ + public function saveWebsiteScopeDataProvider(): array + { + return [ + [ + 'website' => 'base', + 'section' => 'payment', + [ + 'groups' => [ + 'account' => [ + 'fields' => [ + 'merchant_country' => ['value' => 'GB'], + ], + ], + ] + ], + [ + 'groups' => [ + 'account' => [ + 'fields' => [ + 'merchant_country' => ['inherit' => 1], + ], + ], + ], + ], + 'expected' => [ + 'paypal/general/merchant_country' => 'GB', + ], + ] + ]; + } + + /** + * @param string $scope + * @param int $scopeId + * @param array $paths + * @return array + */ + private function getConfigValues(string $scope, int $scopeId, array $paths): array + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Collection $configCollection */ + $configCollectionFactory = $objectManager->create(CollectionFactory::class); + $configCollection = $configCollectionFactory->create(); + $configCollection->addFieldToFilter('scope', $scope); + $configCollection->addFieldToFilter('scope_id', $scopeId); + $configCollection->addFieldToFilter('path', ['in' => $paths]); + $result = []; + foreach ($configCollection as $data) { + $result[$data->getPath()] = $data->getValue(); + } + return $result; + } } 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/Adminhtml/Product/Steps/AttributeValuesTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/AttributeValuesTest.php new file mode 100644 index 0000000000000..b0a1c81857221 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/AttributeValuesTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps; + +use Magento\Backend\Model\Auth\Session; +use Magento\ConfigurableProduct\Block\DataProviders\PermissionsData; +use Magento\Framework\View\Layout; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class AttributeValuesTest extends TestCase +{ + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php + */ + public function testRestrictedUserNotAllowedToManageAttributes() + { + $user = Bootstrap::getObjectManager()->create( + User::class + )->loadByUsername( + 'admincatalog_user' + ); + + /** @var $session Session */ + $session = Bootstrap::getObjectManager()->get( + Session::class + ); + $session->setUser($user); + + /** @var $layout Layout */ + $layout = Bootstrap::getObjectManager()->get( + LayoutInterface::class + ); + + /** @var \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\AttributeValues */ + $block = $layout->createBlock( + AttributeValues::class, + 'step2', + [ + 'data' => [ + 'config' => [ + 'form' => 'product_form.product_form', + 'modal' => 'configurableModal', + 'dataScope' => 'productFormConfigurable', + ], + 'permissions' => Bootstrap::getObjectManager()->get(PermissionsData::class) + ] + ] + ); + $isAllowedToManageAttributes = $block->getPermissions()->isAllowedToManageAttributes(); + $html = $block->toHtml(); + $this->assertFalse($isAllowedToManageAttributes); + $this->assertStringNotContainsString('<button class="action-create-new action-tertiary"', $html); + } +} 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/ConfigurableProductPriceTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableProductPriceTest.php index 327544911a45d..325ab7db23aaf 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableProductPriceTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableProductPriceTest.php @@ -14,13 +14,16 @@ use Magento\Framework\Registry; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; use PHPUnit\Framework\TestCase; /** * Check configurable product price displaying * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppIsolation enabled * @magentoAppArea frontend */ @@ -44,6 +47,12 @@ class ConfigurableProductPriceTest extends TestCase /** @var SerializerInterface */ private $json; + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + /** * @inheritdoc */ @@ -53,11 +62,13 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->registry = $this->objectManager->get(Registry::class); - $this->page = $this->objectManager->get(Page::class); + $this->page = $this->objectManager->get(PageFactory::class)->create(); $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $this->productRepository->cleanCache(); $this->productCustomOption = $this->objectManager->get(ProductCustomOptionInterface::class); $this->json = $this->objectManager->get(SerializerInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); } /** @@ -78,7 +89,20 @@ protected function tearDown(): void */ public function testConfigurablePrice(): void { - $this->assertPrice($this->processPriceView('configurable'), 10.00); + $this->assertPrice('configurable', 10.00); + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testConfigurablePriceOnSecondWebsite(): void + { + $this->executeInStoreContext->execute('fixture_second_store', [$this, 'assertPrice'], 'configurable', 10.00); + $this->resetPageLayout(); + $this->assertPrice('configurable', 150.00); } /** @@ -88,7 +112,7 @@ public function testConfigurablePrice(): void */ public function testConfigurablePriceWithDisabledFirstChild(): void { - $this->assertPrice($this->processPriceView('configurable'), 20.00); + $this->assertPrice('configurable', 20.00); } /** @@ -98,7 +122,7 @@ public function testConfigurablePriceWithDisabledFirstChild(): void */ public function testConfigurablePriceWithOutOfStockFirstChild(): void { - $this->assertPrice($this->processPriceView('configurable'), 20.00); + $this->assertPrice('configurable', 20.00); } /** @@ -110,7 +134,7 @@ public function testConfigurablePriceWithOutOfStockFirstChild(): void */ public function testConfigurablePriceWithCatalogRule(): void { - $this->assertPrice($this->processPriceView('configurable'), 9.00); + $this->assertPrice('configurable', 9.00); } /** @@ -120,7 +144,7 @@ public function testConfigurablePriceWithCatalogRule(): void */ public function testConfigurablePriceWithCustomOption(): void { - $product = $this->productRepository->get('configurable'); + $product = $this->getProduct('configurable'); $this->registerProduct($product); $this->preparePageLayout(); $customOptionsBlock = $this->page->getLayout() @@ -162,6 +186,16 @@ private function preparePageLayout(): void $this->page->getLayout()->generateXml(); } + /** + * Reset layout page to get new block html. + * + * @return void + */ + private function resetPageLayout(): void + { + $this->page = $this->objectManager->get(PageFactory::class)->create(); + } + /** * Process view product final price block html. * @@ -170,7 +204,7 @@ private function preparePageLayout(): void */ private function processPriceView(string $sku): string { - $product = $this->productRepository->get($sku); + $product = $this->getProduct($sku); $this->registerProduct($product); $this->preparePageLayout(); @@ -180,12 +214,13 @@ private function processPriceView(string $sku): string /** * Assert that html contain price label and expected final price amount. * - * @param string $priceBlockHtml + * @param string $sku * @param float $expectedPrice * @return void */ - private function assertPrice(string $priceBlockHtml, float $expectedPrice): void + public function assertPrice(string $sku, float $expectedPrice): void { + $priceBlockHtml = $this->processPriceView($sku); $regexp = '/<span class="price-label">As low as<\/span>.*'; $regexp .= '<span.*data-price-amount="%s".*<span class="price">\$%.2f<\/span><\/span>/'; $this->assertMatchesRegularExpression( @@ -208,4 +243,15 @@ private function assertJsonConfig(string $config, string $expectedPrice, int $op $this->assertNotNull($price); $this->assertEquals($expectedPrice, $price); } + + /** + * Loads product by sku.s + * + * @param string $sku + * @return ProductInterface + */ + private function getProduct(string $sku): ProductInterface + { + return $this->productRepository->get($sku, false, $this->storeManager->getStore()->getId(), true); + } } 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/ConfigurableProduct/Block/Product/View/Type/ConfigurableViewOnCategoryPageTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableViewOnCategoryPageTest.php index d577994cdc45b..bf910359b893b 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableViewOnCategoryPageTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableViewOnCategoryPageTest.php @@ -7,11 +7,18 @@ namespace Magento\ConfigurableProduct\Block\Product\View\Type; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Product\ListProduct; use Magento\Eav\Model\Entity\Collection\AbstractCollection; use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\View\LayoutInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; use PHPUnit\Framework\TestCase; /** @@ -21,17 +28,30 @@ * @magentoAppIsolation enabled * @magentoAppArea frontend * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_out_of_stock_children.php + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ConfigurableViewOnCategoryPageTest extends TestCase { /** @var ObjectManagerInterface */ private $objectManager; - /** @var LayoutInterface */ - private $layout; + /** @var ProductRepositoryInterface */ + private $productRepository; - /** @var ListProduct $listingBlock */ - private $listingBlock; + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var Page */ + private $page; + + /** @var Registry */ + private $registry; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; /** * @inheritdoc @@ -41,9 +61,22 @@ protected function setUp(): void parent::setUp(); $this->objectManager = Bootstrap::getObjectManager(); - $this->layout = $this->objectManager->get(LayoutInterface::class); - $this->listingBlock = $this->layout->createBlock(ListProduct::class); - $this->listingBlock->setCategoryId(333); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $this->page = $this->objectManager->get(PageFactory::class)->create(); + $this->registry = $this->objectManager->get(Registry::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_category'); + parent::tearDown(); } /** @@ -53,8 +86,8 @@ protected function setUp(): void */ public function testOutOfStockProductWithEnabledConfigView(): void { - $collection = $this->listingBlock->getLoadedProductCollection(); - $this->assertCollectionSize(1, $collection); + $this->preparePageLayout(); + $this->assertCollectionSize(1, $this->getListingBlock()->getLoadedProductCollection()); } /** @@ -64,8 +97,50 @@ public function testOutOfStockProductWithEnabledConfigView(): void */ public function testOutOfStockProductWithDisabledConfigView(): void { - $collection = $this->listingBlock->getLoadedProductCollection(); - $this->assertCollectionSize(0, $collection); + $this->preparePageLayout(); + $this->assertCollectionSize(0, $this->getListingBlock()->getLoadedProductCollection()); + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_category.php + * + * @return void + */ + public function testCheckConfigurablePrice(): void + { + $this->assertProductPrice('configurable', 'As low as $10.00'); + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php + * + * @return void + */ + public function testCheckConfigurablePriceOnSecondWebsite(): void + { + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertProductPrice'], + 'configurable', + __('As low as') . ' $10.00' + ); + $this->resetPageLayout(); + $this->assertProductPrice('configurable', __('As low as') . ' $150.00'); + } + + /** + * Checks product price. + * + * @param string $sku + * @param string $priceString + * @return void + */ + public function assertProductPrice(string $sku, string $priceString): void + { + $this->preparePageLayout(); + $this->assertCollectionSize(1, $this->getListingBlock()->getLoadedProductCollection()); + $priceHtml = $this->getListingBlock()->getProductPrice($this->getProduct($sku)); + $this->assertEquals($priceString, $this->clearPriceHtml($priceHtml)); } /** @@ -80,4 +155,64 @@ private function assertCollectionSize(int $expectedSize, AbstractCollection $col $this->assertEquals($expectedSize, $collection->getSize()); $this->assertCount($expectedSize, $collection->getItems()); } + + /** + * Prepare category page. + * + * @return void + */ + private function preparePageLayout(): void + { + $this->registry->unregister('current_category'); + $this->registry->register( + 'current_category', + $this->categoryRepository->get(333, $this->storeManager->getStore()->getId()) + ); + $this->page->addHandle(['default', 'catalog_category_view']); + $this->page->getLayout()->generateXml(); + } + + /** + * Reset layout page to get new block html. + * + * @return void + */ + private function resetPageLayout(): void + { + $this->page = $this->objectManager->get(PageFactory::class)->create(); + } + + /** + * Removes html tags and spaces from price html string. + * + * @param string $priceHtml + * @return string + */ + private function clearPriceHtml(string $priceHtml): string + { + return trim(preg_replace('/\s+/', ' ', strip_tags($priceHtml))); + } + + /** + * Returns product list block. + * + * @return null|ListProduct + */ + private function getListingBlock(): ?ListProduct + { + $block = $this->page->getLayout()->getBlock('category.products.list'); + + return $block ? $block : null; + } + + /** + * Loads product by sku. + * + * @param string $sku + * @return ProductInterface + */ + private function getProduct(string $sku): ProductInterface + { + return $this->productRepository->get($sku, false, $this->storeManager->getStore()->getId(), true); + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/MultiStoreConfigurableViewOnProductPageTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/MultiStoreConfigurableViewOnProductPageTest.php index 3a6052da3964f..cd206ec8ec273 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/MultiStoreConfigurableViewOnProductPageTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/MultiStoreConfigurableViewOnProductPageTest.php @@ -184,7 +184,10 @@ private function prepareConfigurableProduct(string $sku, string $storeCode): voi { $product = $this->productRepository->get($sku, false, null, true); $productToUpdate = $product->getTypeInstance()->getUsedProductCollection($product) - ->setPageSize(1)->getFirstItem(); + ->addStoreFilter($storeCode) + ->setPageSize(1) + ->getFirstItem(); + $this->assertNotEmpty($productToUpdate->getData(), 'Configurable product does not have a child'); $this->executeInStoreContext->execute($storeCode, [$this, 'setProductDisabled'], $productToUpdate); } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/MassDeleteTest.php new file mode 100644 index 0000000000000..4f003e26db43f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/MassDeleteTest.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product; + +use Magento\Catalog\Controller\Adminhtml\Product\MassDeleteTest as CatalogMassDeleteTest; + +/** + * Test for mass configurable product deleting. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class MassDeleteTest extends CatalogMassDeleteTest +{ + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_one_simple.php + * + * @return void + */ + public function testDeleteConfigurableProductViaMassAction(): void + { + $product = $this->productRepository->get('configurable'); + $this->dispatchMassDeleteAction([$product->getId()]); + $this->assertSuccessfulDeleteProducts(1); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php deleted file mode 100644 index 1fffd701c509f..0000000000000 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; - -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\Interception\PluginList; -use PHPUnit\Framework\TestCase; - -/** - * Test configurable fronted product plugin will add children products ids to configurable product identities. - */ -class ProductIdentitiesExtenderTest extends TestCase -{ - /** - * Check, product identities extender plugin is registered for storefront. - * - * @magentoAppArea frontend - * @return void - */ - public function testIdentitiesExtenderIsRegistered(): void - { - $pluginInfo = Bootstrap::getObjectManager()->get(PluginList::class) - ->get(\Magento\Catalog\Model\Product::class, []); - $this->assertSame(ProductIdentitiesExtender::class, $pluginInfo['product_identities_extender']['instance']); - } - - /** - * Check plugin will add children ids to configurable product identities on storefront. - * - * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php - * @magentoAppArea frontend - * @return void - */ - public function testGetIdentitiesForConfigurableProductOnStorefront(): void - { - $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); - $configurableProduct = $productRepository->get('configurable'); - $simpleProduct1 = $productRepository->get('simple_10'); - $simpleProduct2 = $productRepository->get('simple_20'); - $expectedIdentities = [ - 'cat_p_' . $configurableProduct->getId(), - 'cat_p', - 'cat_p_' . $simpleProduct1->getId(), - 'cat_p_' . $simpleProduct2->getId(), - - ]; - $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); - } - - /** - * Check plugin won't add children ids to configurable product identities in admin area. - * - * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php - * @magentoAppArea adminhtml - * @return void - */ - public function testGetIdentitiesForConfigurableProductInAdminArea(): void - { - $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); - $configurableProduct = $productRepository->get('configurable'); - $expectedIdentities = [ - 'cat_p_' . $configurableProduct->getId(), - ]; - $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); - } -} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php index 52bb7a2f8ab6d..2ffba24e465e0 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php @@ -13,14 +13,17 @@ use Magento\Customer\Model\Group; use Magento\Framework\ObjectManagerInterface; use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Catalog\Model\Product\Price\GetPriceIndexDataByProductId; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; use PHPUnit\Framework\TestCase; /** * Provides tests for configurable product pricing. * * @magentoDbIsolation disabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PriceTest extends TestCase { @@ -49,6 +52,16 @@ class PriceTest extends TestCase */ private $websiteRepository; + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var ExecuteInStoreContext + */ + private $executeInStoreContext; + /** * @inheritdoc */ @@ -60,6 +73,8 @@ protected function setUp(): void $this->productRepository->cleanCache(); $this->getPriceIndexDataByProductId = $this->objectManager->get(GetPriceIndexDataByProductId::class); $this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); } /** @@ -84,6 +99,46 @@ public function testGetFinalPrice(): void ); } + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php + * @return void + */ + public function testGetFinalPriceOnSecondWebsite(): void + { + $this->executeInStoreContext->execute('fixture_second_store', [$this, 'assertPrice'], 10); + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertIndexTableData'], + 'configurable', + ['price' => 0, 'final_price' => 0, 'min_price' => 10, 'max_price' => 30, 'tier_price' => null] + ); + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertIndexTableData'], + 'simple_option_1', + ['price' => 20, 'final_price' => 10, 'min_price' => 10, 'max_price' => 10, 'tier_price' => null] + ); + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertIndexTableData'], + 'simple_option_2', + ['price' => 40, 'final_price' => 30, 'min_price' => 30, 'max_price' => 30, 'tier_price' => null] + ); + $this->assertPrice(150); + $this->assertIndexTableData( + 'configurable', + ['price' => 0, 'final_price' => 0, 'min_price' => 150, 'max_price' => 150, 'tier_price' => null] + ); + $this->assertIndexTableData( + 'simple_option_1', + ['price' => 150, 'final_price' => 150, 'min_price' => 150, 'max_price' => 150, 'tier_price' => null] + ); + $this->assertIndexTableData( + 'simple_option_2', + ['price' => 150, 'final_price' => 150, 'min_price' => 150, 'max_price' => 150, 'tier_price' => null] + ); + } + /** * @magentoConfigFixture current_store tax/display/type 1 * @magentoDataFixture Magento/ConfigurableProduct/_files/tax_rule.php @@ -127,7 +182,7 @@ public function testGetFinalPriceIncludingExcludingTax(): void public function testGetFinalPriceWithSelectedSimpleProduct(): void { $product = $this->productRepository->get('configurable'); - $product->addCustomOption('simple_product', 20, $this->productRepository->get('simple_20')); + $product->addCustomOption('simple_product', 20, $this->getProduct('simple_20')); $this->assertPrice(20, $product); } @@ -137,7 +192,7 @@ public function testGetFinalPriceWithSelectedSimpleProduct(): void */ public function testGetFinalPriceWithCustomOptionAndSimpleTierPrice(): void { - $configurable = $this->productRepository->get('configurable'); + $configurable = $this->getProduct('configurable'); $this->assertIndexTableData( 'configurable', ['price' => 0, 'final_price' => 0, 'min_price' => 9, 'max_price' => 30, 'tier_price' => 15] @@ -167,12 +222,12 @@ public function testGetFinalPriceWithCustomOptionAndSimpleTierPrice(): void * @param array $expectedPrices * @return void */ - private function assertIndexTableData(string $sku, array $expectedPrices): void + public function assertIndexTableData(string $sku, array $expectedPrices): void { $data = $this->getPriceIndexDataByProductId->execute( - (int)$this->productRepository->get($sku)->getId(), + (int)$this->getProduct($sku)->getId(), Group::NOT_LOGGED_IN_ID, - (int)$this->websiteRepository->get('base')->getId() + (int)$this->storeManager->getStore()->getWebsiteId() ); $data = reset($data); foreach ($expectedPrices as $column => $price) { @@ -187,13 +242,24 @@ private function assertIndexTableData(string $sku, array $expectedPrices): void * @param ProductInterface|null $product * @return void */ - private function assertPrice(float $expectedPrice, ?ProductInterface $product = null): void + public function assertPrice(float $expectedPrice, ?ProductInterface $product = null): void { - $product = $product ?: $this->productRepository->get('configurable'); + $product = $product ?: $this->getProduct('configurable'); // final price is the lowest price of configurable variations $this->assertEquals( round($expectedPrice, 2), round($this->priceModel->getFinalPrice(1, $product), 2) ); } + + /** + * Loads product by sku. + * + * @param string $sku + * @return ProductInterface + */ + private function getProduct(string $sku): ProductInterface + { + return $this->productRepository->get($sku, false, $this->storeManager->getStore()->getId(), true); + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ProductTest.php index 223c9fbe708e2..c59818aca5191 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ProductTest.php @@ -13,16 +13,37 @@ class ProductTest extends \PHPUnit\Framework\TestCase { /** + * Check that no children identities are added to the parent product in frontend area + * * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea frontend + * @return void */ - public function testGetIdentities() + public function testGetIdentitiesForConfigurableProductOnStorefront(): void { $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); - $confProduct = $productRepository->get('configurable'); - $simple10Product = $productRepository->get('simple_10'); - $simple20Product = $productRepository->get('simple_20'); + $configurableProduct = $productRepository->get('configurable'); + $expectedIdentities = [ + 'cat_p_' . $configurableProduct->getId(), + 'cat_p' + ]; + $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); + } - $this->assertEmpty(array_diff($confProduct->getIdentities(), $simple10Product->getIdentities())); - $this->assertEmpty(array_diff($confProduct->getIdentities(), $simple20Product->getIdentities())); + /** + * Check that no children identities are added to the parent product in frontend area + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea adminhtml + * @return void + */ + public function testGetIdentitiesForConfigurableProductInAdminArea(): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $configurableProduct = $productRepository->get('configurable'); + $expectedIdentities = [ + 'cat_p_' . $configurableProduct->getId(), + ]; + $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php index 32eddb28151a7..ffa84ca740e62 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php @@ -6,6 +6,7 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; @@ -18,7 +19,7 @@ use Magento\Catalog\Api\Data\ProductInterface; /** - * Configurable test + * Test reindex of configurable products * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoAppArea adminhtml @@ -64,7 +65,7 @@ protected function setUp(): void */ public function testGetProductFinalPriceIfOneOfChildIsDisabled(): void { - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(10, $configurableProduct->getMinimalPrice()); $childProduct = $this->productRepository->getById(10, false, null, true); @@ -75,7 +76,7 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabled(): void $this->productRepository->save($childProduct); $this->storeManager->setCurrentStore($currentStoreId); - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(20, $configurableProduct->getMinimalPrice()); } @@ -93,7 +94,7 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabled(): void */ public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore(): void { - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(10, $configurableProduct->getMinimalPrice()); $childProduct = $this->productRepository->get('simple_10', false, null, true); @@ -106,7 +107,7 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore(): void $this->productRepository->save($childProduct); $this->storeManager->setCurrentStore($currentStoreId); - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(20, $configurableProduct->getMinimalPrice()); } @@ -122,7 +123,7 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore(): void */ public function testGetProductMinimalPriceIfOneOfChildIsOutOfStock(): void { - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(10, $configurableProduct->getMinimalPrice()); $childProduct = $this->productRepository->getById(10, false, null, true); @@ -130,25 +131,48 @@ public function testGetProductMinimalPriceIfOneOfChildIsOutOfStock(): void $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); $this->stockRepository->save($stockItem); - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(20, $configurableProduct->getMinimalPrice()); } + /** + * @magentoDataFixture Magento/Catalog/_files/enable_price_index_schedule.php + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testReindexWithCorrectPriority() + { + $configurableProduct = $this->productRepository->get('configurable'); + $childProduct1 = $this->productRepository->get('simple_1'); + $childProduct2 = $this->productRepository->get('simple_2'); + $priceIndexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); + $priceIndexerProcessor->reindexList( + [$configurableProduct->getId(), $childProduct1->getId(), $childProduct2->getId()], + true + ); + + $configurableProduct = $this->getConfigurableProductFromCollection($configurableProduct->getId()); + $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); + } + /** * Retrieve configurable product. * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php * fixture * + * @param int $productId * @return ProductInterface */ - private function getConfigurableProductFromCollection(): ProductInterface + private function getConfigurableProductFromCollection(int $productId): ProductInterface { /** @var Collection $collection */ $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) ->create(); /** @var ProductInterface $configurableProduct */ $configurableProduct = $collection - ->addIdFilter([1]) + ->addIdFilter([$productId]) ->addMinimalPrice() ->load() ->getFirstItem(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first.php new file mode 100644 index 0000000000000..d882d6058f50c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$firstAttribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_first'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$firstAttribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $firstAttribute->setData( + [ + 'attribute_code' => 'test_configurable_first', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable First'], + 'backend_type' => 'int', + 'option' => [ + 'value' => [ + 'first_option_0' => ['First Option 1'], + 'first_option_1' => ['First Option 2'], + 'first_option_2' => ['First Option 3'], + 'first_option_3' => ['First Option 4'] + ], + 'order' => ['first_option_0' => 1, 'first_option_1' => 2, 'first_option_2' => 3, 'first_option_3' => 4], + ], + ] + ); + + $attributeRepository->save($firstAttribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $firstAttribute->getId()); +} + +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first_rollback.php new file mode 100644 index 0000000000000..9ce9b543c5485 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_first'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_second.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_second.php new file mode 100644 index 0000000000000..45192beef84bf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_second.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$secondAttribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_second'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$secondAttribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $secondAttribute->setData( + [ + 'attribute_code' => 'test_configurable_second', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable Second'], + 'backend_type' => 'int', + 'option' => [ + 'value' => [ + 'second_option_0' => ['Second Option 1'], + 'second_option_1' => ['Second Option 2'], + 'second_option_2' => ['Second Option 3'], + 'second_option_3' => ['Second Option 4'] + ], + 'order' => [ + 'second_option_0' => 1, + 'second_option_1' => 2, + 'second_option_2' => 3, + 'second_option_3' => 4 + ], + ], + ] + ); + + $attributeRepository->save($secondAttribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $secondAttribute->getId()); +} + +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_second_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_second_rollback.php new file mode 100644 index 0000000000000..6d7a8f1fe88e5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_second_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_second'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php new file mode 100644 index 0000000000000..a0edde0becd9e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductExtensionFactory; +use Magento\Catalog\Helper\Data; +use Magento\Catalog\Helper\DefaultCategory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange; +use Magento\Config\Model\ResourceModel\Config; +use Magento\ConfigurableProduct\Helper\Product\Options\Factory; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_store_group_and_store.php'); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductAttributeRepositoryInterface $productAttributeRepository */ +$productAttributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var Factory $optionsFactory */ +$optionsFactory = $objectManager->get(Factory::class); +/** @var ProductExtensionFactory $extensionAttributesFactory */ +$extensionAttributesFactory = $objectManager->get(ProductExtensionFactory::class); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +/** @var SwitchPriceAttributeScopeOnConfigChange $observer */ +$observer = $objectManager->get(Observer::class); +/** @var DefaultCategory $categoryHelper */ +$categoryHelper = $objectManager->get(DefaultCategory::class); + +$attribute = $productAttributeRepository->get('test_configurable'); +$options = $attribute->getOptions(); +$baseWebsite = $websiteRepository->get('base'); +$secondWebsite = $websiteRepository->get('test'); +$attributeValues = []; +$associatedProductIds = []; +array_shift($options); + +foreach ($options as $option) { + $product = $productFactory->create(); + $product->setTypeId(ProductType::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsite->getId(), $secondWebsite->getId()]) + ->setName('Configurable Option ' . $option->getLabel()) + ->setSku(strtolower(str_replace(' ', '_', 'simple ' . $option->getLabel()))) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setPrice(150) + ->setCategoryIds([$categoryHelper->getId(), 333]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + $associatedProductIds[] = $product->getId(); + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; +} +$configurableAttributesData = [ + [ + 'values' => $attributeValues, + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + ], +]; +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$product = $productFactory->create(); +$extensionConfigurableAttributes = $product->getExtensionAttributes() ?: $extensionAttributesFactory->create(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); +$product->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsite->getId(), $secondWebsite->getId()]) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([$categoryHelper->getId(), 333]) + ->setSku('configurable') + ->setName('Configurable Product') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->save($product); + +$configResource->saveConfig(Data::XML_PATH_PRICE_SCOPE, Store::PRICE_SCOPE_WEBSITE, 'default', 0); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); +$objectManager->get(SwitchPriceAttributeScopeOnConfigChange::class)->execute($observer); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$secondStoreId = $storeManager->getStore('fixture_second_store')->getId(); + +try { + $currentStoreCode = $storeManager->getStore()->getCode(); + $storeManager->setCurrentStore('fixture_second_store'); + $firstChild = $productRepository->get('simple_option_1', false, $secondStoreId, true); + $firstChild->setPrice(20) + ->setSpecialPrice(10); + $productRepository->save($firstChild); + $secondChild = $productRepository->get('simple_option_2', false, $secondStoreId, true); + $secondChild->setPrice(40) + ->setSpecialPrice(30); + $productRepository->save($secondChild); +} finally { + $storeManager->setCurrentStore($currentStoreCode); +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website_rollback.php new file mode 100644 index 0000000000000..ceeeb13fb5a36 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Helper\Data; +use Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\TestFramework\ConfigurableProduct\Model\DeleteConfigurableProduct; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var DeleteConfigurableProduct $deleteConfigurableProduct */ +$deleteConfigurableProduct = $objectManager->get(DeleteConfigurableProduct::class); +$deleteConfigurableProduct->execute('configurable'); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +$configResource->deleteConfig(Data::XML_PATH_PRICE_SCOPE, 'default', 0); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); +$observer = $objectManager->get(Observer::class); +$objectManager->get(SwitchPriceAttributeScopeOnConfigChange::class)->execute($observer); + +Resolver::getInstance()->requireDataFixture( + 'Magento/Store/_files/second_website_with_store_group_and_store_rollback.php' +); +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_rollback.php' +); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php index 61c2bf7b5fa72..f6e6261c75662 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php @@ -44,7 +44,6 @@ $product = $objectManager->create(Product::class); $productId = array_shift($productIds); $product->setTypeId(Type::TYPE_SIMPLE) - ->setId($productId) ->setAttributeSetId($attributeSetId) ->setWebsiteIds([1]) ->setName('Configurable Option' . $option->getLabel()) @@ -84,7 +83,6 @@ $product->setExtensionAttributes($extensionConfigurableAttributes); $product->setTypeId(Configurable::TYPE_CODE) - ->setId(1) ->setAttributeSetId($attributeSetId) ->setWebsiteIds([1]) ->setName('Configurable Product') @@ -110,7 +108,6 @@ $product = $objectManager->create(Product::class); $productId = array_shift($productIds); $product->setTypeId(Type::TYPE_SIMPLE) - ->setId($productId) ->setAttributeSetId($attributeSetId) ->setWebsiteIds([1]) ->setName('Configurable Option' . $option->getLabel()) @@ -155,7 +152,6 @@ $product->setExtensionAttributes($extensionConfigurableAttributes); $product->setTypeId(Configurable::TYPE_CODE) - ->setId(11) ->setAttributeSetId($attributeSetId) ->setWebsiteIds([1]) ->setName('Configurable Product 12345') diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes.php new file mode 100644 index 0000000000000..bdab71bdf5230 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes.php @@ -0,0 +1,211 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Setup\CategorySetup; +use Magento\ConfigurableProduct\Helper\Product\Options\Factory; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_first.php' +); +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_second.php' +); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/** @var \Magento\Eav\Model\Config $eavConfig */ +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$firstAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_first'); +$secondAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_second'); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $firstAttributeOptions */ +$firstAttributeOptions = $firstAttribute->getOptions(); +/** @var AttributeOptionInterface[] $secondAttributeOptions */ +$secondAttributeOptions = $secondAttribute->getOptions(); + +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10, 20]; +$firstAttributeValues = []; +$secondAttributeValues = []; +$i = 1; +foreach ($productIds as $productId) { + $firstOption = $firstAttributeOptions[$i]; + $secondOption = $secondAttributeOptions[$i]; + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option ' . $firstOption->getLabel() . '-' . $secondOption->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $customAttributes = [ + $firstAttribute->getAttributeCode() => $firstOption->getValue(), + $secondAttribute->getAttributeCode() => $secondOption->getValue() + ]; + foreach ($customAttributes as $attributeCode => $attributeValue) { + $product->setCustomAttributes($customAttributes); + } + $product = $productRepository->save($product); + + $firstAttributeValues[] = [ + 'label' => 'test first ' . $i, + 'attribute_id' => $firstAttribute->getId(), + 'value_index' => $firstOption->getValue(), + ]; + $secondAttributeValues[] = [ + 'label' => 'test second ' . $i, + 'attribute_id' => $secondAttribute->getId(), + 'value_index' => $secondOption->getValue(), + ]; + $associatedProductIds[] = $product->getId(); + $i++; +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); +$configurableAttributesData = [ + [ + 'attribute_id' => $firstAttribute->getId(), + 'code' => $firstAttribute->getAttributeCode(), + 'label' => $firstAttribute->getStoreLabel(), + 'position' => '0', + 'values' => $firstAttributeValues, + ], + [ + 'attribute_id' => $secondAttribute->getId(), + 'code' => $secondAttribute->getAttributeCode(), + 'label' => $secondAttribute->getStoreLabel(), + 'position' => '1', + 'values' => $secondAttributeValues, + ], +]; +$configurableOptions = $optionsFactory->create($configurableAttributesData); +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$productRepository->save($product); + +$firstAttributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [30, 40]; +$firstAttributeValues = []; +$secondAttributeValues = []; + +foreach ($productIds as $productId) { + $firstOption = $firstAttributeOptions[$i]; + $secondOption = $secondAttributeOptions[$i]; + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($firstAttributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option ' . $firstOption->getLabel() . '-' . $secondOption->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $customAttributes = [ + $firstAttribute->getAttributeCode() => $firstOption->getValue(), + $secondAttribute->getAttributeCode() => $secondOption->getValue() + ]; + foreach ($customAttributes as $attributeCode => $attributeValue) { + $product->setCustomAttributes($customAttributes); + } + $product = $productRepository->save($product); + + $firstAttributeValues[] = [ + 'label' => 'test first ' . $i, + 'attribute_id' => $firstAttribute->getId(), + 'value_index' => $firstOption->getValue(), + ]; + $secondAttributeValues[] = [ + 'label' => 'test second ' . $i, + 'attribute_id' => $secondAttribute->getId(), + 'value_index' => $secondOption->getValue(), + ]; + $associatedProductIds[] = $product->getId(); + $i++; +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $firstAttribute->getId(), + 'code' => $firstAttribute->getAttributeCode(), + 'label' => $firstAttribute->getStoreLabel(), + 'position' => '0', + 'values' => $firstAttributeValues, + ], + [ + 'attribute_id' => $secondAttribute->getId(), + 'code' => $secondAttribute->getAttributeCode(), + 'label' => $secondAttribute->getStoreLabel(), + 'position' => '1', + 'values' => $secondAttributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(11) + ->setAttributeSetId($firstAttributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product 12345') + ->setSku('configurable_12345') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_rollback.php new file mode 100644 index 0000000000000..f2bb4ebda2b3e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_rollback.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_10', 'simple_20', 'configurable', 'simple_30', 'simple_40', 'configurable_12345'] as $sku) { + try { + $product = $productRepository->get($sku, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_first_rollback.php' +); +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_second_rollback.php' +); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php new file mode 100644 index 0000000000000..30a9a47f910d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as ProductAttribute; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\DataObject; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_with_uk_address.php'); + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +/** @var Quote $quote */ +$quote = $objectManager->get(QuoteFactory::class)->create(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var Config $eavConfig */ +$eavConfig = $objectManager->get(Config::class); +/** @var ProductAttribute $attribute */ +$attribute = $eavConfig->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'test_configurable'); + +$customer = $customerRepository->get('customer_uk_address@test.com'); +$quote->setStoreId($storeManager->getStore()->getId()) + ->setIsActive(true) + ->setIsMultiShipping(false) + ->setReservedOrderId('customer_quote_configurable_products') + ->assignCustomer($customer); + +$attributeOptions = $attribute->getOptions(); +unset($attributeOptions[0]); +$productConfigurable = $productRepository->get('configurable'); +/** @var DataObject $request */ +$request = $objectManager->create(DataObject::class); + +foreach ($attributeOptions as $attributeOption) { + $productConfigurable = clone $productConfigurable; + $request->setData( + [ + 'product_id' => $productConfigurable->getId(), + 'super_attribute' => [ + $attribute->getAttributeId() => $attributeOption->getValue() + ], + 'qty' => 1 + ] + ); + $quote->addProduct($productConfigurable, $request); +} +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product_rollback.php new file mode 100644 index 0000000000000..e5cd7637c6e55 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +/** @var $objectManager ObjectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$quote = $objectManager->get(GetQuoteByReservedOrderId::class)->execute('customer_quote_configurable_products'); +if ($quote !== null) { + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $objectManager->get(CartRepositoryInterface::class); + $quoteRepository->delete($quote); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_with_uk_address_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable.php index 35439a24cd2db..f1f65602a9273 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable.php @@ -24,6 +24,7 @@ /** @var ProductRepositoryInterface $productRepository */ $productRepository = Bootstrap::getObjectManager() ->get(ProductRepositoryInterface::class); + /** @var $installer CategorySetup */ $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); $eavConfig = Bootstrap::getObjectManager()->get(Config::class); @@ -35,7 +36,7 @@ $attributeValues = []; $attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); $associatedProductIds = []; -$productIds = [10, 20]; +$idsToReindex = $productIds = [10, 20]; array_shift($options); //remove the first option which is empty foreach ($options as $option) { diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_in_multiple_websites.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_in_multiple_websites.php new file mode 100644 index 0000000000000..f58f38df4ff89 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_in_multiple_websites.php @@ -0,0 +1,152 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Setup\CategorySetup; +use Magento\ConfigurableProduct\Helper\Product\Options\Factory; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Eav\Model\Config; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); + +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute.php' +); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +$eavConfig = Bootstrap::getObjectManager()->get(Config::class); +$attribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable'); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10, 20]; +array_shift($options); //remove the first option which is empty +/** @var WebsiteRepositoryInterface $repository */ +$repository = Bootstrap::getObjectManager()->get(WebsiteRepositoryInterface::class); +$websiteId = $repository->get('test')->getId(); +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1, $websiteId]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + + $product = $productRepository->save($product); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +// Remove any previously created product with the same id. +/** @var \Magento\Framework\Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +try { + $productToDelete = $productRepository->getById(1); + $productRepository->delete($productToDelete); + + /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource */ + $itemResource = Bootstrap::getObjectManager()->get(\Magento\Quote\Model\ResourceModel\Quote\Item::class); + $itemResource->getConnection()->delete( + $itemResource->getMainTable(), + 'product_id = ' . $productToDelete->getId() + ); +} catch (\Exception $e) { + // Nothing to remove +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([$websiteId]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); + +$productRepository->save($product); + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_in_multiple_websites_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_in_multiple_websites_rollback.php new file mode 100644 index 0000000000000..98e044d6b1a8a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_in_multiple_websites_rollback.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_10', 'simple_20', 'configurable'] as $sku) { + try { + $product = $productRepository->get($sku, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_rollback.php' +); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php new file mode 100644 index 0000000000000..81a067195e902 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php @@ -0,0 +1,110 @@ +<?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\Indexer\Product\Price\Processor as PriceIndexerProcessor; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Setup\CategorySetup; +use Magento\CatalogInventory\Model\Stock\Item; +use Magento\ConfigurableProduct\Helper\Product\Options\Factory; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Config; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Bootstrap::getInstance()->reinitialize(); + +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); + +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); + +$eavConfig = Bootstrap::getObjectManager()->get(Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$product = Bootstrap::getObjectManager()->create(Product::class); +$product->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); +$product = $productRepository->save($product); + +$attributeValues = []; +$associatedProductIds = []; +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); +array_shift($options); //remove the first option which is empty +$productNumber = 0; +foreach ($options as $option) { + $productNumber++; + + $childProduct = Bootstrap::getObjectManager()->create(Product::class); + $childProduct->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productNumber) + ->setPrice($productNumber * 10) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + ['use_config_manage_stock' => 1,'qty' => $productNumber * 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1] + ); + $childProduct = $productRepository->save($childProduct); + + $stockItem = Bootstrap::getObjectManager()->create(Item::class); + $stockItem->load($childProduct->getId(), 'product_id'); + if (!$stockItem->getProductId()) { + $stockItem->setProductId($childProduct->getId()); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty($productNumber * 100); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $childProduct->getId(); +} + +$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); +$indexerProcessor->reindexList($associatedProductIds); + +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); +$configurableOptions = $optionsFactory->create( + [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], + ] +); +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); +$product = $productRepository->save($product); + +$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); +$indexerProcessor->reindexRow($product->getId()); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples_rollback.php new file mode 100644 index 0000000000000..68621f78745e8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples_rollback.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogInventory\Model\Stock\Status; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); + +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +foreach (['simple_1', 'simple_2', 'configurable'] as $sku) { + try { + $product = $productRepository->get($sku, true); + + $stockStatus = $objectManager->create(Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute_rollback.php'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight.php index 879d19a0d0b96..91eb1d709b8f0 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight.php @@ -20,7 +20,7 @@ \Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); -Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute_first.php'); Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category.php'); /** @var ProductRepositoryInterface $productRepository */ @@ -31,7 +31,7 @@ $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); /** @var Config $eavConfig */ $eavConfig = Bootstrap::getObjectManager()->get(Config::class); -$attribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable'); +$attribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_first'); /* Create simple products per each option value*/ /** @var AttributeOptionInterface[] $options */ $options = $attribute->getOptions(); @@ -46,113 +46,118 @@ 20 => Visibility::VISIBILITY_IN_CATALOG ]; +$i = 0; foreach ($options as $option) { - /** @var $product Product */ - $product = Bootstrap::getObjectManager()->create(Product::class); - $productId = array_shift($productIds); - $product->setTypeId(Type::TYPE_SIMPLE) - ->setId($productId) - ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) - ->setName('Configurable Option' . $option->getLabel()) - ->setSku('simple_' . $productId) - ->setPrice($productId) - ->setTestConfigurable($option->getValue()) - ->setVisibility($visibility[$productId]) - ->setStatus(Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); - $eavAttributeValues = [ - 'category_ids' => [333] + if ($i < 2) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility($visibility[$productId]) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $eavAttributeValues = [ + 'category_ids' => [333], + $attribute->getAttributeCode() => $option->getValue() ]; - foreach ($eavAttributeValues as $eavCategoryAttributeCode => $eavCategoryAttributeValues) { - $product->setCustomAttribute($eavCategoryAttributeCode, $eavCategoryAttributeValues); - } - - $product = $productRepository->save($product); - - /** - * @var \Magento\TestFramework\ObjectManager $objectManager - */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - /** - * @var \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterfaceFactory $mediaGalleryEntryFactory - */ - - $mediaGalleryEntryFactory = $objectManager->get( - \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterfaceFactory::class - ); - - /** - * @var \Magento\Framework\Api\Data\ImageContentInterfaceFactory $imageContentFactory - */ - $imageContentFactory = $objectManager->get(\Magento\Framework\Api\Data\ImageContentInterfaceFactory::class); - $imageContent = $imageContentFactory->create(); - $testImagePath = __DIR__ .'/magento_image.jpg'; - $imageContent->setBase64EncodedData(base64_encode(file_get_contents($testImagePath))); - $imageContent->setType("image/jpeg"); - $imageContent->setName("1.jpg"); - - $video = $mediaGalleryEntryFactory->create(); - $video->setDisabled(false); - $video->setFile('1.jpg'); - $video->setLabel('Video Label'); - $video->setMediaType('external-video'); - $video->setPosition(2); - $video->setContent($imageContent); - - /** - * @var ProductAttributeMediaGalleryEntryExtensionFactory $mediaGalleryEntryExtensionFactory - */ - $mediaGalleryEntryExtensionFactory = $objectManager->get( - \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryExtensionFactory::class - ); - $mediaGalleryEntryExtension = $mediaGalleryEntryExtensionFactory->create(); - - /** - * @var \Magento\Framework\Api\Data\VideoContentInterfaceFactory $videoContentFactory - */ - $videoContentFactory = $objectManager->get( - \Magento\Framework\Api\Data\VideoContentInterfaceFactory::class - ); - $videoContent = $videoContentFactory->create(); - $videoContent->setMediaType('external-video'); - $videoContent->setVideoDescription('Video description'); - $videoContent->setVideoProvider('youtube'); - $videoContent->setVideoMetadata('Video Metadata'); - $videoContent->setVideoTitle('Video title'); - $videoContent->setVideoUrl('http://www.youtube.com/v/tH_2PFNmWoga'); - - $mediaGalleryEntryExtension->setVideoContent($videoContent); - $video->setExtensionAttributes($mediaGalleryEntryExtension); - - /** - * @var \Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface $mediaGalleryManagement - */ - $mediaGalleryManagement = $objectManager->get( - \Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface::class - ); - $mediaGalleryManagement->create('simple_' . $productId, $video); - - /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ - $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); - $stockItem->load($productId, 'product_id'); - - if (!$stockItem->getProductId()) { - $stockItem->setProductId($productId); + foreach ($eavAttributeValues as $eavCategoryAttributeCode => $eavCategoryAttributeValues) { + $product->setCustomAttribute($eavCategoryAttributeCode, $eavCategoryAttributeValues); + } + + $product = $productRepository->save($product); + + /** + * @var \Magento\TestFramework\ObjectManager $objectManager + */ + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + /** + * @var \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterfaceFactory $mediaGalleryEntryFactory + */ + + $mediaGalleryEntryFactory = $objectManager->get( + \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterfaceFactory::class + ); + + /** + * @var \Magento\Framework\Api\Data\ImageContentInterfaceFactory $imageContentFactory + */ + $imageContentFactory = $objectManager->get(\Magento\Framework\Api\Data\ImageContentInterfaceFactory::class); + $imageContent = $imageContentFactory->create(); + $testImagePath = __DIR__ .'/magento_image.jpg'; + $imageContent->setBase64EncodedData(base64_encode(file_get_contents($testImagePath))); + $imageContent->setType("image/jpeg"); + $imageContent->setName("1.jpg"); + + $video = $mediaGalleryEntryFactory->create(); + $video->setDisabled(false); + $video->setFile('1.jpg'); + $video->setLabel('Video Label'); + $video->setMediaType('external-video'); + $video->setPosition(2); + $video->setContent($imageContent); + + /** + * @var ProductAttributeMediaGalleryEntryExtensionFactory $mediaGalleryEntryExtensionFactory + */ + $mediaGalleryEntryExtensionFactory = $objectManager->get( + \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryExtensionFactory::class + ); + $mediaGalleryEntryExtension = $mediaGalleryEntryExtensionFactory->create(); + + /** + * @var \Magento\Framework\Api\Data\VideoContentInterfaceFactory $videoContentFactory + */ + $videoContentFactory = $objectManager->get( + \Magento\Framework\Api\Data\VideoContentInterfaceFactory::class + ); + $videoContent = $videoContentFactory->create(); + $videoContent->setMediaType('external-video'); + $videoContent->setVideoDescription('Video description'); + $videoContent->setVideoProvider('youtube'); + $videoContent->setVideoMetadata('Video Metadata'); + $videoContent->setVideoTitle('Video title'); + $videoContent->setVideoUrl('http://www.youtube.com/v/tH_2PFNmWoga'); + + $mediaGalleryEntryExtension->setVideoContent($videoContent); + $video->setExtensionAttributes($mediaGalleryEntryExtension); + + /** + * @var \Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface $mediaGalleryManagement + */ + $mediaGalleryManagement = $objectManager->get( + \Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface::class + ); + $mediaGalleryManagement->create('simple_' . $productId, $video); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); + $i++; } - $stockItem->setUseConfigManageStock(1); - $stockItem->setQty(1000); - $stockItem->setIsQtyDecimal(0); - $stockItem->setIsInStock(1); - $stockItem->save(); - - $attributeValues[] = [ - 'label' => 'test', - 'attribute_id' => $attribute->getId(), - 'value_index' => $option->getValue(), - ]; - $associatedProductIds[] = $product->getId(); } /** @var $product Product */ diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight_rollback.php index ba3fad88c1fba..570679853fb87 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight_rollback.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight_rollback.php @@ -3,7 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/product_configurable_rollback.php'); -Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category_rollback.php'); +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_first_rollback.php' +); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php index 4e581ee0e995a..1419bec32431c 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php @@ -75,7 +75,14 @@ ] )->setCanSaveCustomOptions(true) ->setHasOptions(true) - ->setCustomAttribute('test_configurable', 42); + ->setCustomAttribute( + 'test_configurable', + Bootstrap::getObjectManager() + ->create(\Magento\Eav\Api\AttributeRepositoryInterface::class) + ->get('catalog_product', 'test_configurable') + ->getOptions()[1] + ->getValue() + ); $oldOptions = [ [ diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/quote_with_configurable_product_last_variation.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/quote_with_configurable_product_last_variation.php index 072c0cd8f9118..1f0dee32ce4a2 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/quote_with_configurable_product_last_variation.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/quote_with_configurable_product_last_variation.php @@ -14,11 +14,11 @@ $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); /** @var ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(ProductRepositoryInterface::class); -$product = $productRepository->getById(10); +$product = $productRepository->get('simple_10'); $product->setStockData(['use_config_manage_stock' => 1, 'qty' => 1, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); $productRepository->save($product); -$product = $productRepository->getById(20); +$product = $productRepository->get('simple_20'); $product->setStockData(['use_config_manage_stock' => 1, 'qty' => 0, 'is_qty_decimal' => 0, 'is_in_stock' => 0]); $productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php new file mode 100644 index 0000000000000..7fd64c95f9942 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Authorization\Model\Acl\Role\Group; +use Magento\Authorization\Model\RoleFactory; +use Magento\Authorization\Model\Role; +use Magento\Authorization\Model\Rules; +use Magento\Authorization\Model\UserContextInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; + +/** @var Role $role */ +$role = Bootstrap::getObjectManager()->get(RoleFactory::class)->create(); +$role->setName('role_catalog_permissions'); +$role->setData('role_name', $role->getName()); +$role->setRoleType(Group::ROLE_TYPE); +$role->setUserType((string)UserContextInterface::USER_TYPE_ADMIN); +$role->save(); + +/** @var $rule Rules */ +$rule = Bootstrap::getObjectManager()->create(Rules::class); +$rule->setRoleId($role->getId())->setResources(['Magento_Catalog::catalog'])->saveRel(); + +/** @var User $user */ +$user = Bootstrap::getObjectManager()->create(User::class); +$user->setData( + [ + 'firstname' => 'firstname', + 'lastname' => 'lastname', + 'email' => 'admincatalog@example.com', + 'username' => 'admincatalog_user', + 'password' => 'admincatalog_password1', + 'is_active' => 1, + ] +); +$user->setRoleId($role->getId())->save(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions_rollback.php new file mode 100644 index 0000000000000..743503d1bd388 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Authorization\Model\Role; +use Magento\Authorization\Model\RoleFactory; +use Magento\Authorization\Model\Rules; +use Magento\Authorization\Model\RulesFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; + +// Deleting the user and the role. +/** @var User $user */ +$user = Bootstrap::getObjectManager()->create(User::class); +$user->loadByUsername('admincatalog_user')->delete(); +/** @var Role $role */ +$role = Bootstrap::getObjectManager()->get(RoleFactory::class)->create(); +$role->load('role_catalog_permissions', 'role_name'); +if ($role->getId()) { + /** @var Rules $rules */ + $rules = Bootstrap::getObjectManager()->get(RulesFactory::class)->create(); + $rules->load($role->getId(), 'role_id'); + $rules->delete(); + $role->delete(); +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/CachedBlockTest.php b/dev/tests/integration/testsuite/Magento/Csp/CachedBlockTest.php new file mode 100644 index 0000000000000..1b5325a07bdd1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/CachedBlockTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp; + +use Magento\Csp\Model\Collector\DynamicCollector; +use Magento\Csp\Model\Collector\DynamicCollectorMock; +use Magento\Framework\Math\Random; +use Magento\Framework\View\LayoutInterface; +use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Element\Template; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test that inline util works fine with cached blocks. + */ +class CachedBlockTest extends TestCase +{ + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @var DynamicCollectorMock + */ + private $dynamicCollected; + + /** + * @var Random + */ + private $random; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + DynamicCollector::class => DynamicCollectorMock::class + ] + ]); + $this->layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + $this->dynamicCollected = Bootstrap::getObjectManager()->get(DynamicCollector::class); + $this->random = Bootstrap::getObjectManager()->get(Random::class); + } + + /** + * Validate policies preserved when reading block from cache. + * + * @return void + * + * @magentoAppArea frontend + * @magentoCache block_html enabled + */ + public function testCachedPolicies(): void + { + /** @var Template $block */ + $block = $this->layout->createBlock( + Template::class, + 'test-block', + ['data' => ['cache_lifetime' => 3600, 'cache_key' => $this->random->getRandomString(32)]] + ); + $block->setTemplate('Magento_TestModuleCspUtil::secure.phtml'); + //Clearing previously added just in case. + $this->dynamicCollected->consumeAdded(); + + $block->toHtml(); + $dynamic = $this->dynamicCollected->consumeAdded(); + $this->assertNotEmpty($dynamic); + + //From cache + $block->toHtml(); + $cached = $this->dynamicCollected->consumeAdded(); + $this->assertEquals($dynamic, $cached); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/CspAwareActionTest.php b/dev/tests/integration/testsuite/Magento/Csp/CspAwareActionTest.php new file mode 100644 index 0000000000000..883851c77a46f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/CspAwareActionTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp; + +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test that controllers can modify CSPs for a page. + * + * @magentoAppArea frontend + */ +class CspAwareActionTest extends AbstractController +{ + /** + * Check that a CSP aware action can modify CSPs after ALL other policies had been gathered. + * + * @return void + * @magentoConfigFixture default_store csp/mode/storefront/report_only 0 + * @magentoConfigFixture default_store csp/policies/storefront/script/policy_id script-src + * @magentoConfigFixture default_store csp/policies/storefront/script/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/script/hosts/example http://controller.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/script/self 0 + * @magentoConfigFixture default_store csp/policies/storefront/script/inline 0 + */ + public function testAwareAction(): void + { + $this->getRequest()->setMethod('GET'); + $this->dispatch('csputil/csp/aware'); + $header = $this->getResponse()->getHeader('Content-Security-Policy'); + $this->assertNotEmpty($header); + + $this->assertStringContainsString( + 'script-src https://controller.magento.com' + .' \'self\' \'sha256-H4RRnauTM2X2Xg/z9zkno1crqhsaY3uKKu97uwmnXXE=\'', + $header->getFieldValue() + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/CspTest.php b/dev/tests/integration/testsuite/Magento/Csp/CspTest.php index e66c6af36e42c..8905c953a00e3 100644 --- a/dev/tests/integration/testsuite/Magento/Csp/CspTest.php +++ b/dev/tests/integration/testsuite/Magento/Csp/CspTest.php @@ -23,10 +23,6 @@ class CspTest extends AbstractController */ private function searchInResponse($response, string $search): bool { - if (mb_stripos(mb_strtolower($response->getBody()), mb_strtolower($search)) !== false) { - return true; - } - foreach ($response->getHeaders() as $header) { if (mb_stripos(mb_strtolower($header->toString()), mb_strtolower($search)) !== false) { return true; @@ -67,7 +63,7 @@ public function testStorefrontPolicies(): void $this->assertFalse($this->searchInResponse($response, '\'none\'')); $this->assertTrue($this->searchInResponse($response, 'script-src')); $this->assertTrue($this->searchInResponse($response, '\'unsafe-inline\'')); - $this->assertFalse($this->searchInResponse($response, 'font-src')); + $this->assertTrue($this->searchInResponse($response, 'font-src')); //Policies configured in cps_whitelist.xml files $this->assertTrue($this->searchInResponse($response, 'object-src')); $this->assertTrue($this->searchInResponse($response, 'media-src')); @@ -104,7 +100,7 @@ public function testAdminPolicies(): void $this->assertFalse($this->searchInResponse($response, '\'none\'')); $this->assertTrue($this->searchInResponse($response, 'script-src')); $this->assertTrue($this->searchInResponse($response, '\'unsafe-inline\'')); - $this->assertFalse($this->searchInResponse($response, 'font-src')); + $this->assertTrue($this->searchInResponse($response, 'font-src')); } /** diff --git a/dev/tests/integration/testsuite/Magento/Csp/CspUtilTest.php b/dev/tests/integration/testsuite/Magento/Csp/CspUtilTest.php new file mode 100644 index 0000000000000..93d019f572e63 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/CspUtilTest.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp; + +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test CSP util use cases. + * + * @magentoAppArea frontend + */ +class CspUtilTest extends AbstractController +{ + /** + * Test that CSP helper for templates works. + * + * @return void + * @magentoConfigFixture default_store csp/mode/storefront/report_only 0 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/inline 0 + */ + public function testPhtmlHelper(): void + { + $this->getRequest()->setMethod('GET'); + $this->dispatch('csputil/csp/helper'); + $content = $this->getResponse()->getContent(); + + $this->assertStringContainsString( + '<script src="http://my.magento.com/static/script.js"/>', + $content + ); + $this->assertStringContainsString("<script>\n let myVar = 1;\n</script>", $content); + $header = $this->getResponse()->getHeader('Content-Security-Policy'); + $this->assertNotEmpty($header); + $this->assertStringContainsString('http://my.magento.com', $header->getFieldValue()); + $this->assertStringContainsString('\'sha256-H4RRnauTM2X2Xg/z9zkno1crqhsaY3uKKu97uwmnXXE=\'', $header->getFieldValue()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Helper/InlineUtilTest.php b/dev/tests/integration/testsuite/Magento/Csp/Helper/InlineUtilTest.php new file mode 100644 index 0000000000000..0f31f0beccda9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Helper/InlineUtilTest.php @@ -0,0 +1,308 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Helper; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Model\Collector\DynamicCollector; +use Magento\Csp\Model\Collector\DynamicCollectorMock; +use Magento\Csp\Model\Policy\FetchPolicy; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Cover CSP util use cases. + */ +class InlineUtilTest extends TestCase +{ + /** + * @var InlineUtil + */ + private $util; + + /** + * @var SecureHtmlRenderer + */ + private $secureHtmlRenderer; + + /** + * @var DynamicCollectorMock + */ + private $dynamicCollector; + + /** + * @inheritDoc + */ + public function setUp(): void + { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + DynamicCollector::class => DynamicCollectorMock::class + ] + ]); + $this->util = Bootstrap::getObjectManager()->get(InlineUtil::class); + $this->secureHtmlRenderer = Bootstrap::getObjectManager()->get(SecureHtmlRenderer::class); + $this->dynamicCollector = Bootstrap::getObjectManager()->get(DynamicCollector::class); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $this->util = null; + $this->secureHtmlRenderer = null; + $this->dynamicCollector->consumeAdded(); + $this->dynamicCollector = null; + } + + /** + * Test tag rendering. + * + * @param string $tagName + * @param array $attributes + * @param string|null $content + * @param string $result Expected result. + * @param PolicyInterface[] $policiesExpected + * @return void + * @dataProvider getTags + */ + public function testRenderTag( + string $tagName, + array $attributes, + ?string $content, + string $result, + array $policiesExpected + ): void { + $this->assertEquals($result, $this->util->renderTag($tagName, $attributes, $content)); + $this->assertEquals($policiesExpected, $this->dynamicCollector->consumeAdded()); + } + + /** + * Test data for tag rendering test. + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function getTags(): array + { + return [ + 'remote-script' => [ + 'script', + ['src' => 'http://magento.com/static/some-script.js'], + null, + '<script src="http://magento.com/static/some-script.js"/>', + [new FetchPolicy('script-src', false, ['http://magento.com'])] + ], + 'inline-script' => [ + 'script', + ['type' => 'text/javascript'], + "\n let someVar = 25;\n document.getElementById('test').innerText = someVar;\n", + "<script type=\"text/javascript\">\n let someVar = 25;" + ."\n document.getElementById('test').innerText = someVar;\n</script>", + [ + new FetchPolicy( + 'script-src', + false, + [], + [], + false, + false, + false, + [], + ['U+SKpEef030N2YgyKKdIBIvPy8Fmd42N/JcTZgQV+DA=' => 'sha256'] + ) + ] + ], + 'remote-style' => [ + 'link', + ['rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'http://magento.com/static/style.css'], + null, + '<link rel="stylesheet" type="text/css"' + . ' href="http://magento.com/static/style.css"/>', + [new FetchPolicy('style-src', false, ['http://magento.com'])] + ], + 'inline-style' => [ + 'style', + [], + "\n h1 {color: red;}\n p {color: green;}\n", + "<style>\n h1 {color: red;}\n p {color: green;}\n</style>", + [ + new FetchPolicy( + 'style-src', + false, + [], + [], + false, + false, + false, + [], + ['KISO7smrk+XdGrEsiPvVjX6qx4wNef/UKjNb26RaKGM=' => 'sha256'] + ) + ] + ], + 'remote-image' => [ + 'img', + ['src' => 'http://magento.com/static/my.jpg'], + null, + '<img src="http://magento.com/static/my.jpg"/>', + [new FetchPolicy('img-src', false, ['http://magento.com'])] + ], + 'remote-font' => [ + 'style', + ['type' => 'text/css'], + "\n @font-face {\n font-family: \"MyCustomFont\";" + ."\n src: url(\"http://magento.com/static/font.ttf\");\n }\n" + ." @font-face {\n font-family: \"MyCustomFont2\";" + ."\n src: url('https://magento.com/static/font-2.ttf')," + ."\n url(static/font.ttf)," + ."\n url(https://devdocs.magento.com/static/another-font.woff)," + ."\n url(http://devdocs.magento.com/static/font.woff);\n }\n", + "<style type=\"text/css\">" + ."\n @font-face {\n font-family: \"MyCustomFont\";" + ."\n src: url(\"http://magento.com/static/font.ttf\");\n }\n" + ." @font-face {\n font-family: \"MyCustomFont2\";" + ."\n src: url('https://magento.com/static/font-2.ttf')," + ."\n url(static/font.ttf)," + ."\n url(https://devdocs.magento.com/static/another-font.woff)," + ."\n url(http://devdocs.magento.com/static/font.woff);\n }\n" + ."</style>", + [ + new FetchPolicy( + 'style-src', + false, + [ + 'http://magento.com', + 'https://magento.com', + 'https://devdocs.magento.com', + 'http://devdocs.magento.com' + ] + ), + new FetchPolicy( + 'style-src', + false, + [], + [], + false, + false, + false, + [], + ['TP6Ulnz1kstJ8PYUKvowgJm0phHhtqJnJCnWxKLXkf0=' => 'sha256'] + ) + ] + ], + 'cross-origin-form' => [ + 'form', + ['action' => 'https://magento.com/submit', 'method' => 'post'], + "\n <input type=\"text\" name=\"test\" /><input type=\"submit\" value=\"Submit\" />\n", + "<form action=\"https://magento.com/submit\" method=\"post\">" + ."\n <input type=\"text\" name=\"test\" /><input type=\"submit\" value=\"Submit\" />\n" + ."</form>", + [new FetchPolicy('form-action', false, ['https://magento.com'])] + ], + 'cross-origin-iframe' => [ + 'iframe', + ['src' => 'http://magento.com/some-page'], + null, + '<iframe src="http://magento.com/some-page"/>', + [new FetchPolicy('frame-src', false, ['http://magento.com'])] + ], + 'remote-track' => [ + 'track', + ['src' => 'http://magento.com/static/track.vtt', 'kind' => 'subtitles'], + null, + '<track src="http://magento.com/static/track.vtt" kind="subtitles"/>', + [new FetchPolicy('media-src', false, ['http://magento.com'])] + ], + 'remote-source' => [ + 'source', + ['src' => 'http://magento.com/static/track.ogg', 'type' => 'audio/ogg'], + null, + '<source src="http://magento.com/static/track.ogg" type="audio/ogg"/>', + [new FetchPolicy('media-src', false, ['http://magento.com'])] + ], + 'remote-video' => [ + 'video', + ['src' => 'https://magento.com/static/video.mp4'], + null, + '<video src="https://magento.com/static/video.mp4"/>', + [new FetchPolicy('media-src', false, ['https://magento.com'])] + ], + 'remote-audio' => [ + 'audio', + ['src' => 'https://magento.com/static/audio.mp3'], + null, + '<audio src="https://magento.com/static/audio.mp3"/>', + [new FetchPolicy('media-src', false, ['https://magento.com'])] + ], + 'remote-object' => [ + 'object', + ['data' => 'http://magento.com/static/flash.swf'], + null, + '<object data="http://magento.com/static/flash.swf"/>', + [new FetchPolicy('object-src', false, ['http://magento.com'])] + ], + 'remote-embed' => [ + 'embed', + ['src' => 'http://magento.com/static/flash.swf'], + null, + '<embed src="http://magento.com/static/flash.swf"/>', + [new FetchPolicy('object-src', false, ['http://magento.com'])] + ], + 'remote-applet' => [ + 'applet', + ['code' => 'SomeApplet.class', 'archive' => 'https://magento.com/applet/my-applet.jar'], + null, + '<applet code="SomeApplet.class" ' + . 'archive="https://magento.com/applet/my-applet.jar"/>', + [new FetchPolicy('object-src', false, ['https://magento.com'])] + ] + ]; + } + + /** + * Test that inline event listeners are rendered properly. + * + * @return void + */ + public function testRenderEventListener(): void + { + $result = $this->util->renderEventListener('onclick', 'alert()'); + $this->assertEquals('onclick="alert()"', $result); + $this->assertEquals( + [new FetchPolicy('script-src', false, [], [], false, true)], + $this->dynamicCollector->consumeAdded() + ); + } + + /** + * Check that CSP logic was added to SecureHtmlRenderer + * + * @return void + */ + public function testSecureHtmlRenderer(): void + { + $scriptTag = $this->secureHtmlRenderer->renderTag( + 'script', + ['src' => 'https://test.magento.com/static/script.js'] + ); + $eventListener = $this->secureHtmlRenderer->renderEventListener('onclick', 'alert()'); + + $this->assertEquals( + '<script src="https://test.magento.com/static/script.js"/>', + $scriptTag + ); + $this->assertEquals( + 'onclick="alert()"', + $eventListener + ); + $policies = $this->dynamicCollector->consumeAdded(); + $this->assertTrue(in_array(new FetchPolicy('script-src', false, ['https://test.magento.com']), $policies)); + $this->assertTrue(in_array(new FetchPolicy('script-src', false, [], [], false, true), $policies)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php index e88d5d723ef46..2d8cbbeedeab9 100644 --- a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php @@ -45,7 +45,7 @@ private function getExpectedPolicies(): array 'child-src', false, ['http://magento.com', 'http://devdocs.magento.com'], - ['http'], + ['http', 'https', 'blob'], true, true, false, @@ -86,59 +86,74 @@ private function getExpectedPolicies(): array * Test initiating policies from config. * * @magentoAppArea frontend - * @magentoConfigFixture default_store csp/policies/storefront/default_src/policy_id default-src - * @magentoConfigFixture default_store csp/policies/storefront/default_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example http://magento.com - * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example2 http://devdocs.magento.com - * @magentoConfigFixture default_store csp/policies/storefront/default_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/child_src/policy_id child-src - * @magentoConfigFixture default_store csp/policies/storefront/child_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/child_src/hosts/example http://magento.com - * @magentoConfigFixture default_store csp/policies/storefront/child_src/hosts/example2 http://devdocs.magento.com - * @magentoConfigFixture default_store csp/policies/storefront/child_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/child_src/inline 1 - * @magentoConfigFixture default_store csp/policies/storefront/child_src/schemes/scheme1 http - * @magentoConfigFixture default_store csp/policies/storefront/child_src/dynamic 1 - * @magentoConfigFixture default_store csp/policies/storefront/child_src2/policy_id child-src - * @magentoConfigFixture default_store csp/policies/storefront/child_src2/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/child_src2/eval 1 - * @magentoConfigFixture default_store csp/policies/storefront/connect_src/policy_id connect-src - * @magentoConfigFixture default_store csp/policies/storefront/connect_src/none 1 - * @magentoConfigFixture default_store csp/policies/storefront/font_src/policy_id font-src - * @magentoConfigFixture default_store csp/policies/storefront/font_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/font_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/frame_src/policy_id frame-src - * @magentoConfigFixture default_store csp/policies/storefront/frame_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/frame_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/frame_src/dynamic 1 - * @magentoConfigFixture default_store csp/policies/storefront/img_src/policy_id img-src - * @magentoConfigFixture default_store csp/policies/storefront/img_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/img_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/manifest_src/policy_id manifest-src - * @magentoConfigFixture default_store csp/policies/storefront/manifest_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/manifest_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/media_src/policy_id media-src - * @magentoConfigFixture default_store csp/policies/storefront/media_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/media_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/object_src/policy_id object-src - * @magentoConfigFixture default_store csp/policies/storefront/object_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/object_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/script_src/policy_id script-src - * @magentoConfigFixture default_store csp/policies/storefront/script_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/script_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/script_src/event_handlers 1 + * @magentoConfigFixture default_store csp/policies/storefront/default/policy_id default-src + * @magentoConfigFixture default_store csp/policies/storefront/default/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/default/hosts/example http://magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default/hosts/example2 http://devdocs.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/default/eval 0 + * @magentoConfigFixture default_store csp/policies/storefront/default/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/children/policy_id child-src + * @magentoConfigFixture default_store csp/policies/storefront/children/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/children/hosts/example http://magento.com + * @magentoConfigFixture default_store csp/policies/storefront/children/hosts/example2 http://devdocs.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/children/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/children/inline 1 + * @magentoConfigFixture default_store csp/policies/storefront/children/schemes/scheme1 http + * @magentoConfigFixture default_store csp/policies/storefront/children/dynamic 1 + * @magentoConfigFixture default_store csp/policies/storefront/children-2/policy_id child-src + * @magentoConfigFixture default_store csp/policies/storefront/children-2/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/children-2/eval 1 + * @magentoConfigFixture default_store csp/policies/storefront/connections/policy_id connect-src + * @magentoConfigFixture default_store csp/policies/storefront/connections/none 1 + * @magentoConfigFixture default_store csp/policies/storefront/connections/self 0 + * @magentoConfigFixture default_store csp/policies/storefront/connections/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/fonts/policy_id font-src + * @magentoConfigFixture default_store csp/policies/storefront/fonts/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/fonts/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/fonts/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/frames/policy_id frame-src + * @magentoConfigFixture default_store csp/policies/storefront/frames/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/frames/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/frames/dynamic 1 + * @magentoConfigFixture default_store csp/policies/storefront/frames/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/images/policy_id img-src + * @magentoConfigFixture default_store csp/policies/storefront/images/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/images/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/images/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/manifests/policy_id manifest-src + * @magentoConfigFixture default_store csp/policies/storefront/manifests/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/manifests/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/manifests/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/media/policy_id media-src + * @magentoConfigFixture default_store csp/policies/storefront/media/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/media/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/media/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/objects/policy_id object-src + * @magentoConfigFixture default_store csp/policies/storefront/objects/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/objects/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/objects/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/policy_id script-src + * @magentoConfigFixture default_store csp/policies/storefront/scripts/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/eval 0 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/event_handlers 1 * @magentoConfigFixture default_store csp/policies/storefront/base_uri/policy_id base-uri * @magentoConfigFixture default_store csp/policies/storefront/base_uri/none 0 * @magentoConfigFixture default_store csp/policies/storefront/base_uri/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/style_src/policy_id style-src - * @magentoConfigFixture default_store csp/policies/storefront/style_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/style_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/form_action/policy_id form-action - * @magentoConfigFixture default_store csp/policies/storefront/form_action/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/form_action/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/frame_ancestors/policy_id frame-ancestors - * @magentoConfigFixture default_store csp/policies/storefront/frame_ancestors/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/frame_ancestors/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/styles/policy_id style-src + * @magentoConfigFixture default_store csp/policies/storefront/styles/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/styles/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/styles/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/forms/policy_id form-action + * @magentoConfigFixture default_store csp/policies/storefront/forms/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/forms/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/forms/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/frame-ancestors/policy_id frame-ancestors + * @magentoConfigFixture default_store csp/policies/storefront/frame-ancestors/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/frame-ancestors/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/frame-ancestors/inline 0 * @magentoConfigFixture default_store csp/policies/storefront/plugin_types/policy_id plugin-types * @magentoConfigFixture default_store csp/policies/storefront/plugin_types/types/fl application/x-shockwave-flash * @magentoConfigFixture default_store csp/policies/storefront/plugin_types/types/applet application/x-java-applet @@ -155,32 +170,30 @@ private function getExpectedPolicies(): array * @magentoConfigFixture default_store csp/policies/storefront/sandbox/navigation 1 * @magentoConfigFixture default_store csp/policies/storefront/sandbox/navigation_by_user 1 * @magentoConfigFixture default_store csp/policies/storefront/mixed_content/policy_id block-all-mixed-content + * @magentoConfigFixture default_store csp/policies/storefront/base/policy_id base-uri + * @magentoConfigFixture default_store csp/policies/storefront/base/inline 0 * @magentoConfigFixture default_store csp/policies/storefront/upgrade/policy_id upgrade-insecure-requests * @return void */ public function testCollecting(): void { $policies = $this->collector->collect([new FlagPolicy('upgrade-insecure-requests')]); - $checked = []; $expectedPolicies = $this->getExpectedPolicies(); - - //Policies were collected $this->assertNotEmpty($policies); - //Default policies are being kept - /** @var PolicyInterface $defaultPolicy */ $defaultPolicy = array_shift($policies); $this->assertEquals('upgrade-insecure-requests', $defaultPolicy->getId()); - //Comparing collected with configured - /** @var PolicyInterface $policy */ + $expectedPolicyKeys = array_keys($expectedPolicies); + $checkedKeys = []; + foreach ($policies as $policy) { $id = $policy->getId(); + $this->assertTrue(in_array($id, $expectedPolicyKeys)); if ($id === 'child-src' && $policy->isEvalAllowed()) { $id = 'child-src2'; } - $this->assertEquals($expectedPolicies[$id], $policy); - $checked[] = $id; + $this->assertEquals($expectedPolicies[$id]->getValue(), $policy->getValue()); + $checkedKeys[] = $id; } - $expectedIds = array_keys($expectedPolicies); - $this->assertEquals(sort($expectedIds), sort($checked)); + $this->assertEmpty(array_diff($expectedPolicyKeys, $checkedKeys)); } } diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ControllerCollectorTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ControllerCollectorTest.php new file mode 100644 index 0000000000000..c3a5e58e7f9be --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ControllerCollectorTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\CspAwareActionInterface; +use Magento\Csp\Model\Policy\FetchPolicy; +use Magento\Csp\Model\Policy\FlagPolicy; +use Magento\Framework\Exception\NotFoundException; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test collecting policies from a CSP-aware controllers. + */ +class ControllerCollectorTest extends TestCase +{ + /** + * @var ControllerCollector + */ + private $collector; + + /** + * @inheritDoc + */ + public function setUp(): void + { + $this->collector = Bootstrap::getObjectManager()->create(ControllerCollector::class); + } + + /** + * Test collection. + * + * @return void + */ + public function testCollect(): void + { + $controller = new class implements CspAwareActionInterface { + /** + * @inheritDoc + */ + public function execute() + { + throw new NotFoundException(__('Page not found.')); + } + + /** + * @inheritDoc + */ + public function modifyCsp(array $appliedPolicies): array + { + $processed = []; + foreach ($appliedPolicies as $policy) { + if ($policy instanceof FetchPolicy && $policy->getHostSources()) { + $policy = new FetchPolicy( + 'default-src', + false, + array_map( + function ($host) { + return str_replace('http://', 'https://', $host); + }, + $policy->getHostSources() + ) + ); + } + $processed[] = $policy; + } + $processed[] = new FlagPolicy(FlagPolicy::POLICIES[0]); + + return $processed; + } + }; + + $this->collector->setCurrentActionInstance($controller); + $collected = $this->collector->collect([new FetchPolicy('default-src', false, ['http://magento.com'])]); + $this->assertEquals( + [new FetchPolicy('default-src', false, ['https://magento.com']), new FlagPolicy(FlagPolicy::POLICIES[0])], + $collected + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php index 453d7bd0947af..67a15c24ea410 100644 --- a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php @@ -53,7 +53,13 @@ public function testCollecting(): void if ($policy->getId() === 'object-src') { $this->assertInstanceOf(FetchPolicy::class, $policy); $this->assertEquals(['http://magento.com', 'https://devdocs.magento.com'], $policy->getHostSources()); - $this->assertEquals(['B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256'], $policy->getHashes()); + $this->assertEquals( + [ + 'B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256', + 'B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF9=' => 'sha256' + ], + $policy->getHashes() + ); $objectSrcChecked = true; } elseif ($policy->getId() === 'media-src') { $this->assertInstanceOf(FetchPolicy::class, $policy); diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/DynamicCollectorMock.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/DynamicCollectorMock.php new file mode 100644 index 0000000000000..744df4c92018b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/DynamicCollectorMock.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\Data\PolicyInterface; + +/** + * Helps with testing CSP policies. + */ +class DynamicCollectorMock extends DynamicCollector +{ + /** + * @var PolicyInterface[] + */ + private $added = []; + + /** + * @inheritDoc + */ + public function add(PolicyInterface $policy): void + { + $this->added[] = $policy; + + parent::add($policy); + } + + /** + * Collect added policies and start a new cycle. + * + * @return PolicyInterface[] + */ + public function consumeAdded(): array + { + $policies = $this->added; + $this->added = []; + + return $policies; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php index e9e9ed99ecd7c..388d35d1285b2 100644 --- a/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php @@ -53,13 +53,7 @@ public function testRenderRestrictMode(): void $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy')); $this->assertEmpty($this->response->getHeader('Content-Security-Policy-Report-Only')); - $contentSecurityPolicyContent = []; - if ($header instanceof \ArrayIterator) { - foreach ($header as $item) { - $contentSecurityPolicyContent[] = $item->getFieldValue(); - } - } - $this->assertEquals(['default-src https://magento.com \'self\';'], $contentSecurityPolicyContent); + $this->assertEquals('default-src https://magento.com \'self\';', $header->getFieldValue()); } /** @@ -79,15 +73,9 @@ public function testRenderRestrictWithReportingMode(): void $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy')); $this->assertEmpty($this->response->getHeader('Content-Security-Policy-Report-Only')); - $contentSecurityPolicyContent = []; - if ($header instanceof \ArrayIterator) { - foreach ($header as $item) { - $contentSecurityPolicyContent[] = $item->getFieldValue(); - } - } $this->assertEquals( - ['default-src https://magento.com \'self\'; report-uri /csp-reports/; report-to report-endpoint;'], - $contentSecurityPolicyContent + 'default-src https://magento.com \'self\'; report-uri /csp-reports/; report-to report-endpoint;', + $header->getFieldValue() ); $this->assertNotEmpty($reportToHeader = $this->response->getHeader('Report-To')); $this->assertNotEmpty($reportData = json_decode("[{$reportToHeader->getFieldValue()}]", true)); @@ -111,7 +99,7 @@ public function testRenderReportMode(): void ['https://magento.com'], ['https'], true, - true, + false, true, ['5749837589457695'], ['B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256'], @@ -124,13 +112,48 @@ public function testRenderReportMode(): void $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy-Report-Only')); $this->assertEmpty($this->response->getHeader('Content-Security-Policy')); $this->assertEquals( - 'default-src https://magento.com https: \'self\' \'unsafe-inline\' \'unsafe-eval\' \'strict-dynamic\'' + 'default-src https://magento.com https: \'self\' \'unsafe-eval\' \'strict-dynamic\'' . ' \'unsafe-hashes\' \'nonce-'.base64_encode($policy->getNonceValues()[0]).'\'' . ' \'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=\';', $header->getFieldValue() ); } + /** + * Test rendering a fetch policy with inline allowed. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/mode/storefront/report_only 1 + * @magentoConfigFixture default_store csp/mode/storefront/report_uri 0 + * + * @return void + */ + public function testFetchPolicyInlineAllowed(): void + { + $policy = new FetchPolicy( + 'script-src', + false, + ['https://magento.com'], + ['https'], + true, + true, + true, + ['5749837589457695'], + ['B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256'], + false, + false + ); + + $this->renderer->render($policy, $this->response); + + $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy-Report-Only')); + $this->assertEmpty($this->response->getHeader('Content-Security-Policy')); + $this->assertEquals( + 'script-src https://magento.com https: \'self\' \'unsafe-inline\' \'unsafe-eval\';', + $header->getFieldValue() + ); + } + /** * Test policy rendering in report-only mode with report URL provided. * diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Account/AuthenticationPopupTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Account/AuthenticationPopupTest.php new file mode 100644 index 0000000000000..5744e5e05f5fc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Account/AuthenticationPopupTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Account; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests for authentication popup block. + * + * @see \Magento\Customer\Block\Account\AuthenticationPopup + * @magentoAppArea frontend + */ +class AuthenticationPopupTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var AuthenticationPopup */ + private $block; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(AuthenticationPopup::class); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 1 + * + * @return void + */ + public function testAutocompletePasswordEnabled(): void + { + $this->assertEquals('on', $this->block->getConfig()['autocomplete']); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 0 + * + * @return void + */ + public function testAutocompletePasswordDisabled(): void + { + $this->assertEquals('off', $this->block->getConfig()['autocomplete']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Account/ResetPasswordTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Account/ResetPasswordTest.php index 80d77a3f90b1c..36de4386a7938 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Account/ResetPasswordTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Account/ResetPasswordTest.php @@ -83,4 +83,24 @@ public function testResetPasswordForm(): void 'Set password button was not found on the page' ); } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 1 + * + * @return void + */ + public function testAutocompletePasswordEnabled(): void + { + $this->assertFalse($this->block->isAutocompleteDisabled()); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 0 + * + * @return void + */ + public function testAutocompletePasswordDisabled(): void + { + $this->assertTrue($this->block->isAutocompleteDisabled()); + } } 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/Block/Adminhtml/Edit/OrderButtonTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/OrderButtonTest.php new file mode 100644 index 0000000000000..1a093c741b1b2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/OrderButtonTest.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit; + +use Magento\Backend\Model\Search\AuthorizationMock; +use Magento\Customer\Controller\RegistryConstants; +use Magento\Framework\Authorization; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Class checks Create Order button visibility + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class OrderButtonTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var OrderButton */ + private $button; + + /** @var Registry */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->addSharedInstance( + $this->objectManager->get(AuthorizationMock::class), + Authorization::class + ); + $this->button = $this->objectManager->get(OrderButton::class); + $this->registry = $this->objectManager->get(Registry::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + + parent::tearDown(); + } + + /** + * @return void + */ + public function testGetButtonDataWithoutCustomer(): void + { + $this->assertEmpty($this->button->getButtonData()); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testGetButtonDataWithCustomer(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->registry->register(RegistryConstants::CURRENT_CUSTOMER_ID, 1); + $data = $this->button->getButtonData(); + $this->assertNotEmpty($data); + $this->assertEquals(__('Create Order'), $data['label']); + $this->assertStringContainsString('sales/order_create/start/customer_id/1/', $data['on_click']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/AbstractCartTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/AbstractCartTest.php new file mode 100644 index 0000000000000..3681da9a10396 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/AbstractCartTest.php @@ -0,0 +1,148 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Controller\RegistryConstants; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\QuoteFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Base class for testing \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart block + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +abstract class AbstractCartTest extends TestCase +{ + const CUSTOMER_ID_VALUE = 1234; + + /** @var Registry */ + private $registry; + + /** @var Cart */ + protected $block; + + /** @var ObjectManagerInterface */ + protected $objectManager; + + /** @var CartRepositoryInterface */ + protected $quoteRepository; + + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var QuoteFactory */ + private $quoteFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->registerCustomerId(self::CUSTOMER_ID_VALUE); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Cart::class); + $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->quoteFactory = $this->objectManager->get(QuoteFactory::class); + } + + /** + * @inheritdoc + */ + public function tearDown(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + } + + /** + * Check that the expected items of the shopping cart are in the block + * + * @param string $customerEmail + * @return void + */ + protected function processCheckQuoteItems(string $customerEmail): void + { + $customer = $this->customerRepository->get($customerEmail); + $this->registerCustomerId((int)$customer->getId()); + $this->block->toHtml(); + + $quoteItemIds = $this->getQuoteItemIds((int)$customer->getId()); + $this->assertCount( + count($quoteItemIds), + $this->block->getPreparedCollection(), + "Item's count in the customer cart grid block doesn't match expected count." + ); + $this->assertEmpty( + array_diff( + $this->block->getPreparedCollection()->getAllIds(), + $quoteItemIds + ), + "Items in the customer cart grid block doesn't match expected items." + ); + } + + /** + * Checks that customer's shopping cart block is empty + * + * @param string $customerEmail + * @return void + */ + protected function processCheckWithoutQuoteItems(string $customerEmail): void + { + $customer = $this->customerRepository->get($customerEmail); + $this->registerCustomerId((int)$customer->getId()); + $this->block->toHtml(); + + $this->assertCount( + 0, + $this->block->getPreparedCollection(), + "Item's count in the customer cart grid block doesn't match expected count." + ); + } + + /** + * Add customer id to registry. + * + * @param int $customerId + * @return void + */ + private function registerCustomerId(int $customerId): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->registry->register(RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); + } + + /** + * Get shopping cart quote item identifiers by customer id. + * + * @param int $customerId + * @return array + */ + private function getQuoteItemIds(int $customerId): array + { + $ids = []; + /** @var Quote $quote */ + $quote = $this->quoteRepository->getForCustomer($customerId); + /** @var Item $item */ + foreach ($quote->getItems() as $item) { + $ids[] = $item->getId(); + } + + return $ids; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartBundleTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartBundleTest.php new file mode 100644 index 0000000000000..22cb852c73f63 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartBundleTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\Cart; + +use Magento\Customer\Block\Adminhtml\Edit\Tab\AbstractCartTest; +use Magento\Framework\Module\Manager; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class checks customer's shopping cart block with bundle product. + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart + * @magentoAppArea adminhtml + */ +class CartBundleTest extends AbstractCartTest +{ + /** @var CollectionFactory */ + private $quoteCollectionFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->quoteCollectionFactory = $this->objectManager->get(CollectionFactory::class); + } + + /** + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Manager $moduleManager */ + $moduleManager = $objectManager->get(Manager::class); + //This check is needed because Customer independent of Magento_Bundle + if (!$moduleManager->isEnabled('Magento_Bundle')) { + self::markTestSkipped('Magento_Bundle module disabled.'); + } + } + + /** + * @magentoDataFixture Magento/Bundle/_files/quote_with_bundle_and_options.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void + */ + public function testBundleProductView(): void + { + $quoteCollection = $this->quoteCollectionFactory->create(); + $quoteCollection->addFieldToFilter('reserved_order_id', 'test_cart_with_bundle_and_options'); + /** @var Quote $quote */ + $quote = $quoteCollection->getFirstItem(); + $this->assertNotEmpty($quote->getId()); + $quote->setCustomerId(1); + $this->quoteRepository->save($quote); + $this->processCheckQuoteItems('customer@example.com'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartConfigurableTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartConfigurableTest.php new file mode 100644 index 0000000000000..613edc5aec4f9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartConfigurableTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\Cart; + +use Magento\Customer\Block\Adminhtml\Edit\Tab\AbstractCartTest; +use Magento\Framework\Module\Manager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class checks customer's shopping cart block with configurable product. + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart + * @magentoAppArea adminhtml + */ +class CartConfigurableTest extends AbstractCartTest +{ + /** + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Manager $moduleManager */ + $moduleManager = $objectManager->get(Manager::class); + //This check is needed because Customer independent of Magento_ConfigurableProduct + if (!$moduleManager->isEnabled('Magento_ConfigurableProduct')) { + self::markTestSkipped('Magento_ConfigurableProduct module disabled.'); + } + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php + * @return void + */ + public function testConfigurableProductView(): void + { + $this->processCheckQuoteItems('customer_uk_address@test.com'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CollectionTest.php new file mode 100644 index 0000000000000..b37870535d9da --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CollectionTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\Cart; + +use Magento\Customer\Block\Adminhtml\Edit\Tab\Cart; +use Magento\Customer\Controller\RegistryConstants; +use Magento\Framework\Data\Collection; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Model\QuoteRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use PHPUnit\Framework\TestCase; + +/** + * Class checks that shopping cart grid can be filtered + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart::_prepareCollection() + * + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + */ +class CollectionTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** @var Registry */ + private $registry; + + /** @var LayoutInterface */ + private $layout; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoDataFixture Magento/Checkout/_files/customer_quote_on_second_website.php + * + * @return void + */ + public function testCollectionOnDifferentStores(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->registry->register(RegistryConstants::CURRENT_CUSTOMER_ID, 1); + $collectionFirstWebsite = $this->executeInStoreContext->execute( + 'default', + [$this->layout->createBlock(Cart::class), 'getPreparedCollection'] + ); + $this->assertCollection($collectionFirstWebsite, 'Simple Product'); + $this->objectManager->removeSharedInstance(QuoteRepository::class); + $collectionSecondWebsite = $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this->layout->createBlock(Cart::class), 'getPreparedCollection'] + ); + $this->assertCollection($collectionSecondWebsite, 'Simple Product on second website'); + } + + /** + * Check is collection match expected value + * + * @param Collection $collection + * @param string $itemName + * @return void + */ + private function assertCollection(Collection $collection, string $itemName): void + { + $this->assertCount(1, $collection, 'Collection size does not match expected value'); + $this->assertEquals($itemName, $collection->getFirstItem()->getName()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/StoreSwitcherTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/StoreSwitcherTest.php new file mode 100644 index 0000000000000..5aba76b37e74a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/StoreSwitcherTest.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\Cart; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Class checks store switcher appearance in the customer shopping cart block. + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class StoreSwitcherTest extends TestCase +{ + private const WEBSITE_FILTER_XPATH = "//select[@name='website_id' and @id='website_filter']"; + + private const WEBSITE_FILTER_OPTION_XPATH = "//select[@name='website_id' and @id='website_filter']/option"; + + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var PageFactory */ + private $pageFactory; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + } + + /** + * @return void + */ + public function testStoreSwitcherDisplayed(): void + { + $html = $this->getBlockHtml('admin.customer.view.edit.cart'); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(self::WEBSITE_FILTER_XPATH, $html), + 'Website Filter was not found on the page' + ); + $this->checkFilterOptions($html, [$this->storeManager->getWebsite('base')->getName()]); + } + + /** + * @magentoConfigFixture current_store general/single_store_mode/enabled 1 + * + * @return void + */ + public function testStoreSwitcherIsNotDisplayed(): void + { + $html = $this->getBlockHtml('admin.customer.view.edit.cart'); + $this->assertEmpty(Xpath::getElementsCountForXpath(self::WEBSITE_FILTER_XPATH, $html)); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * + * @return void + */ + public function testStoreSwitcherMultiWebsite(): void + { + $expectedWebsites = [ + $this->storeManager->getWebsite('base')->getName(), + $this->storeManager->getWebsite('test')->getName(), + ]; + $html = $this->getBlockHtml('admin.customer.view.edit.cart'); + $this->assertEquals(1, Xpath::getElementsCountForXpath(self::WEBSITE_FILTER_XPATH, $html)); + $this->checkFilterOptions($html, $expectedWebsites); + } + + /** + * Check store switcher appearance + * + * @param string $html + * @param array $expectedOptions + * @return void + */ + private function checkFilterOptions(string $html, array $expectedOptions): void + { + $this->assertEquals( + count($expectedOptions), + Xpath::getElementsCountForXpath(self::WEBSITE_FILTER_OPTION_XPATH, $html), + 'Website filter options count does not match expected value' + ); + $optionPath = self::WEBSITE_FILTER_OPTION_XPATH . "[contains(text(), '%s')]"; + foreach ($expectedOptions as $option) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($optionPath, $option), $html), + sprintf('Option for %s website was not found in filter options list', $option) + ); + } + } + + /** + * Get block html + * + * @param string $alias + * @return string + */ + private function getBlockHtml(string $alias): string + { + $page = $this->preparePage(); + $block = $page->getLayout()->getBlock($alias); + $this->assertNotFalse($block); + + return $block->toHtml(); + } + + /** + * Prepare page layout + * + * @return Page + */ + private function preparePage(): Page + { + $page = $this->pageFactory->create(); + $page->addHandle(['default', 'customer_index_cart']); + $page->getLayout()->generateXml(); + + return $page; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/CartTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/CartTest.php index b5abf1de5732b..df799a0878b59 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/CartTest.php @@ -3,82 +3,39 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Block\Adminhtml\Edit\Tab; -use Magento\Backend\Block\Template\Context; use Magento\Backend\Model\Session\Quote as SessionQuote; -use Magento\Customer\Controller\RegistryConstants; -use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\Registry; use Magento\Quote\Model\Quote; -use Magento\Store\Model\StoreManagerInterface; /** - * Magento\Customer\Block\Adminhtml\Edit\Tab\Cart + * Class checks customer's shopping cart block with simple product and simple product with options. * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart * @magentoAppArea adminhtml - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CartTest extends \PHPUnit\Framework\TestCase +class CartTest extends AbstractCartTest { - const CUSTOMER_ID_VALUE = 1234; - - /** - * @var Context - */ - private $_context; - - /** - * @var Registry - */ - private $_coreRegistry; - - /** - * @var StoreManagerInterface - */ - private $_storeManager; - - /** - * @var Cart - */ - private $_block; - - /** - * @var ObjectManagerInterface - */ - private $_objectManager; - /** - * @inheritdoc + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + * + * @return void */ - protected function setUp(): void + public function testProductOptionsView(): void { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $this->_storeManager = $this->_objectManager->get(\Magento\Store\Model\StoreManager::class); - $this->_context = $this->_objectManager->get( - \Magento\Backend\Block\Template\Context::class, - ['storeManager' => $this->_storeManager] - ); - - $this->_coreRegistry = $this->_objectManager->get(\Magento\Framework\Registry::class); - $this->_coreRegistry->register(RegistryConstants::CURRENT_CUSTOMER_ID, self::CUSTOMER_ID_VALUE); - - $this->_block = $this->_objectManager->get( - \Magento\Framework\View\LayoutInterface::class - )->createBlock( - \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart::class, - '', - ['context' => $this->_context, 'registry' => $this->_coreRegistry] - ); + $this->processCheckQuoteItems('customer_uk_address@test.com'); } /** - * @inheritdoc + * @magentoDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoDataFixture Magento/Customer/_files/two_customers.php + * @return void */ - protected function tearDown(): void + public function testCustomerWithoutQuoteView(): void { - $this->_coreRegistry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->processCheckWithoutQuoteItems('customer_two@example.com'); } /** @@ -95,23 +52,23 @@ protected function tearDown(): void */ public function testVerifyCollectionWithQuote(int $customerId, bool $guest, bool $contains): void { - $session = $this->_objectManager->create(SessionQuote::class); + $session = $this->objectManager->create(SessionQuote::class); $session->setCustomerId($customerId); - $quoteFixture = $this->_objectManager->create(Quote::class); + $quoteFixture = $this->objectManager->create(Quote::class); $quoteFixture->load('test01', 'reserved_order_id'); $quoteFixture->setCustomerIsGuest($guest) ->setCustomerId($customerId) ->save(); - $this->_block->toHtml(); + $this->block->toHtml(); if ($contains) { $this->assertStringContainsString( "We couldn't find any records", - $this->_block->getGridParentHtml() + $this->block->getGridParentHtml() ); } else { $this->assertStringNotContainsString( "We couldn't find any records", - $this->_block->getGridParentHtml() + $this->block->getGridParentHtml() ); } } @@ -144,7 +101,7 @@ public function getQuoteDataProvider(): array */ public function testGetCustomerId(): void { - $this->assertEquals(self::CUSTOMER_ID_VALUE, $this->_block->getCustomerId()); + $this->assertEquals(self::CUSTOMER_ID_VALUE, $this->block->getCustomerId()); } /** @@ -154,7 +111,7 @@ public function testGetCustomerId(): void */ public function testGetGridUrl(): void { - $this->assertStringContainsString('/backend/customer/index/cart', $this->_block->getGridUrl()); + $this->assertStringContainsString('/backend/customer/index/cart', $this->block->getGridUrl()); } /** @@ -164,20 +121,13 @@ public function testGetGridUrl(): void */ public function testGetGridParentHtml(): void { - $this->_block = $this->_objectManager->get( - \Magento\Framework\View\LayoutInterface::class - )->createBlock( - \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart::class, - '', - [] - ); $mockCollection = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) ->disableOriginalConstructor() ->getMock(); - $this->_block->setCollection($mockCollection); + $this->block->setCollection($mockCollection); $this->assertStringContainsString( "<div class=\"admin__data-grid-header admin__data-grid-toolbar\"", - $this->_block->getGridParentHtml() + $this->block->getGridParentHtml() ); } @@ -190,7 +140,7 @@ public function testGetRowUrl(): void { $row = new \Magento\Framework\DataObject(); $row->setProductId(1); - $this->assertStringContainsString('/backend/catalog/product/edit/id/1', $this->_block->getRowUrl($row)); + $this->assertStringContainsString('/backend/catalog/product/edit/id/1', $this->block->getRowUrl($row)); } /** @@ -200,7 +150,7 @@ public function testGetRowUrl(): void */ public function testGetHtml(): void { - $html = $this->_block->toHtml(); + $html = $this->block->toHtml(); $this->assertStringContainsString("<div id=\"customer_cart_grid\"", $html); $this->assertStringContainsString("<div class=\"admin__data-grid-header admin__data-grid-toolbar\"", $html); $this->assertStringContainsString("customer_cart_gridJsObject = new varienGrid(\"customer_cart_grid\",", $html); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/AbstractItemTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/AbstractItemTest.php new file mode 100644 index 0000000000000..82a1f3647b786 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/AbstractItemTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Helper\Product\Configuration; +use Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item as RendererItem; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Base class for testing \Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item block + */ +abstract class AbstractItemTest extends TestCase +{ + /** @var ObjectManager */ + protected $objectManager; + + /** @var RendererItem */ + private $blockRendererItem; + + /** @var CollectionFactory */ + private $quoteItemCollectionFactory; + + /** @var Configuration */ + private $productConfiguration; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->blockRendererItem = $this->objectManager->get(LayoutInterface::class)->createBlock(RendererItem::class); + $this->quoteItemCollectionFactory = $this->objectManager->get(CollectionFactory::class); + $this->productConfiguration = $this->objectManager->get(Configuration::class); + } + + /** + * Check item block rendering + * + * @return void + */ + protected function processRender(): void + { + $itemsCollection = $this->quoteItemCollectionFactory->create(); + /** @var Item $quoteItem */ + $quoteItem = $itemsCollection->getFirstItem(); + $this->assertNotEmpty($quoteItem->getId()); + $this->blockRendererItem->setProductHelpers([]); + $html = $this->blockRendererItem->render($quoteItem); + + $this->assertRendererItemValue($quoteItem, $html); + } + + /** + * Check that the product name and options are in the block. + * + * @param Item $quoteItem + * @param string $html + * @return void + */ + private function assertRendererItemValue(Item $quoteItem, string $html): void + { + $optionsXPath = $this->getOptionsValueXPath($quoteItem); + $productName = $quoteItem->getProduct()->getName(); + + $productNameXPath = count($optionsXPath) === 0 ? "/descendant::*[contains(text(), '$productName')]" + : "//div[contains(@class, 'product-title') and contains(text(), '$productName')]"; + + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($productNameXPath, $html), + 'The block\'s rendered value does not contain expected product name.' + ); + foreach ($optionsXPath as $option) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($option['xpath'], $html), + sprintf('The block\'s rendered value does not contain expected option. Option: %s', $option['label']) + ); + } + } + + /** + * Get item options and their xpath expression + * + * @param Item $quoteItem + * @return array + */ + private function getOptionsValueXPath(Item $quoteItem): array + { + $options = $this->productConfiguration->getOptions($quoteItem); + foreach ($options as $key => $option) { + $options[$key]['xpath'] = "//dl[contains(@class, 'item-options')]" + . "/dt[contains(text(), '{$option['label']}')]" + . "/following-sibling::dd[1]"; + + if (isset($option['option_type']) + && $option['option_type'] == ProductCustomOptionInterface::OPTION_GROUP_FILE) { + $value = explode(" ", $option['print_value']); + $options[$key]['xpath'] .= "/a[contains(text(), '{$value[0]}')]"; + } else { + $options[$key]['xpath'] .= "[contains(text(), '{$option['value']}')]"; + } + } + + return $options; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/Item/ItemConfigurableTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/Item/ItemConfigurableTest.php new file mode 100644 index 0000000000000..adcae7c5b434d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/Item/ItemConfigurableTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item; + +use Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\AbstractItemTest; +use Magento\Framework\Module\Manager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class checks item block rendering with configurable product. + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item + */ +class ItemConfigurableTest extends AbstractItemTest +{ + /** + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Manager $moduleManager */ + $moduleManager = $objectManager->get(Manager::class); + //This check is needed because Customer independent of Magento_ConfigurableProduct + if (!$moduleManager->isEnabled('Magento_ConfigurableProduct')) { + self::markTestSkipped('Magento_ConfigurableProduct module disabled.'); + } + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php + * @return void + */ + public function testRenderConfigurableProduct(): void + { + $this->processRender(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/ItemTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/ItemTest.php new file mode 100644 index 0000000000000..1a26d22b4cc5b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/ItemTest.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer; + +/** + * Class checks item block rendering with simple product and simple product with options. + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item + */ +class ItemTest extends AbstractItemTest +{ + /** + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + * @return void + */ + public function testRenderProductOptions(): void + { + $this->processRender(); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @return void + */ + public function testRenderSimpleProduct(): void + { + $this->processRender(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/WishlistTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/WishlistTest.php new file mode 100644 index 0000000000000..d4ae576523b15 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/WishlistTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\View; + +use Magento\Customer\Controller\RegistryConstants; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests for customer wish list tab. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + */ +class WishlistTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Registry */ + private $registry; + + /** @var Wishlist */ + private $block; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Wishlist::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + * + * @return void + */ + public function testWishListGrid(): void + { + $this->registerCustomerId(1); + $this->assertCount(1, $this->block->getPreparedCollection()); + } + + /** + * Add customer id to registry. + * + * @param int $customerId + * @return void + */ + private function registerCustomerId(int $customerId): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->registry->register(RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/AbstractMultiactionTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/AbstractMultiactionTest.php new file mode 100644 index 0000000000000..02d7c886ec2e2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/AbstractMultiactionTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Grid\Renderer; + +use Magento\Backend\Block\Widget\Grid\Column\Extended; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Base class for testing \Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction block + */ +abstract class AbstractMultiactionTest extends TestCase +{ + /** @var ObjectManager */ + protected $objectManager; + + /** @var Extended */ + protected $blockColumn; + + /** @var Multiaction */ + protected $blockMultiaction; + + /** @var LayoutInterface */ + private $layout; + + /** @var CollectionFactory */ + private $quoteItemCollectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->blockColumn = $this->layout->createBlock(Extended::class); + $this->blockColumn->setData([ + 'header' => 'Action', + 'index' => 'item_id', + 'renderer' => Multiaction::class, + 'filter' => false, + 'sortable' => false, + ]); + $this->blockMultiaction = $this->layout->createBlock(Multiaction::class); + $this->quoteItemCollectionFactory = $this->objectManager->get(CollectionFactory::class); + } + + /** + * Check multiaction block rendering + * + * @return void + */ + protected function processRender(): void + { + $itemsCollection = $this->quoteItemCollectionFactory->create(); + /** @var Item $quoteItem */ + $quoteItem = $itemsCollection->getFirstItem(); + $this->assertNotEmpty($quoteItem->getId()); + $actions = [ + [ + 'caption' => 'configure', + 'url' => 'url_configureItem', + 'process' => 'configurable', + 'control_object' => 'cartControl', + ], + [ + 'caption' => 'delete', + 'url' => 'url_removeItem', + 'onclick' => 'return cartControl.removeItem($item_id);' + ], + ]; + $this->blockColumn->addData(['actions' => $actions]); + $this->blockMultiaction->setColumn($this->blockColumn); + $html = $this->blockMultiaction->render($quoteItem); + + foreach ($actions as $action) { + $this->assertUrl((int)$quoteItem->getId(), $action, $html); + } + } + + /** + * Check that the link in the block is correct + * + * @param int $quoteItemId + * @param array $action + * @param string $html + * @return void + */ + private function assertUrl(int $quoteItemId, array $action, string $html): void + { + $jsFunction = str_replace('url_', '', $action['url']); + $configureXPath = "//a[text()='{$action['caption']}' and @href='{$action['url']}']"; + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($configureXPath, $html), + sprintf('Expected %s link is incorrect or missing', $action['caption']) + ); + $this->assertStringContainsString("return cartControl.$jsFunction($quoteItemId)", $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionBundleTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionBundleTest.php new file mode 100644 index 0000000000000..2e2385c5088f2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionBundleTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction; + +use Magento\Customer\Block\Adminhtml\Grid\Renderer\AbstractMultiactionTest; +use Magento\Framework\Module\Manager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class checks multiaction block rendering with bundle product. + * + * @see \Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction + */ +class MultiactionBundleTest extends AbstractMultiactionTest +{ + /** + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Manager $moduleManager */ + $moduleManager = $objectManager->get(Manager::class); + //This check is needed because Customer independent of Magento_Bundle + if (!$moduleManager->isEnabled('Magento_Bundle')) { + self::markTestSkipped('Magento_Bundle module disabled.'); + } + } + + /** + * @magentoDataFixture Magento/Bundle/_files/quote_with_bundle_and_options.php + * @return void + */ + public function testRenderConfigurableProduct(): void + { + $this->processRender(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionConfigurableTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionConfigurableTest.php new file mode 100644 index 0000000000000..ae279552c122a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionConfigurableTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction; + +use Magento\Customer\Block\Adminhtml\Grid\Renderer\AbstractMultiactionTest; +use Magento\Framework\Module\Manager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class checks multiaction block rendering with configurable product. + * + * @see \Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction + */ +class MultiactionConfigurableTest extends AbstractMultiactionTest +{ + /** + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Manager $moduleManager */ + $moduleManager = $objectManager->get(Manager::class); + //This check is needed because Customer independent of Magento_ConfigurableProduct + if (!$moduleManager->isEnabled('Magento_ConfigurableProduct')) { + self::markTestSkipped('Magento_ConfigurableProduct module disabled.'); + } + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php + * @return void + */ + public function testRenderConfigurableProduct(): void + { + $this->processRender(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/MultiactionTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/MultiactionTest.php new file mode 100644 index 0000000000000..430fba8458c29 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/MultiactionTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Grid\Renderer; + +use Magento\Framework\DataObject; + +/** + * Class checks multiaction block rendering with simple product and simple product with options. + * + * @see \Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction + */ +class MultiactionTest extends AbstractMultiactionTest +{ + /** + * @dataProvider renderEmptyProvider + * @param array $columnData + * @return void + */ + public function testRenderEmpty(array $columnData): void + { + /** @var DataObject $row */ + $row = $this->objectManager->create(DataObject::class); + $this->blockColumn->addData($columnData); + $this->blockMultiaction->setColumn($this->blockColumn); + $this->assertEquals( + ' ', + $this->blockMultiaction->render($row) + ); + } + + /** + * Data provider for testRenderEmpty + * + * @return array + */ + public function renderEmptyProvider(): array + { + return [ + 'empty_actions' => [ + 'column_data' => ['actions' => []], + ], + 'not_array_actions' => [ + 'column_data' => ['actions' => 'actions'], + ], + 'empty_actions_element' => [ + 'column_data' => [ + 'actions' => [ + 'action_1' => 'actions', + ], + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + * @return void + */ + public function testRenderProductOptions(): void + { + $this->processRender(); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @return void + */ + public function testRenderSimpleProduct(): void + { + $this->markTestSkipped('Test is blocked by issue MC-34612'); + $this->processRender(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php index 6788d44d8e536..5c4a29f0a9d32 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php @@ -89,4 +89,24 @@ public function testLoginForm(): void 'Forgot password link does not exist on the page' ); } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 1 + * + * @return void + */ + public function testAutocompletePasswordEnabled(): void + { + $this->assertFalse($this->block->isAutocompleteDisabled()); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 0 + * + * @return void + */ + public function testAutocompletePasswordDisabled(): void + { + $this->assertTrue($this->block->isAutocompleteDisabled()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php index e6d3c5aa39d15..1d06aa7201f64 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php @@ -138,6 +138,22 @@ public function testFaxEnabled(): void $this->assertStringContainsString('title="Fax"', $block->toHtml()); } + /** + * @magentoDataFixture Magento/Customer/_files/attribute_city_store_label_address.php + */ + public function testCityWithStoreLabel(): void + { + /** @var \Magento\Customer\Block\Form\Register $block */ + $block = Bootstrap::getObjectManager()->create( + Register::class + )->setTemplate('Magento_Customer::form/register.phtml') + ->setShowAddressFields(true); + $this->setAttributeDataProvider($block); + + $this->assertStringNotContainsString('title="City"', $block->toHtml()); + $this->assertStringContainsString('title="Suburb"', $block->toHtml()); + } + /** * @inheritdoc */ 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 c2e55029cab13..6abbff18c645c 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -10,27 +10,26 @@ 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; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; use Magento\Theme\Controller\Result\MessagePlugin; use PHPUnit\Framework\Constraint\StringContains; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AccountTest extends \Magento\TestFramework\TestCase\AbstractController +class AccountTest extends AbstractController { /** * @var TransportBuilderMock @@ -54,9 +53,8 @@ protected function setUp(): void */ protected function login($customerId) { - /** @var \Magento\Customer\Model\Session $session */ - $session = Bootstrap::getObjectManager() - ->get(\Magento\Customer\Model\Session::class); + /** @var Session $session */ + $session = Bootstrap::getObjectManager()->get(Session::class); $session->loginById($customerId); } @@ -148,8 +146,8 @@ public function testCreatepasswordActionWithSession() $customer->setData('confirmation', 'confirmation'); $customer->save(); - /** @var \Magento\Customer\Model\Session $customer */ - $session = Bootstrap::getObjectManager()->get(\Magento\Customer\Model\Session::class); + /** @var Session $customer */ + $session = Bootstrap::getObjectManager()->get(Session::class); $session->setRpToken($token); $session->setRpCustomerId($customer->getId()); @@ -219,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 */ @@ -404,18 +325,16 @@ public function testEditAction() $this->assertEquals(200, $this->getResponse()->getHttpResponseCode(), $body); $this->assertStringContainsString('<div class="field field-name-firstname required">', $body); // Verify the password check box is not checked - $expectedString = <<<EXPECTED_HTML -<input type="checkbox" name="change_password" id="change-password" data-role="change-password" value="1" - title="Change Password" - class="checkbox" /> -EXPECTED_HTML; - $this->assertStringContainsString($expectedString, $body); + $checkboxXpath = '//input[@type="checkbox"][@name="change_password"][@id="change-password"][not (@checked)]' . + '[@data-role="change-password"][@value="1"][@title="Change Password"][@class="checkbox"]'; + + $this->assertEquals(1, Xpath::getElementsCountForXpath($checkboxXpath, $body)); } /** * @magentoDataFixture Magento/Customer/_files/customer.php */ - public function testChangePasswordEditAction() + public function testChangePasswordEditAction(): void { $this->login(1); @@ -425,12 +344,11 @@ public function testChangePasswordEditAction() $this->assertEquals(200, $this->getResponse()->getHttpResponseCode(), $body); $this->assertStringContainsString('<div class="field field-name-firstname required">', $body); // Verify the password check box is checked - $expectedString = <<<EXPECTED_HTML -<input type="checkbox" name="change_password" id="change-password" data-role="change-password" value="1" - title="Change Password" - checked="checked" class="checkbox" /> -EXPECTED_HTML; - $this->assertStringContainsString($expectedString, $body); + $checkboxXpath = '//input[@type="checkbox"][@name="change_password"][@id="change-password"]' . + '[@data-role="change-password"][@value="1"][@title="Change Password"][@checked="checked"]' . + '[@class="checkbox"]'; + + $this->assertEquals(1, Xpath::getElementsCountForXpath($checkboxXpath, $body)); } /** @@ -615,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. * @@ -869,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/Controller/Adminhtml/Cart/Product/Composite/Cart/ConfigureTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/Cart/ConfigureTest.php new file mode 100644 index 0000000000000..87a4eb15f1913 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/Cart/ConfigureTest.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Adminhtml\Cart\Product\Composite\Cart; + +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Tests for configure quote item in customer shopping cart. + * + * @magentoAppArea adminhtml + */ +class ConfigureTest extends AbstractBackendController +{ + /** @var CollectionFactory */ + private $quoteItemCollectionFactory; + + /** @var int */ + private $baseWebsiteId; + + /** @var SerializerInterface */ + private $json; + + /** @inheritdoc */ + public function setUp(): void + { + parent::setUp(); + $this->quoteItemCollectionFactory = $this->_objectManager->get(CollectionFactory::class); + $this->baseWebsiteId = (int)$this->_objectManager->get(StoreManagerInterface::class) + ->getWebsite('base') + ->getId(); + $this->json = $this->_objectManager->get(SerializerInterface::class); + } + + /** + * @return void + */ + public function testConfigureActionNoCustomerId(): void + { + $this->dispatchCompositeCartConfigure(); + $this->assertEquals( + [ + 'error' => true, + 'message' => "The customer ID isn't defined.", + ], + $this->json->unserialize($this->getResponse()->getBody()) + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void + */ + public function testConfigureNoQuoteId(): void + { + $this->dispatchCompositeCartConfigure([ + 'customer_id' => 1, + 'website_id' => $this->baseWebsiteId, + ]); + $this->assertEquals( + [ + 'error' => true, + 'message' => "The quote items are incorrect. Verify the quote items and try again.", + ], + $this->json->unserialize($this->getResponse()->getBody()) + ); + } + + /** + * @dataProvider configureWithQuoteProvider + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/quote.php + * @param bool $hasQuoteItem + * @param string $expectedResponseBody + * @return void + */ + public function testConfigureWithQuote(bool $hasQuoteItem, string $expectedResponseBody): void + { + $itemsCollection = $this->quoteItemCollectionFactory->create(); + $itemId = $itemsCollection->getFirstItem()->getId(); + $this->assertNotEmpty($itemId); + if (!$hasQuoteItem) { + $itemId++; + } + $this->dispatchCompositeCartConfigure([ + 'customer_id' => 1, + 'website_id' => $this->baseWebsiteId, + 'id' => $itemId, + ]); + $this->assertStringContainsString( + $expectedResponseBody, + $this->getResponse()->getBody() + ); + } + + /** + * Create configure with quote provider + * + * @return array + */ + public function configureWithQuoteProvider(): array + { + return [ + 'with_quote_item_id' => [ + 'has_quote_item' => true, + 'expected_response_body' => '<input id="product_composite_configure_input_qty"' + . ' class="input-text admin__control-text qty" type="text" name="qty" value="1">', + ], + 'without_quote_item_id' => [ + 'has_quote_item' => false, + 'expected_response_body' => '{"error":true,"message":"The quote items are incorrect.' + . ' Verify the quote items and try again."}', + ], + ]; + } + + /** + * Dispatch configure quote item in customer shopping cart + * using backend/customer/cart_product_composite_cart/configure action. + * + * @param array $params + * @param array $postValue + * @return void + */ + private function dispatchCompositeCartConfigure(array $params = [], array $postValue = []): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($params); + $this->getRequest()->setPostValue($postValue); + $this->dispatch('backend/customer/cart_product_composite_cart/configure'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/Cart/UpdateTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/Cart/UpdateTest.php new file mode 100644 index 0000000000000..fd0e7a8d95833 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/Cart/UpdateTest.php @@ -0,0 +1,325 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Adminhtml\Cart\Product\Composite\Cart; + +use Magento\Backend\Model\Session; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Model\Product\Option as ProductOption; +use Magento\Catalog\Model\Product\Option\Type\File\ValidatorInfo; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\DataObject; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Tests for update quote item in customer shopping cart. + * + * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class UpdateTest extends AbstractBackendController +{ + /** @var CollectionFactory */ + private $quoteItemCollectionFactory; + + /** @var Session */ + private $session; + + /** @var SerializerInterface */ + private $json; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var int */ + private $baseWebsiteId; + + /** @inheritdoc */ + public function setUp(): void + { + parent::setUp(); + $this->quoteItemCollectionFactory = $this->_objectManager->get(CollectionFactory::class); + $this->session = $this->_objectManager->get(Session::class); + $this->json = $this->_objectManager->get(SerializerInterface::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + $this->baseWebsiteId = (int)$this->_objectManager->get(StoreManagerInterface::class) + ->getWebsite('base') + ->getId(); + } + + /** + * @return void + */ + public function testUpdateNoCustomerId(): void + { + $expectedUpdateResult = [ + 'error' => true, + 'message' => (string)__("The customer ID isn't defined."), + 'js_var_name' => null, + ]; + $this->dispatchCompositeCartUpdate(); + /** @var DataObject $updateResult */ + $updateResult = $this->session->getCompositeProductResult(); + $this->assertEquals($expectedUpdateResult, $updateResult->getData()); + $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void + */ + public function testUpdateNoQuoteId(): void + { + $expectedUpdateResult = [ + 'error' => true, + 'message' => (string)__('The quote items are incorrect. Verify the quote items and try again.'), + 'js_var_name' => 'iFrameResponse', + ]; + $this->dispatchCompositeCartUpdate([ + 'customer_id' => 1, + 'website_id' => $this->baseWebsiteId, + 'as_js_varname' => 'iFrameResponse', + ]); + /** @var DataObject $updateResult */ + $updateResult = $this->session->getCompositeProductResult(); + $this->assertEquals($expectedUpdateResult, $updateResult->getData()); + $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); + } + + /** + * @dataProvider updateWithQuoteProvider + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/quote.php + * @param bool $hasQuoteItem + * @param array $expectedUpdateResult + * @return void + */ + public function testUpdateWithQuote(bool $hasQuoteItem, array $expectedUpdateResult): void + { + $itemsCollection = $this->quoteItemCollectionFactory->create(); + $itemId = $itemsCollection->getFirstItem()->getId(); + $this->assertNotEmpty($itemId); + if (!$hasQuoteItem) { + $itemId++; + } + $this->dispatchCompositeCartUpdate( + [ + 'customer_id' => 1, + 'website_id' => $this->baseWebsiteId, + ], + [ + 'id' => $itemId, + 'as_js_varname' => 'iFrameResponse', + 'qty' => 20, + ] + ); + /** @var DataObject $updateResult */ + $updateResult = $this->session->getCompositeProductResult(); + $this->assertEquals($expectedUpdateResult, $updateResult->getData()); + $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); + } + + /** + * Create update with quote provider + * + * @return array + */ + public function updateWithQuoteProvider(): array + { + return [ + 'with_quote_item_id' => [ + 'has_quote_item' => true, + 'expected_update_result' => [ + 'ok' => true, + 'js_var_name' => 'iFrameResponse', + ], + ], + 'without_quote_item_id' => [ + 'has_quote_item' => false, + 'expected_update_result' => [ + 'error' => true, + 'message' => (string)__('The quote items are incorrect. Verify the quote items and try again.'), + 'js_var_name' => 'iFrameResponse', + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + * @return void + */ + public function testUpdateSimpleProductOption(): void + { + $customer = $this->customerRepository->get('customer_uk_address@test.com'); + /** @var Quote $quote */ + $quote = $this->quoteRepository->getForCustomer($customer->getId()); + /** @var QuoteItem $quoteItem */ + $quoteItem = $quote->getItemsCollection()->getFirstItem(); + $this->assertNotEmpty($quoteItem->getId()); + $expectedData = $this->prepareExpectedData($quoteItem); + $expectedUpdateResult = [ + 'ok' => true, + 'js_var_name' => 'iFrameResponse', + ]; + $expectedParams = [ + 'id' => $quoteItem->getId(), + 'as_js_varname' => 'iFrameResponse', + 'options' => $expectedData['options'], + 'qty' => 5, + ]; + $this->dispatchCompositeCartUpdate( + [ + 'customer_id' => $customer->getId(), + 'website_id' => $customer->getWebsiteId(), + ], + $expectedParams + ); + /** @var DataObject $updateResult */ + $updateResult = $this->session->getCompositeProductResult(); + $this->assertEquals($expectedUpdateResult, $updateResult->getData()); + + $quoteItem = $this->getQuoteItemBySku($quote, $expectedData['sku']); + $this->assertNotNull($quoteItem, 'Missing expected shopping cart item after update.'); + $this->assertQuoteItemOptions($quoteItem, $expectedParams); + $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); + } + + /** + * Prepare quote item options and sku for update. + * + * @param QuoteItem $quoteItem + * @return array + */ + private function prepareExpectedData(QuoteItem $quoteItem): array + { + $buyRequest = $this->json->unserialize($quoteItem->getOptionByCode('info_buyRequest')->getValue()); + $productOptions = $quoteItem->getProduct()->getOptions(); + $options = []; + $sku = $quoteItem->getSku(); + /** @var ProductOption $productOption */ + foreach ($productOptions as $productOption) { + $itemOptionValue = $buyRequest['options'][$productOption->getId()]; + switch ($productOption->getType()) { + case ProductCustomOptionInterface::OPTION_TYPE_RADIO: + $productValues = $productOption->getValues(); + $currentRadioSku = $productValues[$itemOptionValue]->getSku(); + unset($productValues[$itemOptionValue]); + $value = (string)key($productValues); + $newRadioSku = $productValues[$value]->getSku(); + $sku = str_replace($currentRadioSku, $newRadioSku, $sku); + break; + case ProductCustomOptionInterface::OPTION_TYPE_DATE: + $value = ['year' => 2019, 'month' => 8, 'day' => 9, 'hour' => 13, 'minute' => 35]; + break; + case ProductCustomOptionInterface::OPTION_TYPE_FILE: + $itemOptionValue['title'] = 'testcart.jpg'; + $value = $itemOptionValue; + $validatorInfoMock = $this->prepareValidatorInfoMock(); + $this->_objectManager->addSharedInstance($validatorInfoMock, ValidatorInfo::class); + break; + case ProductCustomOptionInterface::OPTION_TYPE_AREA: + $value = 'testcart'; + break; + default: + $value = $itemOptionValue; + break; + } + $options[$productOption->getId()] = $value; + } + + return [ + 'options' => $options, + 'sku' => $sku, + ]; + } + + /** + * Prepare mock for updating file type options. + * + * @return MockObject + */ + private function prepareValidatorInfoMock(): MockObject + { + $validatorInfoMock = $this->createMock(ValidatorInfo::class); + $validatorInfoMock->method('setUseQuotePath')->willReturnSelf(); + $validatorInfoMock->expects($this->any()) + ->method('validate') + ->willReturn(true); + + return $validatorInfoMock; + } + + /** + * Get quote item by sku. + * + * @param Quote $quote + * @param string $sku + * @return QuoteItem|null + */ + private function getQuoteItemBySku(Quote $quote, string $sku): ?QuoteItem + { + $itemsCollection = $quote->getItemsCollection(false); + $itemsCollection->addFieldToFilter('sku', $sku); + /** @var QuoteItem $quoteItem */ + $quoteItem = $itemsCollection->getFirstItem(); + + return empty($quoteItem->getId()) ? null : $quoteItem; + } + + /** + * Verify that the quote item options are saved successfully. + * + * @param QuoteItem $quoteItem + * @param array $expectedParams + * @return void + */ + private function assertQuoteItemOptions(QuoteItem $quoteItem, array $expectedParams): void + { + $buyRequest = $this->json->unserialize($quoteItem->getOptionByCode('info_buyRequest')->getValue()); + foreach ($expectedParams as $key => $value) { + if ($key == 'options') { + foreach ($value as $optionId => $optionValue) { + $buyRequestValue = is_array($optionValue) + ? array_intersect_assoc($optionValue, $buyRequest[$key][$optionId]) + : $buyRequest[$key][$optionId]; + $this->assertEquals($optionValue, $buyRequestValue); + } + } else { + $this->assertEquals($value, $buyRequest[$key]); + } + } + } + + /** + * Dispatch update quote item in customer shopping cart + * using backend/customer/cart_product_composite_cart/update action. + * + * @param array $params + * @param array $postValue + * @return void + */ + private function dispatchCompositeCartUpdate(array $params = [], array $postValue = []): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($params); + $this->getRequest()->setPostValue($postValue); + $this->dispatch('backend/customer/cart_product_composite_cart/update'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/CartTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/CartTest.php deleted file mode 100644 index ea21b2df663d9..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/CartTest.php +++ /dev/null @@ -1,105 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Customer\Controller\Adminhtml\Cart\Product\Composite; - -/** - * @magentoAppArea adminhtml - */ -class CartTest extends \Magento\TestFramework\TestCase\AbstractBackendController -{ - /** - * @var \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory - */ - protected $quoteItemCollectionFactory; - - protected function setUp(): void - { - parent::setUp(); - $this->quoteItemCollectionFactory = $this->_objectManager->get( - \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory::class - ); - } - - public function testConfigureActionNoCustomerId() - { - $this->dispatch('backend/customer/cart_product_composite_cart/configure'); - $this->assertEquals( - '{"error":true,"message":"The customer ID isn\'t defined."}', - $this->getResponse()->getBody() - ); - } - - /** - * @magentoDataFixture Magento/Customer/_files/customer.php - */ - public function testConfigureActionNoQuoteId() - { - $this->getRequest()->setParam('customer_id', 1); - $this->getRequest()->setParam('website_id', 1); - $this->dispatch('backend/customer/cart_product_composite_cart/configure'); - $this->assertEquals( - '{"error":true,"message":"The quote items are incorrect. Verify the quote items and try again."}', - $this->getResponse()->getBody() - ); - } - - /** - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoDataFixture Magento/Customer/_files/quote.php - */ - public function testConfigureAction() - { - $items = $this->quoteItemCollectionFactory->create(); - $itemId = $items->getAllIds()[0]; - $this->getRequest()->setParam('customer_id', 1); - $this->getRequest()->setParam('website_id', 1); - $this->getRequest()->setParam('id', $itemId); - $this->dispatch('backend/customer/cart_product_composite_cart/configure'); - $this->assertStringContainsString( - '<input id="product_composite_configure_input_qty" class="input-text admin__control-text qty"' - . ' type="text" name="qty" value="1">', - $this->getResponse()->getBody() - ); - } - - public function testUpdateActionNoCustomerId() - { - $this->dispatch('backend/customer/cart_product_composite_cart/update'); - $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); - } - - /** - * @magentoDataFixture Magento/Customer/_files/customer.php - */ - public function testUpdateActionNoQuoteId() - { - $this->getRequest()->setParam('customer_id', 1); - $this->getRequest()->setParam('website_id', 1); - $this->dispatch('backend/customer/cart_product_composite_cart/update'); - $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); - } - - /** - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoDataFixture Magento/Customer/_files/quote.php - */ - public function testUpdateAction() - { - $items = $this->quoteItemCollectionFactory->create(); - $itemId = $items->getAllIds()[0]; - $this->getRequest()->setParam('customer_id', 1); - $this->getRequest()->setParam('website_id', 1); - $this->getRequest()->setParam('id', $itemId); - - $this->dispatch('backend/customer/cart_product_composite_cart/update'); - $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/CartTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/CartTest.php new file mode 100644 index 0000000000000..3e2652f0a0709 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/CartTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Adminhtml\Index; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Class checks customer's shopping cart controller. + * + * @see \Magento\Customer\Controller\Adminhtml\Index\Cart + * @magentoAppArea adminhtml + */ +class CartTest extends AbstractBackendController +{ + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_sample.php + * @return void + */ + public function testCartAction(): void + { + $this->dispatchShoppingCart( + [ + 'id' => 1, + 'website_id' => 1, + ], + ['delete' => 1] + ); + $body = $this->getResponse()->getBody(); + $this->assertStringContainsString('<div id="customer_cart_grid"', $body); + } + + /** + * Delete customer shopping cart item + * + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + * @return void + */ + public function testDeleteCartItem(): void + { + $customer = $this->customerRepository->get('customer_uk_address@test.com'); + /** @var Quote $quote */ + $quote = $this->quoteRepository->getForCustomer($customer->getId()); + $quoteItemId = $quote->getItemsCollection()->getFirstItem()->getItemId(); + $this->assertNotEmpty($quoteItemId); + $this->dispatchShoppingCart( + [ + 'id' => $customer->getId(), + 'website_id' => $customer->getWebsiteId(), + ], + ['delete' => $quoteItemId] + ); + $quote->getItemsCollection(false); + $this->assertFalse( + $quote->getItemById($quoteItemId), + sprintf('Customer\'s shopping cart item with ID = %s has not been deleted', $quoteItemId) + ); + } + + /** + * Dispatch admin shopping cart using backend/customer/index/cart action. + * + * @param array $params + * @param array $postValue + * @return void + */ + private function dispatchShoppingCart(array $params = [], array $postValue = []): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($params); + $this->getRequest()->setPostValue($postValue); + $this->dispatch('backend/customer/index/cart'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php index 019c1c277e55f..40c84d8b5db58 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php @@ -153,17 +153,6 @@ public function te1stNewActionWithCustomerData() $this->testNewAction(); } - /** - * @magentoDataFixture Magento/Customer/_files/customer_sample.php - */ - public function testCartAction() - { - $this->getRequest()->setParam('id', 1)->setParam('website_id', 1)->setPostValue('delete', 1); - $this->dispatch('backend/customer/index/cart'); - $body = $this->getResponse()->getBody(); - $this->assertStringContainsString('<div id="customer_cart_grid"', $body); - } - /** * @magentoDbIsolation enabled */ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Wishlist/Product/Composite/Wishlist/UpdateTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Wishlist/Product/Composite/Wishlist/UpdateTest.php new file mode 100644 index 0000000000000..035db789b171e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Wishlist/Product/Composite/Wishlist/UpdateTest.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Adminhtml\Wishlist\Product\Composite\Wishlist; + +use Magento\Backend\Model\Session; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\TestFramework\Wishlist\Model\GetWishlistByCustomerId; +use Magento\Wishlist\Model\Item; + +/** + * Tests for update wish list items. + * + * @magentoAppArea adminhtml + */ +class UpdateTest extends AbstractBackendController +{ + /** @var GetWishlistByCustomerId */ + private $getWishlistByCustomerId; + + /** @var SerializerInterface */ + private $json; + + /** @var Session */ + private $session; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->getWishlistByCustomerId = $this->_objectManager->get(GetWishlistByCustomerId::class); + $this->json = $this->_objectManager->get(SerializerInterface::class); + $this->session = $this->_objectManager->get(Session::class); + } + + /** + * @magentoDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testUpdateItem(): void + { + $item = $this->getWishlistByCustomerId->getItemBySku(1, 'simple-1'); + $this->assertNotNull($item); + $params = ['id' => $item->getId(), 'qty' => 5]; + $this->dispatchUpdateItemRequest($params); + $this->assertEquals($params['qty'], $this->getWishlistByCustomerId->getItemBySku(1, 'simple-1')->getQty()); + } + + /** + * @magentoDataFixture Magento/Wishlist/_files/wishlist_with_configurable_product.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testUpdateItemOption(): void + { + $item = $this->getWishlistByCustomerId->getItemBySku(1, 'Configurable product'); + $this->assertNotNull($item); + $params = [ + 'id' => $item->getId(), + 'super_attribute' => $this->performConfigurableOption($item->getProduct()), + 'qty' => 5, + ]; + $this->dispatchUpdateItemRequest($params); + $this->assertUpdatedItem( + $this->getWishlistByCustomerId->getItemBySku(1, 'Configurable product'), + $params + ); + } + + /** + * @return void + */ + public function testUpdateNotExistingItem(): void + { + $this->dispatchUpdateItemRequest(['id' => 989]); + $this->assertTrue($this->session->getCompositeProductResult()->getError()); + $this->assertEquals( + (string)__('Please load Wish List item.'), + $this->session->getCompositeProductResult()->getMessage() + ); + } + + /** + * @return void + */ + public function testUpdateWithoutParams(): void + { + $this->dispatchUpdateItemRequest([]); + $this->assertTrue($this->session->getCompositeProductResult()->getError()); + $this->assertEquals( + (string)__('Please define Wish List item ID.'), + $this->session->getCompositeProductResult()->getMessage() + ); + } + /** + * Assert updated item in wish list. + * + * @param Item $item + * @param array $expectedData + * @return void + */ + private function assertUpdatedItem(Item $item, array $expectedData): void + { + $this->assertEquals($expectedData['qty'], $item->getQty()); + $buyRequestOption = $this->json->unserialize($item->getOptionByCode('info_buyRequest')->getValue()); + foreach ($expectedData as $key => $value) { + $this->assertEquals($value, $buyRequestOption[$key]); + } + } + + /** + * Perform configurable option to select. + * + * @param ProductInterface $product + * @return array + */ + private function performConfigurableOption(ProductInterface $product): array + { + $configurableOptions = $product->getTypeInstance()->getConfigurableOptions($product); + $attributeId = key($configurableOptions); + $option = reset($configurableOptions[$attributeId]); + + return [$attributeId => $option['value_index']]; + } + + /** + * Dispatch update wish list item request. + * + * @param array $params + * @return void + */ + private function dispatchUpdateItemRequest(array $params): void + { + $this->getRequest()->setParams($params)->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/customer/wishlist_product_composite_wishlist/update'); + $this->assertRedirect($this->stringContains('backend/catalog/product/showUpdateResult/')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php index 664c7d9418401..1c9a41962997d 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php @@ -16,7 +16,7 @@ /** * Load customer data test class. * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppArea frontend */ class LoadTest extends AbstractController diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/AuthenticateTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/AuthenticateTest.php new file mode 100644 index 0000000000000..345a001973e8c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/AuthenticateTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\AccountManagement; + +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Exception\State\UserLockedException; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests for customer authenticate via customer account management service. + * + * @magentoDbIsolation enabled + */ +class AuthenticateTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var AccountManagementInterface */ + private $accountManagement; + + /** @var CustomerRegistry */ + private $customerRegistry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->accountManagement = $this->objectManager->get(AccountManagementInterface::class); + $this->customerRegistry = $this->objectManager->get(CustomerRegistry::class); + } + + /** + * @magentoDataFixture Magento/Customer/_files/locked_customer.php + * + * @return void + */ + public function testAuthenticateByLockedCustomer(): void + { + $this->expectExceptionObject(new UserLockedException(__('The account is locked.'))); + $this->accountManagement->authenticate('customer@example.com', 'password'); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/expired_lock_for_customer.php + * + * @return void + */ + public function testAuthenticateByCustomerExpiredLock(): void + { + $email = 'customer@example.com'; + $customer = $this->accountManagement->authenticate($email, 'password'); + $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId()); + $this->assertEquals(0, $customerSecure->getFailuresNum()); + $this->assertNull($customerSecure->getFirstFailure()); + $this->assertNull($customerSecure->getLockExpires()); + } +} 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/AccountManagementTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php index e2b43fcbd2688..6f2cf2d76bd11 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php @@ -13,6 +13,7 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\ExpiredException; use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Url as UrlBuilder; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -155,8 +156,22 @@ public function testLoginWrongUsername() */ public function testChangePassword() { + /** @var SessionManagerInterface $session */ + $session = $this->objectManager->get(SessionManagerInterface::class); + $oldSessionId = $session->getSessionId(); + $session->setTestData('test'); $this->accountManagement->changePassword('customer@example.com', 'password', 'new_Password123'); + $this->assertTrue( + $oldSessionId !== $session->getSessionId(), + 'Customer session id wasn\'t regenerated after change password' + ); + + $session->destroy(); + $session->setSessionId($oldSessionId); + + $this->assertNull($session->getTestData(), 'Customer session data wasn\'t cleaned'); + $this->accountManagement->authenticate('customer@example.com', 'new_Password123'); } 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/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AttributeTest.php new file mode 100644 index 0000000000000..f433324efcfa6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AttributeTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test for \Magento\Customer\Model\Attribute. + */ +class AttributeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Attribute + */ + private $model; + + /** + * @var AttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var int|string + */ + private $customerEntityType; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(Attribute::class); + $this->attributeRepository = $this->objectManager->get(AttributeRepositoryInterface::class); + $this->customerEntityType = $this->objectManager->get(Config::class) + ->getEntityType('customer') + ->getId(); + } + + /** + * Test Create -> Read -> Update -> Delete attribute operations. + * + * @return void + */ + public function testCRUD(): void + { + $this->model->setAttributeCode('test') + ->setEntityTypeId($this->customerEntityType) + ->setFrontendLabel('test') + ->setIsUserDefined(1); + $crud = new \Magento\TestFramework\Entity($this->model, [AttributeInterface::FRONTEND_LABEL => uniqid()]); + $crud->testCrud(); + } + + /** + * @magentoDataFixture Magento/Customer/_files/attribute_user_defined_customer.php + * + * @return void + */ + public function testAttributeSaveWithChangedEntityType(): void + { + $this->expectException( + \Magento\Framework\Exception\LocalizedException::class + ); + $this->expectExceptionMessage('Do not change entity type.'); + + $attribute = $this->attributeRepository->get($this->customerEntityType, 'user_attribute'); + $attribute->setEntityTypeId(5); + $attribute->save(); + } + + /** + * @magentoDataFixture Magento/Customer/_files/attribute_user_defined_customer.php + * + * @return void + */ + public function testAttributeSaveWithoutChangedEntityType(): void + { + $attribute = $this->attributeRepository->get($this->customerEntityType, 'user_attribute'); + $attribute->setSortOrder(1250); + $attribute->save(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AuthenticationTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AuthenticationTest.php new file mode 100644 index 0000000000000..3de64701bdedb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AuthenticationTest.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Framework\Exception\InvalidEmailOrPasswordException; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests for customer authentication model. + * + * @see \Magento\Customer\Model\Authentication + * @magentoDbIsolation enabled + */ +class AuthenticationTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var AuthenticationInterface */ + private $authentication; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->authentication = $this->objectManager->get(AuthenticationInterface::class); + } + + /** + * @magentoDataFixture Magento/Customer/_files/expired_lock_for_customer.php + * + * @return void + */ + public function testCustomerAuthenticate(): void + { + $this->assertTrue($this->authentication->authenticate(1, 'password')); + } + + /** + * @magentoDataFixture Magento/Customer/_files/expired_lock_for_customer.php + * + * @return void + */ + public function testCustomerAuthenticateWithWrongPassword(): void + { + $this->expectExceptionObject(new InvalidEmailOrPasswordException(__('Invalid login or password.'))); + $this->authentication->authenticate(1, 'password1'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Checkout/ConfigProviderTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Checkout/ConfigProviderTest.php new file mode 100644 index 0000000000000..f29b5f622cdf1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Checkout/ConfigProviderTest.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Checkout; + +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for customer checkout config provider. + * + * @see \Magento\Customer\Model\Checkout\ConfigProvider + */ +class ConfigProviderTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var ConfigProvider */ + private $configProvider; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->configProvider = $this->objectManager->get(ConfigProvider::class); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 1 + * + * @return void + */ + public function testAutocompletePasswordEnabled(): void + { + $this->assertEquals('on', $this->configProvider->getConfig()['autocomplete']); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 0 + * + * @return void + */ + public function testAutocompletePasswordDisabled(): void + { + $this->assertEquals('off', $this->configProvider->getConfig()['autocomplete']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/EmailNotificationTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/EmailNotificationTest.php new file mode 100644 index 0000000000000..69afd17c674a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/EmailNotificationTest.php @@ -0,0 +1,223 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory; +use Magento\Email\Model\Template; +use Magento\Email\Model\TemplateFactory; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\Mail\MessageInterface; +use Magento\Framework\Module\Manager; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use PHPUnit\Framework\TestCase; + +/** + * Test for customer email notification model. + * + * @see \Magento\Customer\Model\EmailNotification + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class EmailNotificationTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Manager */ + private $moduleManager; + + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var EmailNotificationInterface */ + private $emailNotification; + + /** @var TransportBuilderMock */ + private $transportBuilder; + + /** @var TemplateResource */ + private $templateResource; + + /** @var TemplateFactory */ + private $templateFactory; + + /** @var MutableScopeConfigInterface */ + private $mutableScopeConfig; + + /** @var CollectionFactory */ + private $templateCollectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->moduleManager = $this->objectManager->get(Manager::class); + //This check is needed because Magento_Customer independent of Magento_Email + if (!$this->moduleManager->isEnabled('Magento_Email')) { + $this->markTestSkipped('Magento_Email module disabled.'); + } + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->emailNotification = $this->objectManager->get(EmailNotificationInterface::class); + $this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class); + $this->templateResource = $this->objectManager->get(TemplateResource::class); + $this->templateFactory = $this->objectManager->get(TemplateFactory::class); + $this->mutableScopeConfig = $this->objectManager->get(MutableScopeConfigInterface::class); + $this->templateCollectionFactory = $this->objectManager->get(CollectionFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->moduleManager->isEnabled('Magento_Email')) { + $this->mutableScopeConfig->clean(); + $collection = $this->templateCollectionFactory->create(); + $template = $collection->addFieldToFilter('template_code', 'customer_password_email_template') + ->getFirstItem(); + if ($template->getId()) { + $this->templateResource->delete($template); + } + } + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testResetPasswordCustomTemplate(): void + { + $this->setEmailTemplateConfig(EmailNotification::XML_PATH_RESET_PASSWORD_TEMPLATE); + $customer = $this->customerRepository->get('customer@example.com'); + $this->emailNotification->credentialsChanged($customer, $customer->getEmail(), true); + $expectedSender = ['name' => 'CustomerSupport', 'email' => 'support@example.com']; + $this->assertMessage($expectedSender); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/password/forgot_email_identity custom1 + * + * @return void + */ + public function testForgotPasswordCustomTemplate(): void + { + $this->setEmailTemplateConfig(EmailNotification::XML_PATH_FORGOT_EMAIL_TEMPLATE); + $customer = $this->customerRepository->get('customer@example.com'); + $this->emailNotification->passwordResetConfirmation($customer); + $expectedSender = ['name' => 'Custom 1', 'email' => 'custom1@example.com']; + $this->assertMessage($expectedSender); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/password/forgot_email_identity custom2 + * + * @return void + */ + public function testRemindPasswordCustomTemplate(): void + { + $this->setEmailTemplateConfig(EmailNotification::XML_PATH_REMIND_EMAIL_TEMPLATE); + $customer = $this->customerRepository->get('customer@example.com'); + $this->emailNotification->passwordReminder($customer); + $expectedSender = ['name' => 'Custom 2', 'email' => 'custom2@example.com']; + $this->assertMessage($expectedSender); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testChangeEmailCustomTemplate(): void + { + $this->setEmailTemplateConfig(EmailNotification::XML_PATH_CHANGE_EMAIL_TEMPLATE); + $customer = $this->customerRepository->get('customer@example.com'); + $customer->setEmail('customer_update@example.com'); + $this->emailNotification->credentialsChanged($customer, 'customer@example.com'); + $expectedSender = ['name' => 'CustomerSupport', 'email' => 'support@example.com']; + $this->assertMessage($expectedSender); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testChangeEmailAndPasswordCustomTemplate(): void + { + $this->setEmailTemplateConfig(EmailNotification::XML_PATH_CHANGE_EMAIL_AND_PASSWORD_TEMPLATE); + $customer = $this->customerRepository->get('customer@example.com'); + $customer->setEmail('customer_update@example.com'); + $this->emailNotification->credentialsChanged($customer, 'customer@example.com', true); + $expectedSender = ['name' => 'CustomerSupport', 'email' => 'support@example.com']; + $this->assertMessage($expectedSender); + } + + /** + * Assert message. + * + * @param array $expectedSender + * @return void + */ + private function assertMessage(array $expectedSender): void + { + $message = $this->transportBuilder->getSentMessage(); + $this->assertNotNull($message); + $this->assertMessageSender($message, $expectedSender); + $this->assertStringContainsString( + 'Text specially for check in test.', + $message->getBody()->getParts()[0]->getRawContent(), + 'Expected text wasn\'t found in message.' + ); + } + + /** + * Assert message sender. + * + * @param MessageInterface $message + * @param array $expectedSender + * @return void + */ + private function assertMessageSender(MessageInterface $message, array $expectedSender): void + { + $messageFrom = $message->getFrom(); + $this->assertNotNull($messageFrom); + $messageFrom = current($messageFrom); + $this->assertEquals($expectedSender['name'], $messageFrom->getName()); + $this->assertEquals($expectedSender['email'], $messageFrom->getEmail()); + } + + /** + * Set email template config. + * + * @param string $configPath + * @return void + */ + private function setEmailTemplateConfig(string $configPath): void + { + $template = $this->templateFactory->create(); + $template->setTemplateCode('customer_password_email_template') + ->setTemplateText(file_get_contents(__DIR__ . '/../_files/customer_password_email_template.html')) + ->setTemplateType(Template::TYPE_HTML); + $this->templateResource->save($template); + $this->mutableScopeConfig->setValue($configPath, $template->getId(), ScopeInterface::SCOPE_STORE, 'default'); + } +} 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/Model/Metadata/Form/ImageTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Metadata/Form/ImageTest.php new file mode 100644 index 0000000000000..48e45126d124b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Metadata/Form/ImageTest.php @@ -0,0 +1,217 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Model\Metadata\Form; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +class ImageTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $fileName = 'magento.jpg'; + + /** + * @var string + */ + private $invalidFileName = '../../invalidFile.xyz'; + + /** + * @var string + */ + private $imageFixtureDir; + + /** + * @var string + */ + private $expectedFileName; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @inheritDoc + */ + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->imageFixtureDir = realpath(__DIR__ . '/../../../_files/image'); + $this->expectedFileName = '/m/a/' . $this->fileName; + } + + /** + * Test for processCustomerAddressValue method + * + * @magentoAppIsolation enabled + * @throws FileSystemException + * @throws \ReflectionException + */ + public function testProcessCustomerAddressValue() + { + $this->mediaDirectory->delete('customer_address'); + $this->mediaDirectory->create($this->mediaDirectory->getRelativePath('customer_address/tmp/')); + $tmpFilePath = $this->mediaDirectory->getAbsolutePath('customer_address/tmp/' . $this->fileName); + copy($this->imageFixtureDir . DIRECTORY_SEPARATOR . $this->fileName, $tmpFilePath); + + $imageFile = [ + 'name' => $this->fileName, + 'type' => 'image/jpeg', + 'tmp_name' => $this->fileName, + 'file' => $this->fileName, + 'error' => 0, + 'size' => 12500, + 'previewType' => 'image', + ]; + + $params = [ + 'entityTypeCode' => 'customer_address', + 'formCode' => 'customer_address_edit', + 'isAjax' => false, + 'value' => $imageFile + ]; + + $expectedPath = $this->mediaDirectory->getAbsolutePath('customer_address' . $this->expectedFileName); + + /** @var Image $image */ + $image = $this->objectManager->create(\Magento\Customer\Model\Metadata\Form\Image::class, $params); + $processCustomerAddressValueMethod = new \ReflectionMethod( + \Magento\Customer\Model\Metadata\Form\Image::class, + 'processCustomerAddressValue' + ); + $processCustomerAddressValueMethod->setAccessible(true); + $actual = $processCustomerAddressValueMethod->invoke($image, $imageFile); + $this->assertEquals($this->expectedFileName, $actual); + $this->assertFileExists($expectedPath); + $this->assertFileNotExists($tmpFilePath); + } + + /** + * Test for processCustomerValue method + * + * @magentoAppIsolation enabled + * @throws FileSystemException + * @throws \ReflectionException + */ + public function testProcessCustomerValue() + { + $this->mediaDirectory->delete('customer'); + $this->mediaDirectory->create($this->mediaDirectory->getRelativePath('customer/tmp/')); + $tmpFilePath = $this->mediaDirectory->getAbsolutePath('customer/tmp/' . $this->fileName); + copy($this->imageFixtureDir . DIRECTORY_SEPARATOR . $this->fileName, $tmpFilePath); + + $imageFile = [ + 'name' => $this->fileName, + 'type' => 'image/jpeg', + 'tmp_name' => $this->fileName, + 'file' => $this->fileName, + 'error' => 0, + 'size' => 12500, + 'previewType' => 'image', + ]; + + $params = [ + 'entityTypeCode' => 'customer', + 'formCode' => 'customer_edit', + 'isAjax' => false, + 'value' => $imageFile + ]; + + /** @var Image $image */ + $image = $this->objectManager->create(\Magento\Customer\Model\Metadata\Form\Image::class, $params); + $processCustomerAddressValueMethod = new \ReflectionMethod( + \Magento\Customer\Model\Metadata\Form\Image::class, + 'processCustomerValue' + ); + $processCustomerAddressValueMethod->setAccessible(true); + $result = $processCustomerAddressValueMethod->invoke($image, $imageFile); + $this->assertInstanceOf('Magento\Framework\Api\ImageContent', $result); + $this->assertFileNotExists($tmpFilePath); + } + + /** + * Test for processCustomerValue method with invalid value + * + * @magentoAppIsolation enabled + * + * @throws FileSystemException + * @throws \ReflectionException + */ + public function testProcessCustomerInvalidValue() + { + $this->expectException( + \Magento\Framework\Exception\ValidatorException::class + ); + + $this->mediaDirectory->delete('customer'); + $this->mediaDirectory->create($this->mediaDirectory->getRelativePath('customer/tmp/')); + $tmpFilePath = $this->mediaDirectory->getAbsolutePath('customer/tmp/' . $this->fileName); + copy($this->imageFixtureDir . DIRECTORY_SEPARATOR . $this->fileName, $tmpFilePath); + + $imageFile = [ + 'name' => $this->fileName, + 'type' => 'image/jpeg', + 'tmp_name' => $this->fileName, + 'file' => $this->invalidFileName, + 'error' => 0, + 'size' => 12500, + 'previewType' => 'image', + ]; + + $params = [ + 'entityTypeCode' => 'customer', + 'formCode' => 'customer_edit', + 'isAjax' => false, + 'value' => $imageFile + ]; + + /** @var Image $image */ + $image = $this->objectManager->create(\Magento\Customer\Model\Metadata\Form\Image::class, $params); + $processCustomerAddressValueMethod = new \ReflectionMethod( + \Magento\Customer\Model\Metadata\Form\Image::class, + 'processCustomerValue' + ); + $processCustomerAddressValueMethod->setAccessible(true); + $processCustomerAddressValueMethod->invoke($image, $imageFile); + } + + /** + * @inheritdoc + * @throws FileSystemException + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Filesystem::class + ); + /** @var WriteInterface $mediaDirectory */ + $mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $mediaDirectory->delete('customer'); + $mediaDirectory->delete('customer_address'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/OptionsTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/OptionsTest.php new file mode 100644 index 0000000000000..879707edd9224 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/OptionsTest.php @@ -0,0 +1,164 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Config\Model\Config\Source\Nooptreq; +use Magento\Customer\Helper\Address; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\Customer\Model\Options. + * @magentoDbIsolation enabled + */ +class OptionsTest extends TestCase +{ + private const XML_PATH_SUFFIX_SHOW = 'customer/address/suffix_show'; + private const XML_PATH_SUFFIX_OPTIONS = 'customer/address/suffix_options'; + private const XML_PATH_PREFIX_SHOW = 'customer/address/prefix_show'; + private const XML_PATH_PREFIX_OPTIONS = 'customer/address/prefix_options'; + + /** + * @var Options + */ + private $model; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->create(Options::class); + } + + /** + * Test suffix and prefix options + * + * @dataProvider optionsDataProvider + * + * @param string $optionType + * @param array $showOptionConfig + * @param array $optionValuesConfig + * @param array $expectedOptions + * @return void + */ + public function testOptions( + string $optionType, + array $showOptionConfig, + array $optionValuesConfig, + array $expectedOptions + ): void { + $this->setConfig($showOptionConfig); + $this->setConfig($optionValuesConfig); + + /** @var array $options */ + $options = $optionType === 'prefix' + ? $this->model->getNamePrefixOptions() + : $this->model->getNameSuffixOptions(); + + $this->assertEquals($expectedOptions, $options); + } + + /** + * Set config param + * + * @param array $data + * @param string|null $scopeType + * @param string|null $scopeCode + * @return void + */ + private function setConfig( + array $data, + ?string $scopeType = ScopeInterface::SCOPE_STORE, + ?string $scopeCode = 'default' + ): void { + $path = array_key_first($data); + $this->objectManager->get(MutableScopeConfigInterface::class) + ->setValue($path, $data[$path], $scopeType, $scopeCode); + } + + /** + * DataProvider for testOptions() + * + * @return array + */ + public function optionsDataProvider(): array + { + $optionPrefixName = 'prefix'; + $optionSuffixName = 'suffix'; + $optionValues = 'v1;v2'; + $expectedValues = ['v1', 'v2']; + $optionValuesWithBlank = ';v1;v2'; + $expectedValuesWithBlank = [' ', 'v1', 'v2']; + $optionValuesWithTwoBlank = ';v1;v2;'; + $expectedValuesTwoBlank = [' ', 'v1', 'v2', ' ']; + + return [ + 'prefix_required_with_blank_option' => [ + $optionPrefixName, + [self::XML_PATH_PREFIX_SHOW => Nooptreq::VALUE_REQUIRED], + [self::XML_PATH_PREFIX_OPTIONS => $optionValuesWithBlank], + $expectedValuesWithBlank, + ], + 'prefix_required' => [ + $optionPrefixName, + [self::XML_PATH_PREFIX_SHOW => Nooptreq::VALUE_REQUIRED], + [self::XML_PATH_PREFIX_OPTIONS => $optionValues], + $expectedValues, + ], + 'prefix_required_with_two_blank_option' => [ + $optionPrefixName, + [self::XML_PATH_PREFIX_SHOW => Nooptreq::VALUE_REQUIRED], + [self::XML_PATH_PREFIX_OPTIONS => $optionValuesWithTwoBlank], + $expectedValuesTwoBlank, + ], + 'prefix_optional' => [ + $optionPrefixName, + [self::XML_PATH_PREFIX_SHOW => Nooptreq::VALUE_OPTIONAL], + [self::XML_PATH_PREFIX_OPTIONS => $optionValues], + $expectedValuesWithBlank, + ], + 'suffix_optional' => [ + $optionSuffixName, + [self::XML_PATH_SUFFIX_SHOW => Nooptreq::VALUE_OPTIONAL], + [self::XML_PATH_SUFFIX_OPTIONS => $optionValues], + $expectedValuesWithBlank, + ], + 'suffix_optional_with_blank_option' => [ + $optionSuffixName, + [self::XML_PATH_SUFFIX_SHOW => Nooptreq::VALUE_OPTIONAL], + [self::XML_PATH_SUFFIX_OPTIONS => $optionValuesWithBlank], + $expectedValuesWithBlank, + ], + 'suffix_required_with_blank_option' => [ + $optionSuffixName, + [self::XML_PATH_SUFFIX_SHOW => Nooptreq::VALUE_OPTIONAL], + [self::XML_PATH_SUFFIX_OPTIONS => $optionValuesWithBlank], + $expectedValuesWithBlank, + ], + ]; + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->objectManager->removeSharedInstance(Address::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php index 00b5d2bc6f279..8651db95ae645 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php @@ -19,6 +19,11 @@ use Magento\Framework\Api\SortOrder; use Magento\Framework\Config\CacheInterface; use Magento\Framework\ObjectManagerInterface; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\InvoiceOrderInterface; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\Customer\Api\Data\AddressInterface; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -34,12 +39,18 @@ */ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase { + const NEW_CUSTOMER_EMAIL = 'new.customer@example.com'; + const CUSTOMER_ID = 1; + /** @var AccountManagementInterface */ private $accountManagement; /** @var CustomerRepositoryInterface */ private $customerRepository; + /** @var OrderRepositoryInterface */ + private $orderRepository; + /** @var ObjectManagerInterface */ private $objectManager; @@ -71,6 +82,7 @@ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class); + $this->orderRepository = $this->objectManager->create(OrderRepositoryInterface::class); $this->customerFactory = $this->objectManager->create(CustomerInterfaceFactory::class); $this->addressFactory = $this->objectManager->create(AddressInterfaceFactory::class); $this->regionFactory = $this->objectManager->create(RegionInterfaceFactory::class); @@ -625,4 +637,55 @@ public function testUpdateDefaultShippingAndDefaultBillingTest() 'Default shipping should not be overridden' ); } + + /** + * Test that UpgradeOrderCustomerEmailObserver is executed + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoDbIsolation enabled + */ + public function testUpgradeOrderCustomerEmailObserverWhenEmailIsModified() + { + $customer = $this->customerRepository->getById(self::CUSTOMER_ID); + $customer->setEmail(self::NEW_CUSTOMER_EMAIL); + + $this->customerRepository->save($customer); + + /** @var SearchCriteriaBuilder $searchBuilder */ + $searchBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + $searchCriteria = $searchBuilder + ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId()) + ->create(); + + $customerOrders = $this->orderRepository->getList($searchCriteria); + + foreach ($customerOrders as $customerOrder) { + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $customerOrder->getCustomerEmail()); + } + } + + /** + * Test that UpgradeOrderCustomerEmailObserver is executed but does not update orders + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoDbIsolation enabled + */ + public function testUpgradeOrderCustomerEmailObserverWhenEmailIsNotModified(): void + { + $customer = $this->customerRepository->getById(self::CUSTOMER_ID); + + $this->customerRepository->save($customer); + + /** @var SearchCriteriaBuilder $searchBuilder */ + $searchBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + $searchCriteria = $searchBuilder + ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId()) + ->create(); + + $customerOrders = $this->orderRepository->getList($searchCriteria); + + foreach ($customerOrders as $customerOrder) { + $this->assertEquals('customer@null.com', $customerOrder->getCustomerEmail()); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php new file mode 100644 index 0000000000000..8a4afc23aaea8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +//@codingStandardsIgnoreFile +/** @var \Magento\Customer\Model\Attribute $model */ +$model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Attribute::class); +/** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ +$storeManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\StoreManager::class); +$model->loadByCode('customer_address', 'city'); +$storeLabels = $model->getStoreLabels(); +$stores = $storeManager->getStores(); +/** @var \Magento\Store\Api\Data\WebsiteInterface $website */ +foreach ($stores as $store) { + $storeLabels[$store->getId()] = 'Suburb'; +} +$model->setStoreLabels($storeLabels); +$model->save(); 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_password_email_template.html b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_password_email_template.html new file mode 100644 index 0000000000000..482fa79247a23 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_password_email_template.html @@ -0,0 +1,10 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +{{template config_path="design/email/header_template"}} +<p>Text specially for check in test.</p> +{{template config_path="design/email/footer_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/Customer/_files/customer_with_uk_address.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_uk_address.php index a7ad0bb82719f..c024d18e40942 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_uk_address.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_uk_address.php @@ -11,9 +11,10 @@ use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Model\Address; use Magento\Customer\Model\AddressFactory; +use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\CustomerFactory; use Magento\Customer\Model\CustomerRegistry; -use Magento\Customer\Model\AddressRegistry; +use Magento\Framework\Encryption\EncryptorInterface; use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\Store\Model\Website; use Magento\Store\Model\WebsiteRepository; @@ -31,6 +32,9 @@ $websiteRepository = $objectManager->create(WebsiteRepositoryInterface::class); /** @var Website $mainWebsite */ $mainWebsite = $websiteRepository->get('base'); +/** @var EncryptorInterface $encryptor */ +$encryptor = $objectManager->get(EncryptorInterface::class); + $customer->setWebsiteId($mainWebsite->getId()) ->setEmail('customer_uk_address@test.com') ->setPassword('password') @@ -67,7 +71,7 @@ ); $customer->addAddress($customerAddress); $customer->isObjectNew(true); -$customerDataModel = $customerRepository->save($customer->getDataModel()); +$customerDataModel = $customerRepository->save($customer->getDataModel(), $encryptor->hash('password')); $addressId = $customerDataModel->getAddresses()[0]->getId(); $customerDataModel->setDefaultShipping($addressId); $customerDataModel->setDefaultBilling($addressId); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer.php b/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer.php new file mode 100644 index 0000000000000..e7c8e5074664e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\CustomerAuthUpdate; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Stdlib\DateTime; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +/** @var CustomerAuthUpdate $customerAuthUpdate */ +$customerAuthUpdate = $objectManager->get(CustomerAuthUpdate::class); +$customerId = 1; + +$customerSecure = $customerRegistry->retrieveSecureData($customerId); +$dateTime = new \DateTimeImmutable(); +$customerSecure->setFailuresNum(10) + ->setFirstFailure($dateTime->modify('-15 minutes')->format(DateTime::DATETIME_PHP_FORMAT)) + ->setLockExpires($dateTime->modify('-5 minutes')->format(DateTime::DATETIME_PHP_FORMAT)); +$customerAuthUpdate->saveAuth($customerId); +$customerRegistry->remove($customerId); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer_rollback.php new file mode 100644 index 0000000000000..abd24344319a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer_rollback.php @@ -0,0 +1,10 @@ +<?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_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/image/magento.jpg b/dev/tests/integration/testsuite/Magento/Customer/_files/image/magento.jpg new file mode 100644 index 0000000000000..5704eccd795de Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/Customer/_files/image/magento.jpg differ diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php index 3a39e62af0ccb..9c24e4b5ff3bd 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php @@ -3,39 +3,41 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//Create customer -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'CharlesTAlston@teleworm.us' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Charles' -)->setLastname( - 'Alston' -)->setGender( - '2' -); + +declare(strict_types=1); + +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $customer Customer + * @var $customerResource CustomerResource + */ +$customer = $objectManager->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); + +$customer->setWebsiteId(1) + ->setEntityId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('CharlesTAlston@teleworm.us') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Charles') + ->setLastname('Alston') + ->setGender('2'); + $customer->isObjectNew(true); // Create address -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); // default_billing and default_shipping information would not be saved, it is needed only for simple check $address->addData( [ @@ -54,14 +56,12 @@ // Assign customer and address $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); // Mark last address as default billing and default shipping for current customer $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); -/** @var $objectManager \Magento\TestFramework\ObjectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$objectManager->get(\Magento\Framework\Registry::class)->unregister('_fixture/Magento_ImportExport_Customer'); -$objectManager->get(\Magento\Framework\Registry::class)->register('_fixture/Magento_ImportExport_Customer', $customer); +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customer'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customer', $customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php index b8a69def69d6b..46086e00244ee 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php @@ -3,41 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var $objectManager ObjectManager */ +$objectManager = Bootstrap::getObjectManager(); + $customers = []; -//Create customer -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'BetsyParker@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Betsy' -)->setLastname( - 'Parker' -)->setGender( - 2 -); +/** + * @var $customer Customer + * @var $customerResource CustomerResource + */ +$customer = $objectManager->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); + +$customer->setWebsiteId(1) + ->setEntityId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('BetsyParker@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Betsy') + ->setLastname('Parker') + ->setGender(2); $customer->isObjectNew(true); // Create address -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); // default_billing and default_shipping information would not be saved, it is needed only for simple check $address->addData( [ @@ -56,46 +59,31 @@ // Assign customer and address $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); // Mark last address as default billing and default shipping for current customer $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 2 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'AnthonyNealy@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Anthony' -)->setLastname( - 'Nealy' -)->setGender( - 1 -); +$customer = $objectManager->create(Customer::class); +$customer->setWebsiteId(1) + ->setEntityId(2) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('AnthonyNealy@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Anthony') + ->setLastname('Nealy') + ->setGender(1); $customer->isObjectNew(true); -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); $address->addData( [ 'firstname' => 'Anthony', @@ -112,7 +100,7 @@ ); $customer->addAddress($address); -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); $address->addData( [ 'firstname' => 'Anthony', @@ -129,45 +117,30 @@ ); $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 3 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'LoriBanks@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Lori' -)->setLastname( - 'Banks' -)->setGender( - 2 -); +$customer = $objectManager->create(Customer::class); +$customer->setWebsiteId(1) + ->setEntityId(3) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('LoriBanks@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Lori') + ->setLastname('Banks') + ->setGender(2); $customer->isObjectNew(true); -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); $address->addData( [ 'firstname' => 'Lori', @@ -183,17 +156,13 @@ ] ); $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -/** @var $objectManager \Magento\TestFramework\ObjectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$objectManager->get(\Magento\Framework\Registry::class) - ->unregister('_fixture/Magento_ImportExport_Customers_Array'); -$objectManager->get(\Magento\Framework\Registry::class) - ->register('_fixture/Magento_ImportExport_Customers_Array', $customers); +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customers_Array'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customers_Array', $customers); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php index 9b989779e4cbd..302ac055f61ca 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php @@ -4,107 +4,75 @@ * See COPYING.txt for license details. */ -use Magento\TestFramework\Helper\Bootstrap; -use Magento\Framework\ObjectManagerInterface; +declare(strict_types=1); + use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; /** @var $objectManager ObjectManagerInterface */ $objectManager = Bootstrap::getObjectManager(); $customers = []; + +/** + * @var $customer Customer + * @var $customerResource CustomerResource + */ $customer = $objectManager->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); -$customer->setWebsiteId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'customer@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Firstname' -)->setLastname( - 'Lastname' -)->setDefaultBilling( - 1 -)->setDefaultShipping( - 1 -); +$customer->setWebsiteId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('customer@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Firstname') + ->setLastname('Lastname') + ->setDefaultBilling(1) + ->setDefaultShipping(1); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; $customer = $objectManager->create(Customer::class); -$customer->setWebsiteId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'julie.worrell@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Julie' -)->setLastname( - 'Worrell' -)->setDefaultBilling( - 1 -)->setDefaultShipping( - 1 -); +$customer->setWebsiteId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('julie.worrell@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Julie') + ->setLastname('Worrell') + ->setDefaultBilling(1) + ->setDefaultShipping(1); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; $customer = $objectManager->create(Customer::class); -$customer->setWebsiteId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'david.lamar@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'David' -)->setLastname( - 'Lamar' -)->setDefaultBilling( - 1 -)->setDefaultShipping( - 1 -); +$customer->setWebsiteId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('david.lamar@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('David') + ->setLastname('Lamar') + ->setDefaultBilling(1) + ->setDefaultShipping(1); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -$objectManager->get(Registry::class) - ->unregister('_fixture/Magento_ImportExport_Customer_Collection'); -$objectManager->get(Registry::class) - ->register('_fixture/Magento_ImportExport_Customer_Collection', $customers); +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customer_Collection'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customer_Collection', $customers); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php index 9a90061a6de76..ca32958e66639 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php @@ -3,43 +3,39 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//Create customer -/** @var Magento\Customer\Model\Customer $customer */ -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 0 -)->setEntityId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'BetsyParker@example.com' -)->setPassword( - 'password' -)->setGroupId( - 0 -)->setStoreId( - 0 -)->setIsActive( - 1 -)->setFirstname( - 'Betsy' -)->setLastname( - 'Parker' -)->setGender( - 2 -); + +declare(strict_types=1); + +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var Customer $customer + * @var CustomerResource $customerResource + */ +$customer = Bootstrap::getObjectManager()->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); + +$customer->setWebsiteId(0) + ->setEntityId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('BetsyParker@example.com') + ->setPassword('password') + ->setGroupId(0) + ->setStoreId(0) + ->setIsActive(1) + ->setFirstname('Betsy') + ->setLastname('Parker') + ->setGender(2); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); -// Create and set addresses -$addressFirst = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Address::class -); +$addressFirst = $objectManager->create(Address::class); $addressFirst->addData( [ 'entity_id' => 1, @@ -57,9 +53,7 @@ $customer->addAddress($addressFirst); $customer->setDefaultBilling($addressFirst->getId()); -$addressSecond = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Address::class -); +$addressSecond = $objectManager->create(Address::class); $addressSecond->addData( [ 'entity_id' => 2, @@ -76,4 +70,4 @@ $addressSecond->isObjectNew(true); $customer->addAddress($addressSecond); $customer->setDefaultShipping($addressSecond->getId()); -$customer->save(); +$customerResource->save($customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/locked_customer.php b/dev/tests/integration/testsuite/Magento/Customer/_files/locked_customer.php new file mode 100644 index 0000000000000..b88f025db4d76 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/locked_customer.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\CustomerAuthUpdate; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Stdlib\DateTime; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +/** @var CustomerAuthUpdate $customerAuthUpdate */ +$customerAuthUpdate = $objectManager->get(CustomerAuthUpdate::class); +$customerId = 1; + +$customerSecure = $customerRegistry->retrieveSecureData($customerId); +$dateTime = new \DateTimeImmutable(); +$customerSecure->setFailuresNum(10) + ->setFirstFailure($dateTime->modify('-5 minutes')->format(DateTime::DATETIME_PHP_FORMAT)) + ->setLockExpires($dateTime->modify('+5 minutes')->format(DateTime::DATETIME_PHP_FORMAT)); +$customerAuthUpdate->saveAuth($customerId); +$customerRegistry->remove($customerId); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/locked_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/locked_customer_rollback.php new file mode 100644 index 0000000000000..abd24344319a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/locked_customer_rollback.php @@ -0,0 +1,10 @@ +<?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_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/sales_order.php b/dev/tests/integration/testsuite/Magento/Customer/_files/sales_order.php deleted file mode 100644 index 2ea0e58fddaba..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/sales_order.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/** @var \Magento\Customer\Model\Customer $customer */ -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -)->load( - 1 -); - -/** @var \Magento\Sales\Model\Order $order */ -$order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order::class -)->loadByIncrementId( - '100000001' -); -$order->setCustomerIsGuest(false)->setCustomerId($customer->getId())->setCustomerEmail($customer->getEmail()); -$order->save(); diff --git a/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/CreateCustomerTest.php b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/CreateCustomerTest.php new file mode 100644 index 0000000000000..11ac2695bd80c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/CreateCustomerTest.php @@ -0,0 +1,238 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\GraphQl\Service\GraphQlRequest; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use PHPUnit\Framework\TestCase; + +/** + * Test creating a customer through GraphQL + * + * @magentoAppArea graphql + */ +class CreateCustomerTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var GraphQlRequest + */ + private $graphQlRequest; + + /** + * @var SerializerInterface + */ + private $json; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->graphQlRequest = $this->objectManager->create(GraphQlRequest::class); + $this->json = $this->objectManager->get(SerializerInterface::class); + + $this->customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class); + $this->storeRepository = $this->objectManager->create(StoreRepositoryInterface::class); + } + + /** + * Test that creating a customer sends an email + */ + public function testCreateCustomerSendsEmail() + { + $query + = <<<QUERY +mutation createAccount { + createCustomer( + input: { + email: "test@magento.com" + firstname: "Test" + lastname: "Magento" + password: "T3stP4assw0rd" + is_subscribed: false + } + ) { + customer { + id + } + } +} +QUERY; + + $response = $this->graphQlRequest->send($query); + $responseData = $this->json->unserialize($response->getContent()); + + // Assert the response of the GraphQL request + $this->assertNull($responseData['data']['createCustomer']['customer']['id']); + + // Verify the customer was created and has the correct data + $customer = $this->customerRepository->get('test@magento.com'); + $this->assertEquals('Test', $customer->getFirstname()); + $this->assertEquals('Magento', $customer->getLastname()); + + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); + $sentMessage = $transportBuilderMock->getSentMessage(); + + // Verify an email was dispatched to the correct user + $this->assertNotNull($sentMessage); + $this->assertEquals('Test Magento', $sentMessage->getTo()[0]->getName()); + $this->assertEquals('test@magento.com', $sentMessage->getTo()[0]->getEmail()); + + // Assert the email contains the expected content + $this->assertEquals('Welcome to Main Website Store', $sentMessage->getSubject()); + $messageRaw = $sentMessage->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString('Welcome to Main Website Store.', $messageRaw); + } + + /** + * Test that creating a customer on an alternative store sends an email + * + * @magentoDataFixture Magento/CustomerGraphQl/_files/website_store_with_store_view.php + */ + public function testCreateCustomerForStoreSendsEmail() + { + $query + = <<<QUERY +mutation createAccount { + createCustomer( + input: { + email: "test@magento.com" + firstname: "Test" + lastname: "Magento" + password: "T3stP4assw0rd" + is_subscribed: false + } + ) { + customer { + id + } + } +} +QUERY; + + $response = $this->graphQlRequest->send( + $query, + [], + '', + [ + 'Store' => 'test_store_view' + ] + ); + $responseData = $this->json->unserialize($response->getContent()); + + // Assert the response of the GraphQL request + $this->assertNull($responseData['data']['createCustomer']['customer']['id']); + + // Verify the customer was created and has the correct data + $customer = $this->customerRepository->get('test@magento.com'); + $this->assertEquals('Test', $customer->getFirstname()); + $this->assertEquals('Magento', $customer->getLastname()); + $this->assertEquals('Test Store View', $customer->getCreatedIn()); + + $store = $this->storeRepository->getById($customer->getStoreId()); + $this->assertEquals('test_store_view', $store->getCode()); + + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); + $sentMessage = $transportBuilderMock->getSentMessage(); + + // Verify an email was dispatched to the correct user + $this->assertNotNull($sentMessage); + $this->assertEquals('Test Magento', $sentMessage->getTo()[0]->getName()); + $this->assertEquals('test@magento.com', $sentMessage->getTo()[0]->getEmail()); + + // Assert the email contains the expected content + $this->assertEquals('Welcome to Test Group', $sentMessage->getSubject()); + $messageRaw = $sentMessage->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString('Welcome to Test Group.', $messageRaw); + } + + /** + * Test that creating a customer on an alternative store sends an email in the translated language + * + * @magentoDataFixture Magento/CustomerGraphQl/_files/website_store_with_store_view.php + * @magentoConfigFixture test_store_view_store general/locale/code fr_FR + * @magentoComponentsDir Magento/CustomerGraphQl/_files + */ + public function testCreateCustomerForStoreSendsTranslatedEmail() + { + $query + = <<<QUERY +mutation createAccount { + createCustomer( + input: { + email: "test@magento.com" + firstname: "Test" + lastname: "Magento" + password: "T3stP4assw0rd" + is_subscribed: false + } + ) { + customer { + id + } + } +} +QUERY; + + $response = $this->graphQlRequest->send( + $query, + [], + '', + [ + 'Store' => 'test_store_view' + ] + ); + $responseData = $this->json->unserialize($response->getContent()); + + // Assert the response of the GraphQL request + $this->assertNull($responseData['data']['createCustomer']['customer']['id']); + + // Verify the customer was created and has the correct data + $customer = $this->customerRepository->get('test@magento.com'); + $this->assertEquals('Test', $customer->getFirstname()); + $this->assertEquals('Magento', $customer->getLastname()); + $this->assertEquals('Test Store View', $customer->getCreatedIn()); + + $store = $this->storeRepository->getById($customer->getStoreId()); + $this->assertEquals('test_store_view', $store->getCode()); + + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); + $sentMessage = $transportBuilderMock->getSentMessage(); + + // Verify an email was dispatched to the correct user + $this->assertNotNull($sentMessage); + $this->assertEquals('Test Magento', $sentMessage->getTo()[0]->getName()); + $this->assertEquals('test@magento.com', $sentMessage->getTo()[0]->getEmail()); + + // Assert the email contains the expected content + $this->assertEquals('Bienvenue sur Test Group', $sentMessage->getSubject()); + $messageRaw = $sentMessage->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString('Bienvenue sur Test Group.', $messageRaw); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/french/fr_fr/1.csv b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/french/fr_fr/1.csv new file mode 100644 index 0000000000000..4d4f9c48f7d40 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/french/fr_fr/1.csv @@ -0,0 +1,2 @@ +"Welcome to %store_name","Bienvenue sur %store_name" +"Welcome to %store_name.","Bienvenue sur %store_name." diff --git a/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/french/fr_fr/language.xml b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/french/fr_fr/language.xml new file mode 100644 index 0000000000000..9926a71910ebe --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/french/fr_fr/language.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** +* Copyright © Magento, Inc. All rights reserved. +* See COPYING.txt for license details. +*/ +--> +<language xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/Language/package.xsd"> + <code>fr_FR</code> + <vendor>french</vendor> + <package>fr_fr</package> + <sort_order>0</sort_order> +</language> diff --git a/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/french/fr_fr/registration.php b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/french/fr_fr/registration.php new file mode 100644 index 0000000000000..95acf0c1487c7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/french/fr_fr/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::LANGUAGE, 'french_fr_fr', __DIR__); diff --git a/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/website_store_with_store_view.php b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/website_store_with_store_view.php new file mode 100644 index 0000000000000..7407c1e4e9d09 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/website_store_with_store_view.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Store\Api\Data\GroupInterface; +use Magento\Store\Api\Data\GroupInterfaceFactory; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\StoreInterfaceFactory; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Api\Data\WebsiteInterfaceFactory; +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; + +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var WebsiteResource $websiteResource */ +$websiteResource = $objectManager->get(WebsiteResource::class); +/** @var StoreResource $storeResource */ +$storeResource = $objectManager->get(StoreResource::class); +/** @var GroupResource $groupResource */ +$groupResource = $objectManager->get(GroupResource::class); +/** @var WebsiteInterface $website */ +$website = $objectManager->get(WebsiteInterfaceFactory::class)->create(); +$website->setCode('test_website')->setName('Test Website'); +$websiteResource->save($website); +/** @var GroupInterface $storeGroup */ +$storeGroup = $objectManager->get(GroupInterfaceFactory::class)->create(); +$storeGroup->setCode('test_group') + ->setName('Test Group') + ->setWebsite($website); +$groupResource->save($storeGroup); +/* Refresh stores memory cache */ +$storeManager->reinitStores(); + +/** @var StoreInterface $store */ +$store = $objectManager->get(StoreInterfaceFactory::class)->create(); +$store->setCode('test_store_view') + ->setWebsiteId($website->getId()) + ->setGroupId($storeGroup->getId()) + ->setName('Test Store View') + ->setSortOrder(10) + ->setIsActive(1); +$storeResource->save($store); diff --git a/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/website_store_with_store_view_rollback.php b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/website_store_with_store_view_rollback.php new file mode 100644 index 0000000000000..29f7fbd56c402 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/_files/website_store_with_store_view_rollback.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\StoreInterfaceFactory; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Api\Data\WebsiteInterfaceFactory; +use Magento\Store\Model\ResourceModel\Store as StoreResource; +use Magento\Store\Model\ResourceModel\Website as WebsiteResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteResource $websiteResource */ +$websiteResource = $objectManager->get(WebsiteResource::class); +/** @var StoreResource $storeResource */ +$storeResource = $objectManager->get(StoreResource::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var WebsiteInterface $website */ +$website = $objectManager->get(WebsiteInterfaceFactory::class)->create(); +$websiteResource->load($website, 'test_website', 'code'); +if ($website->getId()) { + $websiteResource->delete($website); +} +/** @var StoreInterface $store */ +$store = $objectManager->get(StoreInterfaceFactory::class)->create(); +$storeResource->load($store, 'test_store_view', 'code'); +if ($store->getId()) { + $storeResource->delete($store); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php index 832aabe6b6a78..0a5e6cdfe21bd 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php @@ -9,30 +9,41 @@ */ namespace Magento\CustomerImportExport\Model\Import; +use Magento\Catalog\Model\ResourceModel\Product; use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\AddressRegistry; +use Magento\Customer\Model\Indexer\Processor; +use Magento\Customer\Model\ResourceModel\Address\Collection; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Registry; +use Magento\Framework\Stdlib\DateTime; use Magento\ImportExport\Model\Import as ImportModel; use Magento\ImportExport\Model\Import\Adapter as ImportAdapter; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\Import\Source\Csv; +use Magento\ImportExport\Model\ResourceModel\Helper; use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\Indexer\StateInterface; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; use ReflectionClass; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AddressTest extends \PHPUnit\Framework\TestCase +class AddressTest extends TestCase { /** * Tested class name * * @var string */ - protected $_testClassName = \Magento\CustomerImportExport\Model\Import\Address::class; + protected $_testClassName = Address::class; /** * Fixture key from fixture @@ -92,7 +103,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase protected $customerResource; /** - * @var \Magento\Customer\Model\Indexer\Processor + * @var Processor */ private $indexerProcessor; @@ -101,7 +112,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase */ protected function setUp(): void { - /** @var \Magento\Catalog\Model\ResourceModel\Product $productResource */ + /** @var Product $productResource */ $this->customerResource = Bootstrap::getObjectManager()->get( \Magento\Customer\Model\ResourceModel\Customer::class ); @@ -109,7 +120,7 @@ protected function setUp(): void $this->_testClassName ); $this->indexerProcessor = Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Indexer\Processor::class + Processor::class ); } @@ -140,10 +151,10 @@ public function testSaveAddressEntities() */ protected function _addTestAddress(Address $entityAdapter) { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var $objectManager ObjectManager */ $objectManager = Bootstrap::getObjectManager(); - $customers = $objectManager->get(\Magento\Framework\Registry::class)->registry($this->_fixtureKey); + $customers = $objectManager->get(Registry::class)->registry($this->_fixtureKey); /** @var $customer \Magento\Customer\Model\Customer */ $customer = reset($customers); $customerId = $customer->getId(); @@ -153,14 +164,14 @@ protected function _addTestAddress(Address $entityAdapter) \Magento\Customer\Model\Address::class ); $tableName = $addressModel->getResource()->getEntityTable(); - $addressId = $objectManager->get(\Magento\ImportExport\Model\ResourceModel\Helper::class) + $addressId = $objectManager->get(Helper::class) ->getNextAutoincrement($tableName); $newEntityData = [ 'entity_id' => $addressId, 'parent_id' => $customerId, - 'created_at' => (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT), - 'updated_at' => (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT), + 'created_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), + 'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), ]; // invoke _saveAddressEntities @@ -223,11 +234,11 @@ public function testSaveAddressAttributes() */ public function testSaveCustomerDefaults() { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var $objectManager ObjectManager */ $objectManager = Bootstrap::getObjectManager(); // get not default address - $customers = $objectManager->get(\Magento\Framework\Registry::class)->registry($this->_fixtureKey); + $customers = $objectManager->get(Registry::class)->registry($this->_fixtureKey); /** @var $notDefaultAddress \Magento\Customer\Model\Address */ $notDefaultAddress = null; /** @var $addressCustomer \Magento\Customer\Model\Customer */ @@ -300,7 +311,7 @@ public function testImportDataAddUpdate() // set fixture CSV file $sourceFile = __DIR__ . '/_files/address_import_update.csv'; - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var $objectManager ObjectManager */ $objectManager = Bootstrap::getObjectManager(); $filesystem = $objectManager->create(Filesystem::class); @@ -328,7 +339,7 @@ public function testImportDataAddUpdate() // get addresses $addressCollection = Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\ResourceModel\Address\Collection::class + Collection::class ); $addressCollection->addAttributeToSelect($requiredAttributes); $addresses = []; @@ -399,7 +410,7 @@ public function testImportDataDelete() // set fixture CSV file $sourceFile = __DIR__ . '/_files/address_import_delete.csv'; - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var $objectManager ObjectManager */ $objectManager = Bootstrap::getObjectManager(); $filesystem = $objectManager->create(Filesystem::class); $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); @@ -415,9 +426,9 @@ public function testImportDataDelete() $keyAttribute = 'postcode'; // get addresses - /** @var $addressCollection \Magento\Customer\Model\ResourceModel\Address\Collection */ + /** @var $addressCollection Collection */ $addressCollection = Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\ResourceModel\Address\Collection::class + Collection::class ); $addressCollection->addAttributeToSelect($keyAttribute); $addresses = []; @@ -442,7 +453,7 @@ public function testImportDataDelete() */ public function testDifferentOptions(): void { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var $objectManager ObjectManager */ $objectManager = Bootstrap::getObjectManager(); /** @var Filesystem $filesystem */ $filesystem = $objectManager->create(Filesystem::class); @@ -472,9 +483,10 @@ public function testDifferentOptions(): void public function testCustomerIndexer(): void { $file = __DIR__ . '/_files/address_import_update.csv'; - $filesystem = Bootstrap::getObjectManager()->create(Filesystem::class); + $filesystem = Bootstrap::getObjectManager() + ->create(Filesystem::class); $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); - $source = new \Magento\ImportExport\Model\Import\Source\Csv($file, $directoryWrite); + $source = new Csv($file, $directoryWrite); $this->_entityAdapter ->setParameters(['behavior' => ImportModel::BEHAVIOR_ADD_UPDATE]) ->setSource($source) @@ -492,23 +504,15 @@ public function testCustomerIndexer(): void /** * Test import address with region for a country that does not have regions defined * + * @magentoAppIsolation enabled * @magentoDataFixture Magento/Customer/_files/import_export/customer_with_addresses.php */ public function testImportAddressWithOptionalRegion() { - $objectManager = Bootstrap::getObjectManager(); - $customerRepository = $objectManager->get(CustomerRepositoryInterface::class); - $customer = $customerRepository->get('BetsyParker@example.com'); + $customer = $this->getCustomer('BetsyParker@example.com'); $file = __DIR__ . '/_files/import_uk_address.csv'; - $filesystem = Bootstrap::getObjectManager()->create(Filesystem::class); - $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); - $source = new Csv($file, $directoryWrite); - $errors = $this->_entityAdapter - ->setParameters(['behavior' => ImportModel::BEHAVIOR_ADD_UPDATE]) - ->setSource($source) - ->validateData(); - $this->assertEmpty($errors->getAllErrors(), 'Import validation failed'); - $this->_entityAdapter->importData(); + $errors = $this->doImport($file); + $this->assertImportValidationPassed($errors); $address = $this->getAddresses( [ 'parent_id' => $customer->getId(), @@ -520,6 +524,55 @@ public function testImportAddressWithOptionalRegion() $this->assertEquals('Liverpool', $address[0]->getRegion()->getRegion()); } + /** + * Test update first name and last name + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/import_export/customer_with_addresses.php + */ + public function testUpdateFirstAndLastName() + { + $customer = $this->getCustomer('BetsyParker@example.com'); + $addresses = $this->getAddresses( + [ + 'parent_id' => $customer->getId(), + ] + ); + $this->assertCount(1, $addresses); + $address = $addresses[0]; + $row = [ + '_website' => 'base', + '_email' => $customer->getEmail(), + '_entity_id' => $address->getId(), + 'firstname' => 'Mark', + 'lastname' => 'Antony', + ]; + $file = $this->generateImportFile([$row]); + $errors = $this->doImport($file); + $this->assertImportValidationPassed($errors); + $objectManager = Bootstrap::getObjectManager(); + //clear cache + $objectManager->get(AddressRegistry::class)->remove($address->getId()); + $addresses = $this->getAddresses( + [ + 'parent_id' => $customer->getId(), + 'entity_id' => $address->getId(), + ] + ); + $this->assertCount(1, $addresses); + $updatedAddress = $addresses[0]; + //assert that firstname and lastname were updated + $this->assertEquals($row['firstname'], $updatedAddress->getFirstname()); + $this->assertEquals($row['lastname'], $updatedAddress->getLastname()); + //assert other values have not changed + $this->assertEquals($address->getStreet(), $updatedAddress->getStreet()); + $this->assertEquals($address->getCity(), $updatedAddress->getCity()); + $this->assertEquals($address->getCountryId(), $updatedAddress->getCountryId()); + $this->assertEquals($address->getPostcode(), $updatedAddress->getPostcode()); + $this->assertEquals($address->getTelephone(), $updatedAddress->getTelephone()); + $this->assertEquals($address->getRegionId(), $updatedAddress->getRegionId()); + } + /** * Get Addresses by filter * @@ -538,4 +591,121 @@ private function getAddresses(array $filter): array } return $repository->getList($searchCriteriaBuilder->create())->getItems(); } + + /** + * @param string $email + * @return CustomerInterface + */ + private function getCustomer(string $email): CustomerInterface + { + $objectManager = Bootstrap::getObjectManager(); + $customerRepository = $objectManager->get(CustomerRepositoryInterface::class); + return $customerRepository->get($email); + } + + /** + * @param string $file + * @param string $behavior + * @param bool $validateOnly + * @return ProcessingErrorAggregatorInterface + */ + private function doImport( + string $file, + string $behavior = ImportModel::BEHAVIOR_ADD_UPDATE, + bool $validateOnly = false + ): ProcessingErrorAggregatorInterface { + $objectManager = Bootstrap::getObjectManager(); + /** @var Filesystem $filesystem */ + $filesystem = $objectManager->create(Filesystem::class); + $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = ImportAdapter::findAdapterFor($file, $directoryWrite); + $errors = $this->_entityAdapter + ->setParameters(['behavior' => $behavior]) + ->setSource($source) + ->validateData(); + if (!$validateOnly && !$errors->getAllErrors()) { + $this->_entityAdapter->importData(); + } + return $errors; + } + + /** + * @param array $data + * @return string + */ + private function generateImportFile(array $data): string + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Filesystem $filesystem */ + $filesystem = $objectManager->get(Filesystem::class); + $tmpDir = $filesystem->getDirectoryWrite(DirectoryList::TMP); + $tmpFilename = uniqid('test_import_address_') . '.csv'; + $stream = $tmpDir->openFile($tmpFilename, 'w+'); + $stream->lock(); + $stream->writeCsv($this->getFields()); + $emptyRow = array_fill_keys($this->getFields(), ''); + foreach ($data as $row) { + $row = array_replace($emptyRow, $row); + $stream->writeCsv($row); + } + $stream->unlock(); + $stream->close(); + return $tmpDir->getAbsolutePath($tmpFilename); + } + + /** + * @param ProcessingErrorAggregatorInterface $errors + */ + private function assertImportValidationPassed(ProcessingErrorAggregatorInterface $errors): void + { + if ($errors->getAllErrors()) { + $messages = []; + $messages[] = 'Import validation failed'; + $messages[] = ''; + foreach ($errors->getAllErrors() as $error) { + $messages[] = sprintf( + '%s: #%d [%s] %s: %s', + strtoupper($error->getErrorLevel()), + $error->getRowNumber(), + $error->getErrorCode(), + $error->getErrorMessage(), + $error->getErrorDescription() + ); + } + $this->fail(implode("\n", $messages)); + } + } + + /** + * @return array + */ + private function getFields(): array + { + return [ + '_website', + '_email', + '_entity_id', + 'city', + 'company', + 'country_id', + 'fax', + 'firstname', + 'lastname', + 'middlename', + 'postcode', + 'prefix', + 'region', + 'region_id', + 'street', + 'suffix', + 'telephone', + 'vat_id', + 'vat_is_valid', + 'vat_request_date', + 'vat_request_id', + 'vat_request_success', + '_address_default_billing_', + '_address_default_shipping_', + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php index e312d973aeb17..22e95a329361d 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php @@ -169,4 +169,4 @@ /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); -$productRepository->save($product)->getData(); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php index a5c88fc7571a2..2bff6f5ce82f6 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php @@ -54,7 +54,7 @@ protected function setUp(): void $this->model = $this->objectManager->create( ProductDataMapper::class, [ - 'additionalFieldsProvider' => $additionalFieldsProvider + 'additionalFieldsProvider' => $additionalFieldsProvider, ] ); $this->eavConfig = $this->objectManager->get(Config::class); @@ -83,24 +83,24 @@ public function testMapSelectAttributeWithDifferentStoreLabels(): void $defaultStoreMap = [ $productId => [ 'store_id' => $defaultStore->getId(), - 'select_attribute' => $attributeValue, + 'select_attribute' => (int)$attributeValue, 'select_attribute_value' => 'Table_default', - ] + ], ]; $secondStoreMap = [ $productId => [ 'store_id' => $secondStore->getId(), - 'select_attribute' => $attributeValue, + 'select_attribute' => (int)$attributeValue, 'select_attribute_value' => 'Table_fixture_second_store', - ] + ], ]; $data = [ $productId => [ - $attributeId => $attributeValue - ] + $attributeId => $attributeValue, + ], ]; - $this->assertEquals($defaultStoreMap, $this->model->map($data, $defaultStore->getId(), [])); - $this->assertEquals($secondStoreMap, $this->model->map($data, $secondStore->getId(), [])); + $this->assertSame($defaultStoreMap, $this->model->map($data, $defaultStore->getId(), [])); + $this->assertSame($secondStoreMap, $this->model->map($data, $secondStore->getId(), [])); } /** diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php new file mode 100644 index 0000000000000..c1fe6f11f6e6e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php @@ -0,0 +1,195 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product; + +use Magento\AdvancedSearch\Model\Client\ClientInterface; +use Magento\Catalog\Api\Data\EavAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; +use Magento\Catalog\Setup\CategorySetup; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; +use Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver; +use Magento\Elasticsearch\SearchAdapter\ConnectionManager; +use Magento\Framework\Stdlib\ArrayManager; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Check Elasticsearch indexer mapping when working with attributes. + */ +class AttributeTest extends TestCase +{ + /** + * @var ClientInterface + */ + private $client; + + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @var IndexNameResolver + */ + private $indexNameResolver; + + /** + * @var Processor + */ + private $indexerProcessor; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var CategorySetup + */ + private $installer; + + /** + * @var AttributeFactory + */ + private $attributeFactory; + + /** + * @var ProductAttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $connectionManager = Bootstrap::getObjectManager()->get(ConnectionManager::class); + $this->client = $connectionManager->getConnection(); + $this->arrayManager = Bootstrap::getObjectManager()->get(ArrayManager::class); + $this->indexNameResolver = Bootstrap::getObjectManager()->get(IndexNameResolver::class); + $this->indexerProcessor = Bootstrap::getObjectManager()->get(Processor::class); + $this->storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); + $this->installer = Bootstrap::getObjectManager()->get(CategorySetup::class); + $this->attributeFactory = Bootstrap::getObjectManager()->get(AttributeFactory::class); + $this->attributeRepository = Bootstrap::getObjectManager()->get(ProductAttributeRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + parent::tearDown(); + + /** @var ProductAttributeInterface $attribute */ + $attribute = $this->attributeRepository->get('dropdown_attribute'); + $this->attributeRepository->delete($attribute); + } + + /** + * Check Elasticsearch indexer mapping is updated after creating attribute. + * + * @return void + * @magentoConfigFixture default/catalog/search/engine elasticsearch7 + * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php + */ + public function testCheckElasticsearchMappingAfterUpdateAttributeToSearchable(): void + { + $mappedAttributesBefore = $this->getMappingProperties(); + $expectedResult = [ + 'dropdown_attribute' => [ + 'type' => 'integer', + 'index' => false, + ], + 'dropdown_attribute_value' => [ + 'type' => 'text', + 'copy_to' => ['_search'], + ], + ]; + + /** @var ProductAttributeInterface $dropDownAttribute */ + $dropDownAttribute = $this->attributeFactory->create(); + $dropDownAttribute->setData($this->getAttributeData()); + $this->attributeRepository->save($dropDownAttribute); + $this->assertTrue($this->indexerProcessor->getIndexer()->isValid()); + + $mappedAttributesAfter = $this->getMappingProperties(); + $this->assertEquals($expectedResult, array_diff_key($mappedAttributesAfter, $mappedAttributesBefore)); + + $dropDownAttribute->setData(EavAttributeInterface::IS_SEARCHABLE, true); + $this->attributeRepository->save($dropDownAttribute); + $this->assertTrue($this->indexerProcessor->getIndexer()->isInvalid()); + + $this->assertEquals($mappedAttributesAfter, $this->getMappingProperties()); + } + + /** + * Retrieve Elasticsearch indexer mapping. + * + * @return array + */ + private function getMappingProperties(): array + { + $storeId = $this->storeManager->getStore()->getId(); + $mappedIndexerId = $this->indexNameResolver->getIndexMapping(Processor::INDEXER_ID); + $indexName = $this->indexNameResolver->getIndexFromAlias($storeId, $mappedIndexerId); + $mappedAttributes = $this->client->getMapping(['index' => $indexName]); + $pathField = $this->arrayManager->findPath('properties', $mappedAttributes); + + return $this->arrayManager->get($pathField, $mappedAttributes, []); + } + + /** + * Retrieve drop-down attribute data. + * + * @return array + */ + private function getAttributeData(): array + { + $entityTypeId = $this->installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); + + return [ + 'attribute_code' => 'dropdown_attribute', + 'entity_type_id' => $entityTypeId, + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Drop-Down Attribute'], + 'backend_type' => 'varchar', + 'option' => [ + 'value' => [ + 'option_1' => ['Option 1'], + 'option_2' => ['Option 2'], + 'option_3' => ['Option 3'], + ], + 'order' => [ + 'option_1' => 1, + 'option_2' => 2, + 'option_3' => 3, + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php index 1eb2550dc484c..0173a643dd7bd 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php @@ -19,6 +19,7 @@ use Magento\Indexer\Model\Indexer; use Magento\Framework\Search\EngineResolverInterface; use Magento\TestModuleCatalogSearch\Model\ElasticsearchVersionChecker; +use PHPUnit\Framework\TestCase; /** * Important: Please make sure that each integration test file works with unique elastic search index. In order to @@ -29,7 +30,7 @@ * @magentoDataFixture Magento/Elasticsearch/_files/indexer.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class IndexHandlerTest extends \PHPUnit\Framework\TestCase +class IndexHandlerTest extends TestCase { /** * @var string @@ -116,6 +117,9 @@ public function testReindexAll(): void $products = $this->searchByName('Simple Product', $storeId); $this->assertCount(5, $products); + + $this->assertCount(2, $this->searchByBoolAttribute(0, $storeId)); + $this->assertCount(3, $this->searchByBoolAttribute(1, $storeId)); } } @@ -266,6 +270,32 @@ private function searchByName(string $text, int $storeId): array return $products; } + /** + * Search docs in Elasticsearch by boolean attribute. + * + * @param int $value + * @param int $storeId + * @return array + */ + private function searchByBoolAttribute(int $value, int $storeId): array + { + $index = $this->searchIndexNameResolver->getIndexName($storeId, $this->indexer->getId()); + $searchQuery = [ + 'index' => $index, + 'type' => $this->entityType, + 'body' => [ + 'query' => [ + 'query_string' => [ + 'query' => $value, + 'default_field' => 'boolean_attribute', + ], + ], + ], + ]; + $queryResult = $this->client->query($searchQuery); + return isset($queryResult['hits']['hits']) ? $queryResult['hits']['hits'] : []; + } + /** * Returns installed on server search service * diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php index cf87be7e8d710..c6989c7805b4a 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php @@ -4,14 +4,27 @@ * See COPYING.txt for license details. */ -/** @var $objectManager \Magento\Framework\ObjectManagerInterface */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\App\MutableScopeConfig; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_boolean_attribute.php'); -/** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ -$storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); +/** @var $objectManager \Magento\Framework\ObjectManagerInterface */ +$objectManager = Bootstrap::getObjectManager(); -/** @var \Magento\Store\Model\Store $store */ -$store = $objectManager->create(\Magento\Store\Model\Store::class); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var Store $store */ +$store = $objectManager->create(Store::class); $storeCode = 'secondary'; if (!$store->load($storeCode)->getId()) { @@ -23,92 +36,118 @@ ->setIsActive(1); $store->save(); - /** @var \Magento\Framework\App\MutableScopeConfig $scopeConfig */ - $scopeConfig = $objectManager->get(\Magento\Framework\App\MutableScopeConfig::class); + /** @var MutableScopeConfig $scopeConfig */ + $scopeConfig = $objectManager->get(MutableScopeConfig::class); $scopeConfig->setValue( 'general/locale/code', 'de_DE', - \Magento\Store\Model\ScopeInterface::SCOPE_STORES, + ScopeInterface::SCOPE_STORES, $store->getId() ); } -/** @var $productFirst \Magento\Catalog\Model\Product */ -$productFirst = $objectManager->create(\Magento\Catalog\Model\Product::class); -$productFirst->setTypeId('simple') - ->setAttributeSetId(4) - ->setWebsiteIds([1]) - ->setName('Simple Product Apple') - ->setSku('fulltext-1') - ->setPrice(10) - ->setMetaTitle('first meta title') - ->setMetaKeyword('first meta keyword') - ->setMetaDescription('first meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 0]) - ->save(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +try { + $productRepository->get('fulltext-1'); +} catch (NoSuchEntityException $e) { + /** @var $productFirst Product */ + $productFirst = $objectManager->create(Product::class); + $productFirst->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product Apple') + ->setSku('fulltext-1') + ->setPrice(10) + ->setMetaTitle('first meta title') + ->setMetaKeyword('first meta keyword') + ->setMetaDescription('first meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} -/** @var $productSecond \Magento\Catalog\Model\Product */ -$productSecond = $objectManager->create(\Magento\Catalog\Model\Product::class); -$productSecond->setTypeId('simple') - ->setAttributeSetId(4) - ->setWebsiteIds([1]) - ->setName('Simple Product Banana') - ->setSku('fulltext-2') - ->setPrice(20) - ->setMetaTitle('second meta title') - ->setMetaKeyword('second meta keyword') - ->setMetaDescription('second meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 0]) - ->save(); +try { + $productRepository->get('fulltext-2'); +} catch (NoSuchEntityException $e) { + /** @var $productSecond Product */ + $productSecond = $objectManager->create(Product::class); + $productSecond->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product Banana') + ->setSku('fulltext-2') + ->setPrice(20) + ->setMetaTitle('second meta title') + ->setMetaKeyword('second meta keyword') + ->setMetaDescription('second meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} -/** @var $productThird \Magento\Catalog\Model\Product */ -$productThird = $objectManager->create(\Magento\Catalog\Model\Product::class); -$productThird->setTypeId('simple') - ->setAttributeSetId(4) - ->setWebsiteIds([1]) - ->setName('Simple Product Orange') - ->setSku('fulltext-3') - ->setPrice(20) - ->setMetaTitle('third meta title') - ->setMetaKeyword('third meta keyword') - ->setMetaDescription('third meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 0]) - ->save(); +try { + $productRepository->get('fulltext-3'); +} catch (NoSuchEntityException $e) { + /** @var $productThird Product */ + $productThird = $objectManager->create(Product::class); + $productThird->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product Orange') + ->setSku('fulltext-3') + ->setPrice(20) + ->setMetaTitle('third meta title') + ->setMetaKeyword('third meta keyword') + ->setMetaDescription('third meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} -/** @var $productFourth \Magento\Catalog\Model\Product */ -$productFourth = $objectManager->create(\Magento\Catalog\Model\Product::class); -$productFourth->setTypeId('simple') - ->setAttributeSetId(4) - ->setWebsiteIds([1]) - ->setName('Simple Product Papaya') - ->setSku('fulltext-4') - ->setPrice(20) - ->setMetaTitle('fourth meta title') - ->setMetaKeyword('fourth meta keyword') - ->setMetaDescription('fourth meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 0]) - ->save(); +try { + $productRepository->get('fulltext-4'); +} catch (NoSuchEntityException $e) { + /** @var $productFourth Product */ + $productFourth = $objectManager->create(Product::class); + $productFourth->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product Papaya') + ->setSku('fulltext-4') + ->setPrice(20) + ->setMetaTitle('fourth meta title') + ->setMetaKeyword('fourth meta keyword') + ->setMetaDescription('fourth meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(0) + ->save(); +} -/** @var $productFifth \Magento\Catalog\Model\Product */ -$productFifth = $objectManager->create(\Magento\Catalog\Model\Product::class); -$productFifth->setTypeId('simple') - ->setAttributeSetId(4) - ->setWebsiteIds([1]) - ->setName('Simple Product Cherry') - ->setSku('fulltext-5') - ->setPrice(20) - ->setMetaTitle('fifth meta title') - ->setMetaKeyword('fifth meta keyword') - ->setMetaDescription('fifth meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 0]) - ->save(); +try { + $productRepository->get('fulltext-5'); +} catch (NoSuchEntityException $e) { + /** @var $productFifth Product */ + $productFifth = $objectManager->create(Product::class); + $productFifth->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product Cherry') + ->setSku('fulltext-5') + ->setPrice(20) + ->setMetaTitle('fifth meta title') + ->setMetaKeyword('fifth meta keyword') + ->setMetaDescription('fifth meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(0) + ->save(); +} 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/Elasticsearch6/Controller/QuickSearchTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch6/Controller/QuickSearchTest.php index 200360b7340bd..1d640e62dc5d4 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch6/Controller/QuickSearchTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch6/Controller/QuickSearchTest.php @@ -64,6 +64,6 @@ public function testQuickSearchWithImprovedPriceRangeCalculation() $this->storeManager->setCurrentStore($defaultStore); } - $this->assertContains('search product 1', $responseBody); + $this->assertStringContainsString('search product 1', $responseBody); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample index 74c1522fa41f0..930c439899f03 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample @@ -21,11 +21,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicChildMethod(\Laminas\Code\Generator\ClassGenerator $classGenerator, $param1 = '', $param2 = '\\', $param3 = '\'', array $array = []) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicChildMethod'); - if (!$pluginInfo) { - return parent::publicChildMethod($classGenerator, $param1, $param2, $param3, $array); - } else { - return $this->___callPlugins('publicChildMethod', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicChildMethod', func_get_args(), $pluginInfo) : parent::publicChildMethod($classGenerator, $param1, $param2, $param3, $array); } /** @@ -34,11 +30,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicMethodWithReference(\Laminas\Code\Generator\ClassGenerator &$classGenerator, &$param1, array &$array) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicMethodWithReference'); - if (!$pluginInfo) { - return parent::publicMethodWithReference($classGenerator, $param1, $array); - } else { - return $this->___callPlugins('publicMethodWithReference', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicMethodWithReference', func_get_args(), $pluginInfo) : parent::publicMethodWithReference($classGenerator, $param1, $array); } /** @@ -47,11 +39,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicChildWithoutParameters() { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicChildWithoutParameters'); - if (!$pluginInfo) { - return parent::publicChildWithoutParameters(); - } else { - return $this->___callPlugins('publicChildWithoutParameters', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicChildWithoutParameters', func_get_args(), $pluginInfo) : parent::publicChildWithoutParameters(); } /** @@ -60,11 +48,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function public71($arg1, string $arg2, ?int $arg3, ?int $arg4 = null) : void { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'public71'); - if (!$pluginInfo) { - parent::public71($arg1, $arg2, $arg3, $arg4); - } else { - $this->___callPlugins('public71', func_get_args(), $pluginInfo); - } + $pluginInfo ? $this->___callPlugins('public71', func_get_args(), $pluginInfo) : parent::public71($arg1, $arg2, $arg3, $arg4); } /** @@ -73,11 +57,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function public71Another(?\DateTime $arg1, $arg2 = false) : ?string { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'public71Another'); - if (!$pluginInfo) { - return parent::public71Another($arg1, $arg2); - } else { - return $this->___callPlugins('public71Another', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('public71Another', func_get_args(), $pluginInfo) : parent::public71Another($arg1, $arg2); } /** @@ -86,11 +66,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicWithSelf($arg = false) : \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespace { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicWithSelf'); - if (!$pluginInfo) { - return parent::publicWithSelf($arg); - } else { - return $this->___callPlugins('publicWithSelf', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicWithSelf', func_get_args(), $pluginInfo) : parent::publicWithSelf($arg); } /** @@ -99,11 +75,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicParentMethod(\Laminas\Code\Generator\DocBlockGenerator $docBlockGenerator, $param1 = '', $param2 = '\\', $param3 = '\'', array $array = []) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicParentMethod'); - if (!$pluginInfo) { - return parent::publicParentMethod($docBlockGenerator, $param1, $param2, $param3, $array); - } else { - return $this->___callPlugins('publicParentMethod', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicParentMethod', func_get_args(), $pluginInfo) : parent::publicParentMethod($docBlockGenerator, $param1, $param2, $param3, $array); } /** @@ -112,10 +84,6 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicParentWithoutParameters() { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicParentWithoutParameters'); - if (!$pluginInfo) { - return parent::publicParentWithoutParameters(); - } else { - return $this->___callPlugins('publicParentWithoutParameters', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicParentWithoutParameters', func_get_args(), $pluginInfo) : parent::publicParentWithoutParameters(); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock index 064b5d5f992ab..85ee46cd823b8 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock +++ b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock @@ -2352,10 +2352,6 @@ ".php_cs.dist", ".php_cs.dist" ], - [ - ".travis.yml", - ".travis.yml" - ], [ ".user.ini", ".user.ini" @@ -2556,10 +2552,6 @@ "dev/tools", "dev/tools" ], - [ - "dev/travis", - "dev/travis" - ], [ "generated", "generated" @@ -4775,7 +4767,7 @@ "shasum": "ed1da1137848560dde1a85f0f54dc2fac262359e" }, "require": { - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "magento/framework": "102.0.*", "magento/module-advanced-search": "100.3.*", "magento/module-catalog": "103.0.*", @@ -4815,7 +4807,7 @@ "shasum": "a9da3243900390ad163efc7969b07116d2eb793f" }, "require": { - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "magento/framework": "102.0.*", "magento/module-advanced-search": "100.3.*", "magento/module-catalog-search": "101.0.*", @@ -9408,7 +9400,7 @@ "colinmollenhour/php-redis-session-abstract": "~1.4.0", "composer/composer": "^1.6", "dotmailer/dotmailer-magento2-extension": "3.1.1", - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", diff --git a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock index a6f208c9c0d8d..4c2f8692bf805 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock +++ b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock @@ -2352,10 +2352,6 @@ ".php_cs.dist", ".php_cs.dist" ], - [ - ".travis.yml", - ".travis.yml" - ], [ ".user.ini", ".user.ini" @@ -2556,10 +2552,6 @@ "dev/tools", "dev/tools" ], - [ - "dev/travis", - "dev/travis" - ], [ "generated", "generated" @@ -4775,7 +4767,7 @@ "shasum": "ed1da1137848560dde1a85f0f54dc2fac262359e" }, "require": { - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "magento/framework": "102.0.*", "magento/module-advanced-search": "100.3.*", "magento/module-catalog": "103.0.*", @@ -4815,7 +4807,7 @@ "shasum": "a9da3243900390ad163efc7969b07116d2eb793f" }, "require": { - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "magento/framework": "102.0.*", "magento/module-advanced-search": "100.3.*", "magento/module-catalog-search": "101.0.*", @@ -9408,7 +9400,7 @@ "colinmollenhour/php-redis-session-abstract": "~1.4.0", "composer/composer": "^1.6", "dotmailer/dotmailer-magento2-extension": "3.1.1", - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", diff --git a/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php b/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php index cdbfa26111d0f..c6aeaf9e0f927 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php @@ -79,6 +79,7 @@ protected function tearDown(): void * Checks that settings from env.php config file are applied * to created application instance. * + * @magentoAppIsolation enabled * @param bool $isPub * @param array $params * @dataProvider documentRootIsPubProvider diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php index 8388f2e81c0aa..5dfab6fcc756c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php @@ -9,6 +9,9 @@ */ namespace Magento\Framework\DB\Adapter; +/** + * @magentoDbIsolation disabled + */ class InterfaceTest extends \PHPUnit\Framework\TestCase { /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php index 345302a374081..6e3391bd8959f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php @@ -10,6 +10,11 @@ use Magento\Framework\DB\Ddl\Table; use Magento\TestFramework\Helper\Bootstrap; +/** + * Class checks Mysql adapter behaviour + * + * @magentoDbIsolation disabled + */ class MysqlTest extends \PHPUnit\Framework\TestCase { /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php index d4507237b0ad1..db5e90d46880c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php @@ -67,10 +67,13 @@ public function testTransactionLevelDbIsolationEnabled() $this->assertEquals(1, $resourceConnection->getConnection('default')->getTransactionLevel()); } + /** + * @magentoDataFixture Magento/Framework/DB/_files/dummy_fixture.php + */ public function testTransactionLevelDbIsolationDefault() { $resourceConnection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->get(\Magento\Framework\App\ResourceConnection::class); - $this->assertEquals(0, $resourceConnection->getConnection('default')->getTransactionLevel()); + $this->assertEquals(1, $resourceConnection->getConnection('default')->getTransactionLevel()); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/_files/dummy_fixture.php b/dev/tests/integration/testsuite/Magento/Framework/DB/_files/dummy_fixture.php new file mode 100644 index 0000000000000..2dc96aa234590 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/_files/dummy_fixture.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +//this fixture should not do anything diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/Template/Tokenizer/ParameterTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/Template/Tokenizer/ParameterTest.php index 8d4ebc40128d1..aad47165a470a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filter/Template/Tokenizer/ParameterTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/Template/Tokenizer/ParameterTest.php @@ -3,20 +3,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Filter\Template\Tokenizer; -class ParameterTest extends \PHPUnit\Framework\TestCase +use Magento\Catalog\Block\Product\Widget\NewWidget; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\Framework\Filter\Template\Tokenizer\Parameter. + */ +class ParameterTest extends TestCase { /** + * Test for getValue + * + * @dataProvider getValueDataProvider + * * @param string $string * @param array $values - * @dataProvider getValueDataProvider + * @return void */ - public function testGetValue($string, $values) + public function testGetValue($string, $values): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @var \Magento\Framework\Filter\Template\Tokenizer\Parameter $parameter */ - $parameter = $objectManager->create(\Magento\Framework\Filter\Template\Tokenizer\Parameter::class); + $objectManager = Bootstrap::getObjectManager(); + /** @var Parameter $parameter */ + $parameter = $objectManager->create(Parameter::class); $parameter->setString($string); foreach ($values as $value) { @@ -25,30 +38,36 @@ public function testGetValue($string, $values) } /** + * Test for tokenize + * * @dataProvider tokenizeDataProvider + * * @param string $string * @param array $params + * @return void */ - public function testTokenize($string, $params) + public function testTokenize(string $string, array $params): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @var \Magento\Framework\Filter\Template\Tokenizer\Parameter $parameter */ - $parameter = $objectManager->create(\Magento\Framework\Filter\Template\Tokenizer\Parameter::class); + $objectManager = Bootstrap::getObjectManager(); + $parameter = $objectManager->create(Parameter::class); $parameter->setString($string); + $this->assertEquals($params, $parameter->tokenize()); } /** + * DataProvider for testTokenize + * * @return array */ - public function tokenizeDataProvider() + public function tokenizeDataProvider(): array { return [ [ ' type="Magento\\Catalog\\Block\\Product\\Widget\\NewWidget" display_type="all_products"' . ' products_count="10" template="product/widget/new/content/new_grid.phtml"', [ - 'type' => \Magento\Catalog\Block\Product\Widget\NewWidget::class, + 'type' => NewWidget::class, 'display_type' => 'all_products', 'products_count' => 10, 'template' => 'product/widget/new/content/new_grid.phtml' @@ -58,12 +77,24 @@ public function tokenizeDataProvider() ' type="Magento\Catalog\Block\Product\Widget\NewWidget" display_type="all_products"' . ' products_count="10" template="product/widget/new/content/new_grid.phtml"', [ - 'type' => \Magento\Catalog\Block\Product\Widget\NewWidget::class, + 'type' => NewWidget::class, 'display_type' => 'all_products', 'products_count' => 10, 'template' => 'product/widget/new/content/new_grid.phtml' ] - ] + ], + [ + sprintf( + 'type="%s" display_type="all_products" products_count="1" template="content/new_grid.phtml"', + NewWidget::class + ), + [ + 'type' => NewWidget::class, + 'display_type' => 'all_products', + 'products_count' => 1, + 'template' => 'content/new_grid.phtml' + ], + ], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/VariableResolver/LegacyResolverTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/VariableResolver/LegacyResolverTest.php index 6cd211be6f14d..e663b8ccedceb 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filter/VariableResolver/LegacyResolverTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/VariableResolver/LegacyResolverTest.php @@ -117,6 +117,7 @@ public function getThing() ['foo' => $dataClassStub, 'g' => ['h' => ['i' => 'abc']]], 'abca=123,b=321,' ], + 'disallow __callParent method' => ['foo.___callParent()',['foo' => $classStub], null], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php index 79c8765dd4220..96e31a753adaa 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php @@ -47,9 +47,11 @@ protected function setUp(): void $fileResolverMock = $this->getMockBuilder( \Magento\Framework\Config\FileResolverInterface::class )->disableOriginalConstructor()->getMock(); + $filePath1 = __DIR__ . '/../_files/schemaA.graphqls'; + $filePath2 = __DIR__ . '/../_files/schemaB.graphqls'; $fileList = [ - file_get_contents(__DIR__ . '/../_files/schemaA.graphqls'), - file_get_contents(__DIR__ . '/../_files/schemaB.graphqls') + $filePath1 => file_get_contents($filePath1), + $filePath2 => file_get_contents($filePath2) ]; $fileResolverMock->expects($this->any())->method('get')->willReturn($fileList); $graphQlReader = $this->objectManager->create( @@ -219,31 +221,25 @@ function ($a, $b) { } //Checks to make sure that the given description exists in the expectedOutput array $this->assertArrayHasKey( - - array_search( - 'Comment for empty PhysicalProductInterface', - array_column($expectedOutput, 'description') - ), - $expectedOutput - + array_search( + 'Comment for empty PhysicalProductInterface', + array_column($expectedOutput, 'description') + ), + $expectedOutput ); $this->assertArrayHasKey( - - array_search( - 'Comment for empty Enum', - array_column($expectedOutput, 'description') - ), - $expectedOutput - + array_search( + 'Comment for empty Enum', + array_column($expectedOutput, 'description') + ), + $expectedOutput ); $this->assertArrayHasKey( - - array_search( - 'Comment for SearchResultPageInfo', - array_column($expectedOutput, 'description') - ), - $expectedOutput - + array_search( + 'Comment for SearchResultPageInfo', + array_column($expectedOutput, 'description') + ), + $expectedOutput ); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php index 5d4047b1456d5..f74d96bcc42a3 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php @@ -36,9 +36,11 @@ protected function setUp(): void $fileResolverMock = $this->getMockBuilder( \Magento\Framework\Config\FileResolverInterface::class )->disableOriginalConstructor()->getMock(); + $filePath1 = __DIR__ . '/_files/schemaC.graphqls'; + $filePath2 = __DIR__ . '/_files/schemaD.graphqls'; $fileList = [ - file_get_contents(__DIR__ . '/_files/schemaC.graphqls'), - file_get_contents(__DIR__ . '/_files/schemaD.graphqls') + $filePath1 => file_get_contents($filePath1), + $filePath2 => file_get_contents($filePath2) ]; $fileResolverMock->expects($this->any())->method('get')->willReturn($fileList); $graphQlReader = $objectManager->create( @@ -46,10 +48,12 @@ protected function setUp(): void ['fileResolver' => $fileResolverMock] ); $reader = $objectManager->create( + // phpstan:ignore \Magento\Framework\GraphQlSchemaStitching\Reader::class, ['readers' => ['graphql_reader' => $graphQlReader]] ); $data = $objectManager->create( + // phpstan:ignore \Magento\Framework\GraphQl\Config\Data ::class, ['reader' => $reader] ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php b/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php index 1f65bca8f5f1d..b9deeb3bb968f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\Interception; +use Magento\Framework\App\Filesystem\DirectoryList; + /** * Class GeneralTest * @@ -81,6 +83,10 @@ public function setUpInterceptionConfig($pluginConfig) $cacheManager->method('load')->willReturn(null); $definitions = new \Magento\Framework\ObjectManager\Definition\Runtime(); $relations = new \Magento\Framework\ObjectManager\Relations\Runtime(); + $configLoader = $this->createMock(ConfigLoaderInterface::class); + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $directoryList = $this->createMock(DirectoryList::class); + $configWriter = $this->createMock(PluginListGenerator::class); $interceptionConfig = new Config\Config( $this->_configReader, $configScope, @@ -104,6 +110,10 @@ public function setUpInterceptionConfig($pluginConfig) \Magento\Framework\ObjectManager\DefinitionInterface::class => $definitions, \Magento\Framework\Interception\DefinitionInterface::class => $interceptionDefinitions, \Magento\Framework\Serialize\SerializerInterface::class => $json, + \Magento\Framework\Interception\ConfigLoaderInterface::class => $configLoader, + \Psr\Log\LoggerInterface::class => $logger, + \Magento\Framework\App\Filesystem\DirectoryList::class => $directoryList, + \Magento\Framework\App\ObjectManager\ConfigWriterInterface::class => $configWriter ]; $this->_objectManager = new \Magento\Framework\ObjectManager\ObjectManager( $factory, @@ -118,8 +128,8 @@ public function setUpInterceptionConfig($pluginConfig) 'preferences' => [ \Magento\Framework\Interception\PluginListInterface::class => \Magento\Framework\Interception\PluginList\PluginList::class, - \Magento\Framework\Interception\ChainInterface::class => - \Magento\Framework\Interception\Chain\Chain::class, + \Magento\Framework\Interception\ConfigWriterInterface::class => + \Magento\Framework\Interception\PluginListGenerator::class ], ] ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Interception/PluginListGeneratorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Interception/PluginListGeneratorTest.php new file mode 100644 index 0000000000000..1046c678e253a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Interception/PluginListGeneratorTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Interception; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Application; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Provide tests for PluginListGeneratorTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PluginListGeneratorTest extends TestCase +{ + /** + * Generated plugin list config for frontend scope + */ + const CACHE_ID = 'primary|global|frontend|plugin-list'; + + /** + * @var PluginListGenerator + */ + private $model; + + /** + * @var DirectoryList + */ + private $directoryList; + + /** + * @var DriverInterface + */ + private $file; + + /** + * @var Application + */ + private $application; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->application = Bootstrap::getInstance()->getBootstrap()->getApplication(); + $this->directoryList = new DirectoryList(BP, $this->getCustomDirs()); + $this->file = Bootstrap::getObjectManager()->create(DriverInterface::class); + $reader = Bootstrap::getObjectManager()->create( + // phpstan:ignore "Class Magento\Framework\ObjectManager\Config\Reader\Dom\Proxy not found." + \Magento\Framework\ObjectManager\Config\Reader\Dom\Proxy::class + ); + $scopeConfig = Bootstrap::getObjectManager()->create(\Magento\Framework\Config\Scope::class); + $omConfig = Bootstrap::getObjectManager()->create( + \Magento\Framework\Interception\ObjectManager\Config\Developer::class + ); + $relations = Bootstrap::getObjectManager()->create( + \Magento\Framework\ObjectManager\Relations\Runtime::class + ); + $definitions = Bootstrap::getObjectManager()->create( + \Magento\Framework\Interception\Definition\Runtime::class + ); + $classDefinitions = Bootstrap::getObjectManager()->create( + \Magento\Framework\ObjectManager\Definition\Runtime::class + ); + // phpstan:ignore "Class Psr\Log\LoggerInterface\Proxy not found." + $logger = Bootstrap::getObjectManager()->create(\Psr\Log\LoggerInterface\Proxy::class); + $this->model = new PluginListGenerator( + $reader, + $scopeConfig, + $omConfig, + $relations, + $definitions, + $classDefinitions, + $logger, + $this->directoryList, + ['primary', 'global'] + ); + } + + /** + * Test plugin list configuration generation and load. + */ + public function testPluginListConfigGeneration() + { + $scopes = ['frontend']; + $this->model->write($scopes); + $configData = $this->model->load(self::CACHE_ID); + $this->assertNotEmpty($configData[0]); + $this->assertNotEmpty($configData[1]); + $this->assertNotEmpty($configData[2]); + $expected = [ + 1 => [ + 0 => 'genericHeaderPlugin', + 1 => 'response-http-page-cache' + ] + ]; + // Here in test is assumed that this class below has 3 plugins. But the amount of plugins and class itself + // may vary. If it is changed, please update these assertions. + $this->assertArrayHasKey( + 'Magento\\Framework\\App\\Response\\Http_sendResponse___self', + $configData[2], + 'Processed plugin does not exist in the processed plugins array.' + ); + + $this->assertSame( + $expected, + $configData[2]['Magento\\Framework\\App\\Response\\Http_sendResponse___self'], + 'Plugin configurations are not equal' + ); + } + + /** + * Gets customized directory paths + * + * @return array + */ + private function getCustomDirs(): array + { + $path = DirectoryList::PATH; + $generated = "{$this->application->getTempDir()}/generated"; + + return [ + DirectoryList::GENERATED_METADATA => [$path => "{$generated}/metadata"], + ]; + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $filePath = $this->directoryList->getPath(DirectoryList::GENERATED_METADATA) + . '/' . self::CACHE_ID . '.' . 'php'; + + if (file_exists($filePath)) { + $this->file->deleteFile($filePath); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php index 306bda462820a..81ab34fae9b98 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php @@ -47,16 +47,10 @@ public function testParallelLock(): void { $identifier1 = \uniqid('lock_name_1_', true); - $this->assertTrue($this->cacheInstance1->lock($identifier1, 2)); + $this->assertTrue($this->cacheInstance1->lock($identifier1)); - $this->assertFalse($this->cacheInstance1->lock($identifier1, 2)); - $this->assertFalse($this->cacheInstance2->lock($identifier1, 2)); - sleep(4); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); - - $this->assertTrue($this->cacheInstance2->lock($identifier1, -1)); - sleep(4); - $this->assertTrue($this->cacheInstance1->isLocked($identifier1)); + $this->assertFalse($this->cacheInstance1->lock($identifier1, 0)); + $this->assertFalse($this->cacheInstance2->lock($identifier1, 0)); } /** @@ -66,19 +60,17 @@ public function testParallelLock(): void */ public function testParallelLockExpired(): void { - $identifier1 = \uniqid('lock_name_1_', true); + $testLifeTime = 2; + \Closure::bind(function (Cache $class) use ($testLifeTime) { + $class->defaultLifetime = $testLifeTime; + }, null, $this->cacheInstance1)($this->cacheInstance1); - $this->assertTrue($this->cacheInstance1->lock($identifier1, 1)); - sleep(2); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); + $identifier1 = \uniqid('lock_name_1_', true); - $this->assertTrue($this->cacheInstance1->lock($identifier1, 1)); - sleep(2); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); + $this->assertTrue($this->cacheInstance1->lock($identifier1, 0)); + $this->assertTrue($this->cacheInstance2->lock($identifier1, $testLifeTime + 1)); - $this->assertTrue($this->cacheInstance2->lock($identifier1, 1)); - sleep(2); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); + $this->cacheInstance2->unlock($identifier1); } /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php index c11004f503c40..e5fed191ea17e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php @@ -66,14 +66,21 @@ protected function setUp(): void */ public function testWaitForMessages() { - $this->assertArrayHasKey('queue', $this->config); - $this->assertArrayHasKey('consumers_wait_for_messages', $this->config['queue']); - $this->assertEquals(1, $this->config['queue']['consumers_wait_for_messages']); + $this->publisherConsumerController->stopConsumers(); + + $config = $this->config; + $config['queue']['consumers_wait_for_messages'] = 1; + $this->writeConfig($config); + + $loadedConfig = $this->loadConfig(); + $this->assertArrayHasKey('queue', $loadedConfig); + $this->assertArrayHasKey('consumers_wait_for_messages', $loadedConfig['queue']); + $this->assertEquals(1, $loadedConfig['queue']['consumers_wait_for_messages']); foreach ($this->messages as $message) { $this->publishMessage($message); } - + $this->publisherConsumerController->startConsumers(); $this->waitForAsynchronousResult(count($this->messages), $this->logFilePath); foreach ($this->messages as $item) { diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php index 2797cad61084c..ba2225fbe5eac 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php @@ -9,6 +9,8 @@ /** * Test Class for \Magento\Framework\Mview\View\Changelog + * + * @magentoDbIsolation disabled */ class ChangelogTest extends \PHPUnit\Framework\TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/CompiledTest.php b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/CompiledTest.php index c620251ca9b67..7d3b9d2089cf9 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/CompiledTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/CompiledTest.php @@ -14,6 +14,9 @@ use Magento\Framework\ObjectManager\TestAsset\InterfaceImplementation; use Magento\Framework\ObjectManager\TestAsset\TestAssetInterface; +/** + * @magentoAppIsolation enabled + */ class CompiledTest extends AbstractFactoryRuntimeDefinitionsTestCases { /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/Dynamic/DeveloperTest.php b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/Dynamic/DeveloperTest.php index 7fa7e677e0d8d..c74c00de4ce53 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/Dynamic/DeveloperTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/Dynamic/DeveloperTest.php @@ -15,6 +15,9 @@ use Magento\Framework\ObjectManager\TestAsset\InterfaceImplementation; use Magento\Framework\ObjectManager\TestAsset\TestAssetInterface; +/** + * @magentoAppIsolation enabled + */ class DeveloperTest extends AbstractFactoryRuntimeDefinitionsTestCases { /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/Session/SessionManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Session/SessionManagerTest.php index c58689f0cd8e7..d35d875ff8006 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Session/SessionManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Session/SessionManagerTest.php @@ -225,22 +225,6 @@ public function testSetSessionId() $this->assertEquals('test', $this->model->getSessionId()); } - /** - * @magentoConfigFixture current_store web/session/use_frontend_sid 1 - */ - public function testSetSessionIdFromParam() - { - $this->initializeModel(); - $this->appState->expects($this->any()) - ->method('getAreaCode') - ->willReturn(\Magento\Framework\App\Area::AREA_FRONTEND); - $currentId = $this->model->getSessionId(); - $this->assertNotEquals('test_id', $this->model->getSessionId()); - $this->request->getQuery()->set(SidResolverInterface::SESSION_ID_QUERY_PARAM, 'test-id'); - $this->model->setSessionId($this->sidResolver->getSid($this->model)); - $this->assertEquals($currentId, $this->model->getSessionId()); - } - public function testGetSessionIdForHost() { $this->initializeModel(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php index 04f64ff93ab1e..785637a9470cb 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php @@ -23,13 +23,6 @@ protected function setUp(): void $this->model = Bootstrap::getObjectManager()->create(\Magento\Framework\Url::class); } - public function testSetGetUseSession() - { - $this->assertFalse((bool)$this->model->getUseSession()); - $this->model->setUseSession(false); - $this->assertFalse($this->model->getUseSession()); - } - public function testSetRouteFrontName() { $value = 'route'; diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTemplateTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTemplateTest.php new file mode 100644 index 0000000000000..0d2b85b4ae20d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTemplateTest.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\View\Helper; + +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test the secure HTML helper and templates. + * + * @magentoAppArea frontend + */ +class SecureHtmlRendererTemplateTest extends AbstractController +{ + /** + * Test using the helper inside templates. + * + * @return void + */ + public function testTemplateUsage(): void + { + $this->getRequest()->setMethod('GET'); + $this->dispatch('securehtml/secure/helper'); + $content = $this->getResponse()->getContent(); + + $this->assertStringContainsString( + '<h1 onclick="alert()">Hello there!</h1>', + $content + ); + $this->assertStringContainsString( + '<script src="http://my.magento.com/static/script.js"/>', + $content + ); + $this->assertStringContainsString( + "<script>\n let myVar = 1;\n</script>", + $content + ); + $this->assertStringContainsString( + '<div>I am just <a> text</div>', + $content + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTest.php new file mode 100644 index 0000000000000..8fcb464aec734 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\View\Helper; + +use Magento\Framework\View\Helper\SecureHtmlRender\TagData; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for the secure HTML helper. + */ +class SecureHtmlRendererTest extends TestCase +{ + /** + * @var SecureHtmlRenderer + */ + private $helper; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + //Clearing the processors list to ensure stable results. + $this->helper = $objectManager->create(SecureHtmlRenderer::class, ['processors' => []]); + } + + /** + * Provides tags to render. + * + * @return array + */ + public function getTags(): array + { + return [ + [ + new TagData('div', ['style' => 'display: none;', 'width' => '20px'], 'some <text>', true), + '<div style="display: none;" width="20px">some <text></div>' + ], + [ + new TagData('div', [], 'some <b>HTML</b>', false), + '<div>some <b>HTML</b></div>' + ], + [ + new TagData('img', ['src' => 'https://magento.com/img.jpg'], null, true), + '<img src="https://magento.com/img.jpg"/>' + ] + ]; + } + + /** + * Test tag rendering. + * + * @param TagData $tagData + * @param string $expected Expected HTML. + * @return void + * @dataProvider getTags + */ + public function testRenderTag(TagData $tagData, string $expected): void + { + $this->assertEquals( + $expected, + $this->helper->renderTag( + $tagData->getTag(), + $tagData->getAttributes(), + $tagData->getContent(), + $tagData->isTextContent() + ) + ); + } + + /** + * Test rendering an event listener. + * + * @return void + */ + public function testRenderEventHandler(): void + { + $this->assertEquals( + 'onclick="alert(this.parent.getAttribute("data-title"))"', + $this->helper->renderEventListener('onclick', 'alert(this.parent.getAttribute("data-title"))') + ); + } + + /** + * Test rendering JS listeners as separate tags. + * + * @return void + */ + public function testRenderEventListenerAsTag(): void + { + $html = $this->helper->renderEventListenerAsTag('onclick', 'alert(1)', '#id'); + $this->assertStringContainsString('alert(1)', $html); + $this->assertStringContainsString('#id', $html); + $this->assertStringContainsString('click', $html); + } + + /** + * Check handler validation + * + * @return void + */ + public function testInvalidEventListener(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->helper->renderEventListenerAsTag('nonevent', '', ''); + } + + /** + * Test rendering "style" attribute as separate tag. + * + * @return void + */ + public function testRenderStyleAsTag(): void + { + $html = $this->helper->renderStyleAsTag('display: none; font-size: 3em; ', '#id'); + $this->assertStringContainsString('#id', $html); + $this->assertStringContainsString('display', $html); + $this->assertStringContainsString('none', $html); + $this->assertStringContainsString('fontSize', $html); + $this->assertStringContainsString('3em', $html); + } + + /** + * Check style validation + * + * @return void + */ + public function testInvalidStyle(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->helper->renderStyleAsTag('display;', ''); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message.php new file mode 100644 index 0000000000000..55b38d9900acd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\GiftMessage\Model\Message; +use Magento\GiftMessage\Model\ResourceModel\Message as MessageResource; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address as OrderAddress; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$addressData = include __DIR__ . '/../../../../Magento/Sales/_files/address_data.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Order $order */ +/** @var Order\Payment $payment */ +/** @var Order\Item $orderItem */ +/** @var array $addressData Data for creating addresses for the orders. */ +$orders = [ + [ + 'increment_id' => '999999990', + 'state' => Order::STATE_NEW, + 'status' => 'processing', + 'grand_total' => 120.00, + 'subtotal' => 120.00, + 'base_grand_total' => 120.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '999999991', + 'state' => Order::STATE_PROCESSING, + 'status' => 'processing', + 'grand_total' => 130.00, + 'base_grand_total' => 130.00, + 'subtotal' => 130.00, + 'total_paid' => 130.00, + 'store_id' => 1, + 'website_id' => 1, + ] +]; + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); + +/** @var array $orderData */ +foreach ($orders as $orderData) { + /** @var Magento\Sales\Model\Order $order */ + $order = $objectManager->create(Order::class); + + // Reset addresses + /** @var Order\Address $billingAddress */ + $billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); + $billingAddress->setAddressType('billing'); + + $shippingAddress = clone $billingAddress; + $shippingAddress->setId(null)->setAddressType('shipping'); + + /** @var MessageResource $message */ + $message = $objectManager->create(MessageResource::class); + + /** @var Message $message */ + $messageModel = $objectManager->create(Message::class); + + $messageModel->setSender('John Doe'); + $messageModel->setRecipient('Jane Roe'); + $messageModel->setMessage('Gift Message Text'); + $message->save($messageModel); + + /** @var Order\Item $orderItem */ + $orderItem = $objectManager->create(Order\Item::class); + $orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple'); + + $order + ->setData($orderData) + ->addItem($orderItem) + ->setCustomerIsGuest(false) + ->setCustomerId(1) + ->setCustomerEmail('customer@example.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setPayment($payment); + $order->setGiftMessageId($messageModel->getId()); + $orderRepository->save($order); +} diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message_rollback.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message_rollback.php new file mode 100644 index 0000000000000..5aaf728243729 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message_rollback.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Registry; +use Magento\GiftMessage\Model\Message; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); +$productRepository->delete($product); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var OrderInterfaceFactory $orderFactory */ +$orderFactory = $objectManager->create(OrderInterfaceFactory::class); +$orders = []; +$orders[] = $orderFactory->create()->loadByIncrementId('999999990'); +$orders[] = $orderFactory->create()->loadByIncrementId('999999991'); + +foreach ($orders as $order) { + if ($order->getGiftMessageId()) { + $message = $objectManager->create(Message::class); + $message->load($order->getGiftMessageId()); + $message->delete(); + } + if ($order->getId()) { + $orderRepository->delete($order); + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php new file mode 100644 index 0000000000000..4cbe088893b03 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\GiftMessage\Model\Message; +use Magento\GiftMessage\Model\ResourceModel\Message as MessageResource; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResource; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products.php'); + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var QuoteResource $quote */ +$quote = $objectManager->create(QuoteResource::class); + +/** @var Quote $quoteModel */ +$quoteModel = $objectManager->create(Quote::class); +$quoteModel->setData(['store_id' => 1, 'is_active' => 1, 'is_multi_shipping' => 0]); +$quote->save($quoteModel); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); + +$quoteModel->setReservedOrderId('test_guest_order_with_gift_message') + ->addProduct($product, 1); +$quoteModel->collectTotals(); +$quote->save($quoteModel); + +/** @var MessageResource $message */ +$message = $objectManager->create(MessageResource::class); + +/** @var Message $message */ +$messageModel = $objectManager->create(Message::class); + +$messageModel->setSender('John Doe'); +$messageModel->setRecipient('Jane Roe'); +$messageModel->setMessage('Gift Message Text'); +$message->save($messageModel); + +$quoteModel->getItemByProduct($product)->setGiftMessageId($messageModel->getId()); +$quote->save($quoteModel); + +/** @var QuoteIdMaskResource $quoteIdMask */ +$quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) + ->create(); + +/** @var QuoteIdMask $quoteIdMaskModel */ +$quoteIdMaskModel = $objectManager->create(QuoteIdMask::class); + +$quoteIdMaskModel->setQuoteId($quoteModel->getId()); +$quoteIdMaskModel->setDataChanges(true); +$quoteIdMask->save($quoteIdMaskModel); diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php new file mode 100644 index 0000000000000..9c215cb432b45 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\Product; +use Magento\Framework\Registry; +use Magento\GiftMessage\Model\Message; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; + +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$objectManager = Bootstrap::getObjectManager(); +$quote = $objectManager->create(Quote::class); +$quote->load('test_guest_order_with_gift_message', 'reserved_order_id'); +$message = $objectManager->create(Message::class); +$product = $objectManager->create(Product::class); +foreach ($quote->getAllItems() as $item) { + $message->load($item->getGiftMessageId()); + $message->delete(); + $sku = $item->getSku(); + $product->load($product->getIdBySku($sku)); + if ($product->getId()) { + $product->delete(); + } +} +$quote->delete(); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/address_data.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/address_data.php new file mode 100644 index 0000000000000..394b13078010a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/address_data.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +return [ + 'region' => 'CA', + 'region_id' => '12', + 'postcode' => '11111', + 'lastname' => 'lastname', + 'firstname' => 'firstname', + 'street' => 'street', + 'city' => 'Los Angeles', + 'email' => 'admin@example.com', + 'telephone' => '11111111', + 'country_id' => 'US' +]; diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments.php new file mode 100644 index 0000000000000..def622b8f5025 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Shipment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Sales\Model\Order\ShipmentFactory; +use Magento\Framework\DB\Transaction; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/customer_order_with_two_items.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Transaction $transaction */ +$transaction = $objectManager->create(Transaction::class); + +/** @var Order $order */ +$order = $objectManager->create(Order::class)->loadByIncrementId('100000555'); + +$items = []; +$shipmentIds = ['0000000098', '0000000099']; +$i = 0; +foreach ($order->getItems() as $orderItem) { + $items[$orderItem->getId()] = $orderItem->getQtyOrdered(); + /** @var Shipment $shipment */ + $shipment = $objectManager->get(ShipmentFactory::class)->create($order, $items); + $shipment->setIncrementId($shipmentIds[$i]); + $shipment->register(); + + $transaction->addObject($shipment)->addObject($order)->save(); + $i++; +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments_rollback.php new file mode 100644 index 0000000000000..5fc01f2ecc073 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments_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/Sales/_files/customer_order_with_two_items_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment.php new file mode 100644 index 0000000000000..22eac03f9a6a8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Sales\Api\ShipmentCommentRepositoryInterface; +use Magento\Sales\Model\Order\Shipment\Comment; +use Magento\Sales\Model\Order\ShipmentFactory; +use Magento\Sales\Model\Order\Shipment\Track; +use Magento\Framework\DB\Transaction; +use Magento\Sales\Api\ShipmentTrackRepositoryInterface; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/customer_order_with_two_items.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Transaction $transaction */ +$transaction = $objectManager->create(Transaction::class); + +/** @var Order $order */ +$order = $objectManager->create(Order::class)->loadByIncrementId('100000555'); + +$items = []; +foreach ($order->getItems() as $orderItem) { + $items[$orderItem->getId()] = $orderItem->getQtyOrdered(); +} +$shipment = $objectManager->get(ShipmentFactory::class)->create($order, $items); +$shipment->register(); + +$transaction->addObject($shipment)->addObject($order)->save(); + +//Add shipment comments +$shipmentCommentRepository = $objectManager->get(ShipmentCommentRepositoryInterface::class); +$comments = [ + [ + 'comment' => 'This comment is visible to the customer', + 'is_visible_on_front' => 1, + 'is_customer_notified' => 1, + ], + [ + 'comment' => 'This comment should not be visible to the customer', + 'is_visible_on_front' => 0, + 'is_customer_notified' => 0, + ], +]; + +foreach ($comments as $commentData) { + /** @var Comment $comment */ + $comment = $objectManager->create(Comment::class); + $comment->setParentId($shipment->getId()); + $comment->setComment($commentData['comment']); + $comment->setIsVisibleOnFront($commentData['is_visible_on_front']); + $comment->setIsCustomerNotified($commentData['is_customer_notified']); + $shipmentCommentRepository->save($comment); +} + +//Add tracking +/** @var ShipmentTrackRepositoryInterface $shipmentTrackRepository */ +$shipmentTrackRepository = $objectManager->get(ShipmentTrackRepositoryInterface::class); +/** @var Track $track */ +$track = $objectManager->create(Track::class); +$track->setOrderId($order->getId()); +$track->setParentId($shipment->getId()); +$track->setTitle('United Parcel Service'); +$track->setCarrierCode('ups'); +$track->setTrackNumber('1234567890'); +$shipmentTrackRepository->save($track); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment_rollback.php new file mode 100644 index 0000000000000..5fc01f2ecc073 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment_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/Sales/_files/customer_order_with_two_items_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping.php new file mode 100644 index 0000000000000..848ee1ff0174b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Framework\DB\Transaction; +use Magento\Sales\Model\Order\ShipmentFactory; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_with_customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Transaction $transaction */ +$transaction = $objectManager->get(Transaction::class); +/** @var Order $order */ +$order = $objectManager->create(Order::class)->loadByIncrementId('100000001'); +//Set the shipping method +$order->setShippingDescription('UPS Next Day Air'); +$order->setShippingMethod('ups_01'); +$order->save(); + +//Create Shipment with UPS tracking and some items +$shipmentItems = []; +foreach ($order->getItems() as $orderItem) { + $shipmentItems[$orderItem->getId()] = $orderItem->getQtyOrdered(); +} +$tracking = [ + 'carrier_code' => 'ups', + 'title' => 'United Parcel Service', + 'number' => '987654321' +]; + +$shipment = $objectManager->get(ShipmentFactory::class)->create($order, $shipmentItems, [$tracking]); +$shipment->register(); +$transaction->addObject($shipment)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping_rollback.php new file mode 100644 index 0000000000000..bbb90e0326aec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping_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/Sales/_files/order_with_customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals.php new file mode 100644 index 0000000000000..f8dd55c6fdbeb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order\Payment; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$addressData = include __DIR__ . '/address_data.php'; + +$objectManager = Bootstrap::getObjectManager(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Magento\Catalog\Model\Product $product */ +$product = $productRepository->get('simple'); + +$billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +/** @var Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setBaseRowTotal($product->getPrice()); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(110) + ->setShippingAmount(10.0) + ->setBaseShippingAmount(10.0) + ->setTaxAmount(5.0) + ->setGrandTotal(100) + ->setBaseSubtotal(100) + ->setBaseGrandTotal(100) + ->setOrderCurrencyCode("USD") + ->setBaseCurrencyCode('USD') + ->setCustomerIsGuest(false) + ->setCustomerId(1) + ->setCustomerEmail('customer@example.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals_rollback.php new file mode 100644 index 0000000000000..113f84dae385e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals_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/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer.php new file mode 100644 index 0000000000000..3caa1410c65e9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Model\Order; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order.php'); +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); +/** @var Order $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001'); +$payment = $order->getPayment(); +$orderItems = $order->getItems(); +$orderItem = reset($orderItems); +$addressData = include __DIR__ . '/address_data.php'; +$orders = [ + [ + 'increment_id' => '100000002', + 'state' => \Magento\Sales\Model\Order::STATE_NEW, + 'status' => 'processing', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 120.00, + 'subtotal' => 120.00, + 'base_grand_total' => 120.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000003', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'processing', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 130.00, + 'base_grand_total' => 130.00, + 'subtotal' => 130.00, + 'total_paid' => 130.00, + 'store_id' => 0, + 'website_id' => 0, + ], + [ + 'increment_id' => '100000004', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'closed', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 140.00, + 'base_grand_total' => 140.00, + 'subtotal' => 140.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000005', + 'state' => \Magento\Sales\Model\Order::STATE_COMPLETE, + 'status' => 'complete', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 150.00, + 'base_grand_total' => 150.00, + 'subtotal' => 150.00, + 'total_paid' => 150.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000006', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'Processing', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 160.00, + 'base_grand_total' => 160.00, + 'subtotal' => 160.00, + 'total_paid' => 160.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000007', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'Processing', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 180.00, + 'base_grand_total' => 180.00, + 'subtotal' => 170.00, + 'tax_amount' => 5.00, + 'shipping_amount'=> 5.00, + 'base_shipping_amount'=> 4.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000008', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'Processing', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 190.00, + 'base_grand_total' => 190.00, + 'subtotal' => 180.00, + 'tax_amount' => 5.00, + 'shipping_amount'=> 5.00, + 'base_shipping_amount'=> 4.00, + 'store_id' => 1, + 'website_id' => 1, + ] +]; + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +/** @var array $orderData */ +foreach ($orders as $orderData) { + $newPayment = clone $payment; + $newPayment->setId(null); + /** @var $order \Magento\Sales\Model\Order */ + $order = $objectManager->create( + \Magento\Sales\Model\Order::class + ); + + // Reset addresses + /** @var Order\Address $billingAddress */ + $billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); + $billingAddress->setAddressType('billing'); + + $shippingAddress = clone $billingAddress; + $shippingAddress->setId(null)->setAddressType('shipping'); + + /** @var Order\Item $orderItem */ + $orderItem = $objectManager->create(Order\Item::class); + $orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + + $order->setData($orderData) + ->addItem($orderItem) + ->setCustomerIsGuest(false) + ->setCustomerId(1) + ->setCustomerEmail('customer@example.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setPayment($newPayment); + + $orderRepository->save($order); +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer_rollback.php new file mode 100644 index 0000000000000..dc455c3cb2c49 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer_rollback.php @@ -0,0 +1,10 @@ +<?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/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php new file mode 100644 index 0000000000000..c10ca26e640f1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order\Payment; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store.php'); + +/** @var \Magento\Catalog\Model\Product $product */ + +$addressData = include __DIR__ . '/address_data.php'; + +$objectManager = Bootstrap::getObjectManager(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Magento\Catalog\Model\Product $product */ +$product = $productRepository->get('simple'); + +$secondStore = Bootstrap::getObjectManager() + ->create(Store::class); + +$billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); +$customerIdFromFixture = 1; +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +/** @var Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(110) + ->setOrderCurrencyCode("USD") + ->setShippingAmount(10.0) + ->setBaseShippingAmount(10.0) + ->setTaxAmount(5.0) + ->setGrandTotal(100) + ->setBaseSubtotal(10) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(false) + ->setCustomerId($customerIdFromFixture) + ->setCustomerEmail('customer@null.com') + ->setOrderCurrencyCode('USD') + ->setBaseCurrencyCode('USD') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); + +/** @var Payment $payment */ +$secondPayment = $objectManager->create(Payment::class); +$secondPayment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$secondOrderItem = $objectManager->create(OrderItem::class); +$secondOrderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + +/** @var Order $order */ +$secondOrder = $objectManager->create(Order::class); +$secondOrder->setIncrementId('100000002') + ->setState(Order::STATE_PROCESSING) + ->setStatus($secondOrder->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(110) + ->setOrderCurrencyCode("USD") + ->setShippingAmount(10.0) + ->setBaseShippingAmount(10.0) + ->setTaxAmount(5.0) + ->setGrandTotal(100) + ->setBaseSubtotal(110) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(false) + ->setCustomerId($customerIdFromFixture) + ->setCustomerEmail('customer@null.com') + ->setOrderCurrencyCode('USD') + ->setBaseCurrencyCode('USD') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($secondStore->load('fixture_second_store')->getId()) + ->addItem($secondOrderItem) + ->setPayment($secondPayment); +$orderRepository->save($secondOrder); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews_rollback.php new file mode 100644 index 0000000000000..fe98d8659d3c0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews_rollback.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php new file mode 100644 index 0000000000000..fbd710fc07c0c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +//configuration setting for shipping tax class and shipping tax calculation and display +$configWriter->save('tax/classes/shipping_tax_class', '2'); +$configWriter->save('tax/calculation/shipping_includes_tax', '1'); +$configWriter->save('tax/sales_display/shipping', '3'); +$configWriter->save('tax/display/shipping', '3'); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings_rollback.php new file mode 100644 index 0000000000000..21b0a4317fc78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings_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\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +//Apply discount on prices to include tax +$configWriter->save('tax/classes/shipping_tax_class', '0'); +$configWriter->save('tax/calculation/shipping_includes_tax', '0'); +$configWriter->save('tax/sales_display/shipping', '1'); +$configWriter->save('tax/display/shipping', '1'); +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php new file mode 100644 index 0000000000000..9e1ce11a01b0e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +//configuration setting for shipping tax class and shipping tax calculation and display +$configWriter->save('tax/classes/shipping_tax_class', '2'); +$configWriter->save('tax/calculation/shipping_includes_tax', '0'); +$configWriter->save('tax/sales_display/shipping', '3'); +$configWriter->save('tax/display/shipping', '3'); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings_rollback.php new file mode 100644 index 0000000000000..21b0a4317fc78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings_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\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +//Apply discount on prices to include tax +$configWriter->save('tax/classes/shipping_tax_class', '0'); +$configWriter->save('tax/calculation/shipping_includes_tax', '0'); +$configWriter->save('tax/sales_display/shipping', '1'); +$configWriter->save('tax/display/shipping', '1'); +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php new file mode 100644 index 0000000000000..2603e2056f19d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\Data\TaxRateInterface; +use Magento\Tax\Api\Data\TaxRuleInterface; +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Api\DataObjectHelper; + +$objectManager = Bootstrap::getObjectManager(); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var Rate $rate */ +$rate = $rateFactory->create(); +$rateData = [ + Rate::KEY_COUNTRY_ID => 'US', + Rate::KEY_REGION_ID => '1', + Rate::KEY_POSTCODE => '*', + Rate::KEY_CODE => 'US-AL-*-Rate-1', + Rate::KEY_PERCENTAGE_RATE => '5.5', +]; +$dataObjectHelper->populateWithArray($rate, $rateData, TaxRateInterface::class); +$rateRepository->save($rate); + +$rule = $ruleFactory->create(); +$ruleData = [ + Rule::KEY_CODE=> 'GraphQl Test Rule AL', + Rule::KEY_PRIORITY => '0', + Rule::KEY_POSITION => '0', + Rule::KEY_CUSTOMER_TAX_CLASS_IDS => [3], + Rule::KEY_PRODUCT_TAX_CLASS_IDS => [2], + Rule::KEY_TAX_RATE_IDS => [$rate->getId()], +]; +$dataObjectHelper->populateWithArray($rule, $ruleData, TaxRuleInterface::class); +$ruleRepository->save($rule); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php new file mode 100644 index 0000000000000..22372f3a21022 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Tax\Model\ResourceModel\Calculation\Rate as RateResource; +use Magento\Tax\Model\ResourceModel\Calculation\Rule as RuleResource; + +$objectManager = Bootstrap::getObjectManager(); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var RateResource $rateResource */ +$rateResource = $objectManager->get(RateResource::class); +/** @var RuleResource $ruleResource */ +$ruleResource = $objectManager->get(RuleResource::class); + +$rate = $rateFactory->create(); +$rateResource->load($rate, 'US-AL-*-Rate-1', Rate::KEY_CODE); +$rule = $ruleFactory->create(); +$ruleResource->load($rule, 'GraphQl Test Rule AL', Rule::KEY_CODE); +$ruleRepository->delete($rule); +$rateRepository->delete($rate); diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_in_multiple_websites.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_in_multiple_websites.php new file mode 100644 index 0000000000000..bdfdb5943d4ba --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_in_multiple_websites.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture( + 'Magento/Catalog/_files/product_associated.php' +); +Resolver::getInstance()->requireDataFixture( + 'Magento/Catalog/_files/product_virtual_in_stock.php' +); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + +/** @var WebsiteRepositoryInterface $repository */ +$repository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $repository->get('test')->getId(); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId( + \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE +)->setAttributeSetId( + 4 +)->setWebsiteIds( + [$websiteId] +)->setName( + 'Grouped Product' +)->setSku( + 'grouped-product' +)->setPrice( + 100 +)->setTaxClassId( + 0 +)->setVisibility( + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH +)->setStatus( + \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED +); + +$newLinks = []; +$productLinkFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); + +/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ +$productLink = $productLinkFactory->create(); +$linkedProduct = $productRepository->getById(1); +$linkedProduct->setWebsiteIds([1, $websiteId])->save(); +$productLink->setSku($product->getSku()) + ->setLinkType('associated') + ->setLinkedProductSku($linkedProduct->getSku()) + ->setLinkedProductType($linkedProduct->getTypeId()) + ->setPosition(1) + ->getExtensionAttributes() + ->setQty(1); +$newLinks[] = $productLink; + +/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ +$productLink = $productLinkFactory->create(); +$linkedProduct = $productRepository->getById(21); +$linkedProduct->setWebsiteIds([1, $websiteId])->save(); +$productLink->setSku($product->getSku()) + ->setLinkType('associated') + ->setLinkedProductSku($linkedProduct->getSku()) + ->setLinkedProductType($linkedProduct->getTypeId()) + ->setPosition(2) + ->getExtensionAttributes() + ->setQty(2); +$newLinks[] = $productLink; +$product->setProductLinks($newLinks); +$product->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->save($product); + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_in_multiple_websites_rollback.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_in_multiple_websites_rollback.php new file mode 100644 index 0000000000000..23d82d3de4d0e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_in_multiple_websites_rollback.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\Framework\Exception\NoSuchEntityException; + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +/** + * @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Catalog\Api\ProductRepositoryInterface::class +); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +try { + $productRepository->deleteById('simple'); +} catch (NoSuchEntityException $e) { + //already deleted +} + +try { + $productRepository->deleteById('virtual-product'); +} catch (NoSuchEntityException $e) { + //already deleted +} + +try { + /** @var $groupedProduct \Magento\Catalog\Model\Product */ + $productRepository->deleteById('grouped-product'); +} catch (NoSuchEntityException $e) { + //already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_items_in_multiple_websites.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_items_in_multiple_websites.php new file mode 100644 index 0000000000000..f047bdca83905 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_items_in_multiple_websites.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture( + 'Magento/Catalog/_files/product_associated.php' +); +Resolver::getInstance()->requireDataFixture( + 'Magento/Catalog/_files/product_virtual_in_stock.php' +); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + +/** @var WebsiteRepositoryInterface $repository */ +$repository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $repository->get('test')->getId(); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId( + \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE +)->setAttributeSetId( + 4 +)->setWebsiteIds( + [1, $websiteId] +)->setName( + 'Grouped Product' +)->setSku( + 'grouped-product' +)->setPrice( + 100 +)->setTaxClassId( + 0 +)->setVisibility( + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH +)->setStatus( + \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED +); + +$newLinks = []; +$productLinkFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); + +/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ +$productLink = $productLinkFactory->create(); +$linkedProduct = $productRepository->getById(1); +$linkedProduct->setWebsiteIds([1])->save(); + +$productLink->setSku($product->getSku()) + ->setLinkType('associated') + ->setLinkedProductSku($linkedProduct->getSku()) + ->setLinkedProductType($linkedProduct->getTypeId()) + ->setPosition(1) + ->getExtensionAttributes() + ->setQty(1); +$newLinks[] = $productLink; + +/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ +$productLink = $productLinkFactory->create(); +$linkedProduct = $productRepository->getById(21); +$linkedProduct->setWebsiteIds([$websiteId])->save(); +$productLink->setSku($product->getSku()) + ->setLinkType('associated') + ->setLinkedProductSku($linkedProduct->getSku()) + ->setLinkedProductType($linkedProduct->getTypeId()) + ->setPosition(2) + ->getExtensionAttributes() + ->setQty(2); +$newLinks[] = $productLink; +$product->setProductLinks($newLinks); +$product->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->save($product); + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_items_in_multiple_websites_rollback.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_items_in_multiple_websites_rollback.php new file mode 100644 index 0000000000000..37a49b9c08579 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_items_in_multiple_websites_rollback.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\Framework\Exception\NoSuchEntityException; + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +/** + * @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Catalog\Api\ProductRepositoryInterface::class +); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +try { + $productRepository->deleteById('simple'); +} catch (NoSuchEntityException $e) { + //already deleted +} + +try { + $productRepository->deleteById('virtual-product'); +} catch (NoSuchEntityException $e) { + //already deleted +} + +try { + $productRepository->deleteById('grouped-product'); +} catch (NoSuchEntityException $e) { + //already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DeleteTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DeleteTest.php index 2a460a8ce622a..53e90ebf76f66 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DeleteTest.php @@ -16,13 +16,15 @@ /** * Test for \Magento\ImportExport\Controller\Adminhtml\Export\File\Delete class. + * + * @magentoAppArea adminhtml */ class DeleteTest extends AbstractBackendController { /** * @var WriteInterface */ - private $varDirectory; + protected $varDirectory; /** * @var string @@ -83,7 +85,7 @@ public function testExecute($file): void * @param $destinationFilePath * @return void */ - private function copyFile($destinationFilePath): void + protected function copyFile($destinationFilePath): void { //Refers to application root directory $rootDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::ROOT); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php index 9d83b3d2ece98..1bd41b047163a 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php @@ -9,6 +9,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\ImportExport\Model\Import; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -28,43 +29,53 @@ class CsvTest extends TestCase */ private $objectManager; - /** - * @var Csv - */ - private $csv; - /** * @inheritdoc */ protected function setUp(): void { - parent::setUp(); - $this->objectManager = Bootstrap::getObjectManager(); - $this->csv = $this->objectManager->create( - Csv::class, - ['destination' => $this->destination] - ); } /** * Test to destruct export adapter + * + * @dataProvider destructDataProvider + * + * @param string $destination + * @param bool $shouldBeDeleted + * @return void */ - public function testDestruct(): void + public function testDestruct(string $destination, bool $shouldBeDeleted): void { + $csv = $this->objectManager->create(Csv::class, ['destination' => $destination]); /** @var Filesystem $fileSystem */ $fileSystem = $this->objectManager->get(Filesystem::class); $directoryHandle = $fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); /** Assert that the destination file is present after construct */ $this->assertFileExists( - $directoryHandle->getAbsolutePath($this->destination), + $directoryHandle->getAbsolutePath($destination), 'The destination file was\'t created after construct' ); - /** Assert that the destination file was removed after destruct */ - $this->csv = null; - $this->assertFileNotExists( - $directoryHandle->getAbsolutePath($this->destination), - 'The destination file was\'t removed after destruct' - ); + unset($csv); + + if ($shouldBeDeleted) { + $this->assertFileDoesNotExist($directoryHandle->getAbsolutePath($destination)); + } else { + $this->assertFileExists($directoryHandle->getAbsolutePath($destination)); + } + } + + /** + * DataProvider for testDestruct + * + * @return array + */ + public function destructDataProvider(): array + { + return [ + 'temporary file' => [$this->destination, true], + 'import history file' => [Import::IMPORT_HISTORY_DIR . $this->destination, false], + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php new file mode 100644 index 0000000000000..218221c35632c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Report; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + */ +class CsvTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Filesystem\Directory\WriteInterface + */ + private $directory; + + /** + * @var Csv + */ + private $csvReport; + + /** + * @var string|null + */ + private $importFilePath; + + /** + * @var string|null + */ + private $reportPath; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $filesystem = Bootstrap::getObjectManager()->create(Filesystem::class); + $this->directory = $filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + + $this->csvReport = Bootstrap::getObjectManager()->create(Csv::class); + } + /** + * @inheritDoc + */ + protected function tearDown(): void + { + foreach ([$this->importFilePath, $this->reportPath] as $path) { + if ($path && $this->directory->isExist($path)) { + $this->directory->delete($path); + } + } + } + + /** + * @return void + */ + public function testCreateReport() + { + $importData = <<<fileContent +sku,store_view_code,name,price,product_type,attribute_set_code,weight +simple1,,"simple 1",10,simple,Default,-5 +fileContent; + $this->importFilePath = 'test_import.csv'; + $this->directory->writeFile($this->importFilePath, $importData); + + $errorAggregator = Bootstrap::getObjectManager()->create(ProcessingErrorAggregatorInterface::class); + $error = 'Value for \'weight\' attribute contains incorrect value'; + $errorAggregator->addError($error, ProcessingError::ERROR_LEVEL_CRITICAL, 1, 'weight', $error); + + $outputFileName = $this->csvReport->createReport( + $this->directory->getAbsolutePath($this->importFilePath), + $errorAggregator + ); + + $this->reportPath = Import::IMPORT_HISTORY_DIR . $outputFileName; + $this->assertTrue($this->directory->isExist($this->reportPath), 'Report was not generated'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/DeleteTest.php index 3531937b881e2..31ddbb5c0b46f 100644 --- a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/DeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/DeleteTest.php @@ -37,10 +37,10 @@ public function testRender() { $integration = $this->getFixtureIntegration(); $buttonHtml = $this->deleteButtonBlock->render($integration); - $this->assertStringContainsString('title="Remove"', $buttonHtml); - $this->assertStringContainsString( - 'onclick="this.setAttribute('data-url', ' - . ''http://localhost/index.php/backend/admin/integration/delete/id/' + self::assertStringContainsString('title="Remove"', $buttonHtml); + self::assertStringContainsString( + 'this.setAttribute(\'data-url\', ' + . '\'http://localhost/index.php/backend/admin/integration/delete/id/' . $integration->getId(), $buttonHtml ); @@ -52,14 +52,18 @@ public function testRenderDisabled() $integration = $this->getFixtureIntegration(); $integration->setSetupType(Integration::TYPE_CONFIG); $buttonHtml = $this->deleteButtonBlock->render($integration); - $this->assertStringContainsString('title="Uninstall the extension to remove this integration"', $buttonHtml); - $this->assertStringContainsString( - 'onclick="this.setAttribute('data-url', ' - . ''http://localhost/index.php/backend/admin/integration/delete/id/' + self::assertStringContainsString( + 'title="' .$this->deleteButtonBlock->escapeHtmlAttr('Uninstall the extension to remove this integration') + .'"', + $buttonHtml + ); + self::assertStringContainsString( + 'this.setAttribute(\'data-url\', ' + . '\'http://localhost/index.php/backend/admin/integration/delete/id/' . $integration->getId(), $buttonHtml ); - $this->assertStringContainsString('disabled="disabled"', $buttonHtml); + self::assertStringContainsString('disabled="disabled"', $buttonHtml); } /** diff --git a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/EditTest.php b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/EditTest.php index 6b1322e58f130..522af5e08f1de 100644 --- a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/EditTest.php +++ b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/EditTest.php @@ -38,9 +38,9 @@ public function testRenderEdit() $integration = $this->getFixtureIntegration(); $buttonHtml = $this->editButtonBlock->render($integration); $this->assertStringContainsString('title="Edit"', $buttonHtml); - $this->assertStringContainsString('class="action edit"', $buttonHtml); + $this->assertStringContainsString('class="' .$this->editButtonBlock->escapeHtmlAttr('action edit') .'"', $buttonHtml); $this->assertStringContainsString( - 'onclick="window.location.href='http://localhost/index.php/backend/admin/integration/edit/id/' + 'window.location.href=\'http://localhost/index.php/backend/admin/integration/edit/id/' . $integration->getId(), $buttonHtml ); @@ -52,7 +52,7 @@ public function testRenderView() $integration->setSetupType(Integration::TYPE_CONFIG); $buttonHtml = $this->editButtonBlock->render($integration); $this->assertStringContainsString('title="View"', $buttonHtml); - $this->assertStringContainsString('class="action info"', $buttonHtml); + $this->assertStringContainsString('class="' .$this->editButtonBlock->escapeHtmlAttr('action info') .'"', $buttonHtml); } /** diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php index dd4fdde250c03..b6508e3b3dfda 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php @@ -53,7 +53,7 @@ public function testGetFilters(): void ['is_filterable' => '1'], [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 1], ], 'Category 1' ); diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php index 07882b68d62d5..e226881b9cfcc 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php @@ -76,7 +76,7 @@ public function getFiltersDataProvider(): array ], [ 'label' => '<span class="price">$60.00</span> and above', - 'value' => '60-', + 'value' => '60-70', 'count' => 1, ], ], @@ -94,7 +94,7 @@ public function getFiltersDataProvider(): array ], [ 'label' => '<span class="price">$50.00</span> and above', - 'value' => '50-', + 'value' => '50-60', 'count' => 1, ], ], diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php index 3b2673b18635a..97928463620f4 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php @@ -71,15 +71,15 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], ['label' => '$20.00 - $29.99', 'value' => '20-30', 'count' => 1], - ['label' => '$50.00 and above', 'value' => '50-', 'count' => 1], + ['label' => '$50.00 and above', 'value' => '50-60', 'count' => 1], ], ], 'auto_calculation_variation_with_big_price_difference' => [ 'config' => ['catalog/layered_navigation/price_range_calculation' => 'auto'], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 300.00], 'expectation' => [ - ['label' => '$0.00 - $99.99', 'value' => '-100', 'count' => 2], - ['label' => '$300.00 and above', 'value' => '300-', 'count' => 1], + ['label' => '$0.00 - $99.99', 'value' => '0-100', 'count' => 2], + ['label' => '$300.00 and above', 'value' => '300-400', 'count' => 1], ], ], 'auto_calculation_variation_with_fixed_price_step' => [ @@ -88,7 +88,7 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$300.00 - $399.99', 'value' => '300-400', 'count' => 1], ['label' => '$400.00 - $499.99', 'value' => '400-500', 'count' => 1], - ['label' => '$500.00 and above', 'value' => '500-', 'count' => 1], + ['label' => '$500.00 and above', 'value' => '500-600', 'count' => 1], ], ], 'improved_calculation_variation_with_small_price_difference' => [ @@ -98,8 +98,8 @@ public function getFiltersDataProvider(): array ], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 50.00], 'expectation' => [ - ['label' => '$0.00 - $49.99', 'value' => '-50', 'count' => 2], - ['label' => '$50.00 and above', 'value' => '50-', 'count' => 1], + ['label' => '$0.00 - $19.99', 'value' => '0-20', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-50', 'count' => 2], ], ], 'improved_calculation_variation_with_big_price_difference' => [ @@ -109,8 +109,8 @@ public function getFiltersDataProvider(): array ], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 300.00], 'expectation' => [ - ['label' => '$0.00 - $299.99', 'value' => '-300', 'count' => 2.0], - ['label' => '$300.00 and above', 'value' => '300-', 'count' => 1.0], + ['label' => '$0.00 - $19.99', 'value' => '0-20', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-300', 'count' => 2], ], ], 'manual_calculation_with_price_step_200' => [ @@ -121,7 +121,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 300.00, 'simple1001' => 300.00, 'simple1002' => 500.00], 'expectation' => [ ['label' => '$200.00 - $399.99', 'value' => '200-400', 'count' => 2], - ['label' => '$400.00 and above', 'value' => '400-', 'count' => 1], + ['label' => '$400.00 and above', 'value' => '400-600', 'count' => 1], ], ], 'manual_calculation_with_price_step_10' => [ @@ -132,7 +132,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 300.00, 'simple1001' => 300.00, 'simple1002' => 500.00], 'expectation' => [ ['label' => '$300.00 - $309.99', 'value' => '300-310', 'count' => 2], - ['label' => '$500.00 and above', 'value' => '500-', 'count' => 1], + ['label' => '$500.00 and above', 'value' => '500-510', 'count' => 1], ], ], 'manual_calculation_with_number_of_intervals_10' => [ @@ -145,7 +145,7 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], ['label' => '$20.00 - $29.99', 'value' => '20-30', 'count' => 1], - ['label' => '$30.00 and above', 'value' => '30-', 'count' => 1], + ['label' => '$30.00 and above', 'value' => '30-40', 'count' => 1], ], ], 'manual_calculation_with_number_of_intervals_2' => [ @@ -157,7 +157,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 30.00], 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 2], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 2], ], ], ]; diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php index 435dd29e16dfa..760f4031b8844 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php @@ -32,7 +32,7 @@ public function testGetFilters(): void ['is_filterable_in_search' => 1], [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 1], ] ); } diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByContentFieldTest.php b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByContentFieldTest.php new file mode 100644 index 0000000000000..1b08c7b55769a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByContentFieldTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Model\ResourceModel; + +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for GetAssetIdsByContentFieldTest + */ +class GetAssetIdsByContentFieldTest extends TestCase +{ + private const STORE_FIELD = 'store_id'; + private const STATUS_FIELD = 'content_status'; + private const STATUS_ENABLED = '1'; + private const STATUS_DISABLED = '0'; + private const FIXTURE_ASSET_ID = 2020; + private const DEFAULT_STORE_ID = '1'; + + /** + * @var GetAssetIdsByContentFieldInterface + */ + private $getAssetIdsByContentField; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getAssetIdsByContentField = $objectManager->get(GetAssetIdsByContentFieldInterface::class); + } + + /** + * Test for getting asset id by category fields + * + * @dataProvider dataProvider + * @magentoConfigFixture system/media_gallery/enabled 1 + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * + * @param string $field + * @param string $value + * @param array $expectedAssetIds + * @throws InvalidArgumentException + */ + public function testCategoryFields(string $field, string $value, array $expectedAssetIds): void + { + $this->assertEquals( + $expectedAssetIds, + $this->getAssetIdsByContentField->execute($field, $value) + ); + } + + /** + * Test for getting asset id by product fields + * + * @dataProvider dataProvider + * @magentoConfigFixture system/media_gallery/enabled 1 + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @param string $field + * @param string $value + * @param array $expectedAssetIds + * @throws InvalidArgumentException + */ + public function testProductFields(string $field, string $value, array $expectedAssetIds): void + { + $this->assertEquals( + $expectedAssetIds, + $this->getAssetIdsByContentField->execute($field, $value) + ); + } + + /** + * Test for getting asset when media gallery disabled + * + * @magentoConfigFixture system/media_gallery/enabled 0 + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @throws InvalidArgumentException + */ + public function testProductFieldsWithDisabledMediaGallery(): void + { + $this->assertEquals( + [], + $this->getAssetIdsByContentField->execute(self::STATUS_FIELD, self::STATUS_ENABLED) + ); + } + + /** + * Data provider for tests + * + * @return array + */ + public static function dataProvider(): array + { + return [ + [self::STATUS_FIELD, self::STATUS_ENABLED, [self::FIXTURE_ASSET_ID]], + [self::STATUS_FIELD, self::STATUS_DISABLED, []], + [self::STORE_FIELD, self::DEFAULT_STORE_ID, [self::FIXTURE_ASSET_ID]], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php index 2a1177661572a..2d54b5fa862ea 100644 --- a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php +++ b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; -use Magento\Catalog\Api\Data\ProductExtensionInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; @@ -14,8 +12,6 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; -Bootstrap::getInstance()->reinitialize(); - /** @var ObjectManager $objectManager */ $objectManager = Bootstrap::getObjectManager(); diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentFieldTest.php b/dev/tests/integration/testsuite/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentFieldTest.php new file mode 100644 index 0000000000000..c7fb0a38340c1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentFieldTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Model\ResourceModel; + +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for GetAssetIdsByContentFieldTest + */ +class GetAssetIdsByContentFieldTest extends TestCase +{ + private const STORE_FIELD = 'store_id'; + private const STATUS_FIELD = 'content_status'; + private const STATUS_ENABLED = '1'; + private const STATUS_DISABLED = '0'; + private const FIXTURE_ASSET_ID = 2020; + private const DEFAULT_STORE_ID = '1'; + private const ADMIN_STORE_ID = '0'; + + /** + * @var GetAssetIdsByContentFieldInterface + */ + private $getAssetIdsByContentField; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getAssetIdsByContentField = $objectManager->get(GetAssetIdsByContentFieldInterface::class); + } + + /** + * Test for getting asset id by block field + * + * @dataProvider blockDataProvider + * @magentoConfigFixture system/media_gallery/enabled 1 + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * + * @param string $field + * @param string $value + * @param array $expectedAssetIds + * @throws InvalidArgumentException + */ + public function testBlockFields(string $field, string $value, array $expectedAssetIds): void + { + $this->assertEquals( + $expectedAssetIds, + $this->getAssetIdsByContentField->execute($field, $value) + ); + } + + /** + * Test for getting asset id by page field + * + * @dataProvider pageDataProvider + * @magentoConfigFixture system/media_gallery/enabled 1 + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * + * @param string $field + * @param string $value + * @param array $expectedAssetIds + * @throws InvalidArgumentException + */ + public function testPageFields(string $field, string $value, array $expectedAssetIds): void + { + $this->assertEquals( + $expectedAssetIds, + $this->getAssetIdsByContentField->execute($field, $value) + ); + } + + /** + * Data provider for block tests + * + * @return array + */ + public static function blockDataProvider(): array + { + return [ + [self::STATUS_FIELD, self::STATUS_ENABLED, [self::FIXTURE_ASSET_ID]], + [self::STATUS_FIELD, self::STATUS_DISABLED, []], + [self::STORE_FIELD, self::DEFAULT_STORE_ID, [self::FIXTURE_ASSET_ID]], + ]; + } + + /** + * Data provider for page tests + * + * @return array + */ + public static function pageDataProvider(): array + { + return [ + [self::STATUS_FIELD, self::STATUS_ENABLED, [self::FIXTURE_ASSET_ID]], + [self::STATUS_FIELD, self::STATUS_DISABLED, []], + [self::STORE_FIELD, self::ADMIN_STORE_ID, [self::FIXTURE_ASSET_ID]], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsBlacklistedTest.php b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsExcludedTest.php similarity index 62% rename from dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsBlacklistedTest.php rename to dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsExcludedTest.php index f63674754ea3d..bd0df51162620 100644 --- a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsBlacklistedTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsExcludedTest.php @@ -7,18 +7,17 @@ namespace Magento\MediaGallery\Model; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** - * Test for IsPathBlacklistedInterface + * Test for IsPathExcludedInterface */ -class IsBlacklistedTest extends TestCase +class IsExcludedTest extends TestCase { - /** - * @var IsPathBlacklistedInterface + * @var IsPathExcludedInterface */ private $service; @@ -27,23 +26,23 @@ class IsBlacklistedTest extends TestCase */ protected function setUp(): void { - $this->service = Bootstrap::getObjectManager()->get(IsPathBlacklistedInterface::class); + $this->service = Bootstrap::getObjectManager()->get(IsPathExcludedInterface::class); } /** - * Testing the blacklisted paths + * Testing the excluded paths * * @param string $path - * @param bool $isBlacklisted + * @param bool $isExcluded * @dataProvider pathsProvider */ - public function testExecute(string $path, bool $isBlacklisted): void + public function testExecute(string $path, bool $isExcluded): void { - $this->assertEquals($isBlacklisted, $this->service->execute($path)); + $this->assertEquals($isExcluded, $this->service->execute($path)); } /** - * Provider of paths and if the path should be in the blacklist + * Provider of paths and if the path should be in the excluded list * * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/ResourceModel/AssetKeywordsTest.php b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/ResourceModel/AssetKeywordsTest.php index beb146b0b816f..def1eb3231be4 100644 --- a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/ResourceModel/AssetKeywordsTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/ResourceModel/AssetKeywordsTest.php @@ -8,9 +8,9 @@ namespace Magento\MediaGallery\Model\ResourceModel; use Behat\Gherkin\Keywords\KeywordsInterface; -use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; -use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterface; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; @@ -66,29 +66,44 @@ protected function setUp(): void * * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php * @dataProvider keywordsProvider - * @param array $keywords + * @param string[] $keywords + * @param string[] $updatedKeywords * @throws \Magento\Framework\Exception\LocalizedException */ - public function testSaveAndGetKeywords(array $keywords): void + public function testSaveAndGetKeywords(array $keywords, array $updatedKeywords): void { - $keywords = ['pear', 'plum']; - $loadedAssets = $this->getAssetsByPath->execute([self::FIXTURE_ASSET_PATH]); $this->assertCount(1, $loadedAssets); $loadedAsset = current($loadedAssets); + $this->updateAssetKeywords($loadedAsset->getId(), $keywords); + $this->updateAssetKeywords($loadedAsset->getId(), $updatedKeywords); + } + + /** + * Update Asset keywords + * + * @param int $assetId + * @param string[] $keywords + */ + private function updateAssetKeywords(int $assetId, array $keywords): void + { $assetKeywords = $this->assetsKeywordsFactory->create( [ - 'assetId' => $loadedAsset->getId(), + 'assetId' => $assetId, 'keywords' => $this->getKeywords($keywords) ] ); $this->saveAssetsKeywords->execute([$assetKeywords]); - $loadedAssetKeywords = $this->getAssetsKeywords->execute([$loadedAsset->getId()]); + $loadedAssetKeywords = $this->getAssetsKeywords->execute([$assetId]); - $this->assertCount(1, $loadedAssetKeywords); + if (empty($keywords)) { + $this->assertEmpty($loadedAssetKeywords); + return; + } + $this->assertCount(1, $loadedAssetKeywords); /** @var AssetKeywordsInterface $loadedAssetKeyword */ $loadedAssetKeyword = current($loadedAssetKeywords); @@ -115,10 +130,17 @@ public function testSaveAndGetKeywords(array $keywords): void public function keywordsProvider(): array { return [ - [['one-keyword']], - [['кириллица']], - [['plum', 'pear']], - [[]] + [['one-keyword'],['plum','orange']], + [['кириллица'],[]], + [[],['plum']], + [['plum', 'pear'],['plum','pear']], + [['plum', 'pear'],['plum','orange']], + [['plum', 'pear','grape'],['plum','orange']], + [['plum', 'pear','grape'],['mango']], + [['plum', 'pear','grape'],['orange']], + [['plum', 'pear','grape'],[]], + [['plum', 'pear'],['plum', 'pear','grape','mango','orange']], + [[],[]] ]; } diff --git a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/SearchAssetsTest.php b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/SearchAssetsTest.php new file mode 100644 index 0000000000000..924c7d81365a2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/SearchAssetsTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\MediaGalleryApi\Api\SearchAssetsInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Verify SearchAssets By searchCriteria + */ +class SearchAssetsTest extends TestCase +{ + private const FIXTURE_ASSET_PATH = 'testDirectory/path.jpg'; + + /** + * @var SearchAssetsInterfcae + */ + private $searchAssets; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->filterBuilder = Bootstrap::getObjectManager()->get(FilterBuilder::class); + $this->filterGroupBuilder = Bootstrap::getObjectManager()->get(FilterGroupBuilder::class); + $this->searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); + $this->searchAssets = Bootstrap::getObjectManager()->get(SearchAssetsInterface::class); + } + + /** + * Verify search asstes by searching with search criteria + * + * @dataProvider searchCriteriaProvider + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(array $searchCriteriaData): void + { + $titleFilter = $this->filterBuilder->setField($searchCriteriaData['field']) + ->setConditionType($searchCriteriaData['conditionType']) + ->setValue($searchCriteriaData['value']) + ->create(); + $searchCriteria = $this->searchCriteriaBuilder + ->setFilterGroups([$this->filterGroupBuilder->setFilters([$titleFilter])->create()]) + ->create(); + + $assets = $this->searchAssets->execute($searchCriteria); + + $this->assertCount(1, $assets); + $this->assertEquals($assets[0]->getPath(), self::FIXTURE_ASSET_PATH); + } + + /** + * Search criteria params provider + * + * @return array + */ + public function searchCriteriaProvider(): array + { + return [ + [ + ['field' => 'id', 'conditionType' => 'eq', 'value' => 2020], + ], + [ + ['field' => 'title', 'conditionType' => 'fulltext', 'value' => 'Img'], + ], + [ + ['field' => 'content_type', 'conditionType' => 'eq', 'value' => 'image'] + ], + [ + ['field' => 'description', 'conditionType' => 'fulltext', 'value' => 'description'] + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/MediaGallery/_files/media_asset.php b/dev/tests/integration/testsuite/Magento/MediaGallery/_files/media_asset.php index 1a2dce9e032fa..33efe102362ba 100644 --- a/dev/tests/integration/testsuite/Magento/MediaGallery/_files/media_asset.php +++ b/dev/tests/integration/testsuite/Magento/MediaGallery/_files/media_asset.php @@ -18,6 +18,7 @@ [ 'id' => 2020, 'path' => 'testDirectory/path.jpg', + 'description' => 'Description of an image', 'contentType' => 'image', 'title' => 'Img', 'source' => 'Local', 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/MemoryUsageTest.php b/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php index 1cefa80d8f611..cff3bae8bc2e1 100644 --- a/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php +++ b/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php @@ -32,7 +32,7 @@ protected function setUp(): void */ public function testAppReinitializationNoMemoryLeak() { - $this->markTestSkipped('Test fails at Travis. Skipped until MAGETWO-47111'); + $this->markTestSkipped('Skipped until MAGETWO-47111'); $this->_deallocateUnusedMemory(); $actualMemoryUsage = $this->_helper->getRealMemoryUsage(); diff --git a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php index 8fafec2ee091f..dca0ef14663f4 100644 --- a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php +++ b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php @@ -127,6 +127,7 @@ public function testSpecificConsumerAndRerun() $specificConsumer = 'exportProcessor'; $config = $this->config; $config['cron_consumers_runner'] = ['consumers' => [$specificConsumer], 'max_messages' => 0]; + $config['queue'] = ['only_spawn_when_message_available' => 0]; $this->writeConfig($config); $this->reRunConsumersAndCheckLocks($specificConsumer); $this->reRunConsumersAndCheckLocks($specificConsumer); diff --git a/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php b/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php index 31b0ac84266e7..3b70659f80a42 100644 --- a/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php +++ b/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php @@ -24,7 +24,7 @@ $objectManager = Bootstrap::getObjectManager(); /** @var ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(ProductRepositoryInterface::class); -$product = $productRepository->getById(10); +$product = $productRepository->get('simple_10'); $product->setStockData(['use_config_manage_stock' => 1, 'qty' => 4, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); $productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php index 8d6caad63ab77..74160f9460851 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php @@ -62,11 +62,19 @@ protected function tearDown(): void public function testSaveActionCreateNewTemplateAndVerifySuccessMessage() { $this->getRequest()->setParam('id', $this->model->getId()); + $this->getRequest()->setParam('is_legacy', 1); + $this->dispatch('backend/newsletter/template/save'); + /** * Check that errors was generated and set to session */ $this->assertSessionMessages($this->isEmpty(), \Magento\Framework\Message\MessageInterface::TYPE_ERROR); + + $this->model->load($this->getRequest()->getPostValue('code'), 'template_code'); + + $this->assertEquals(0, $this->model->getIsLegacy()); + /** * Check that success message is set */ @@ -90,6 +98,8 @@ public function testSaveActionEditTemplateAndVerifySuccessMessage() $this->assertEquals('some_unique_code', $this->model->getTemplateCode()); $this->getRequest()->setParam('id', $this->model->getId()); + $this->getRequest()->setParam('is_legacy', 1); + $this->dispatch('backend/newsletter/template/save'); /** @@ -97,6 +107,10 @@ public function testSaveActionEditTemplateAndVerifySuccessMessage() */ $this->assertSessionMessages($this->isEmpty(), \Magento\Framework\Message\MessageInterface::TYPE_ERROR); + $this->model->load($this->getRequest()->getPostValue('code'), 'template_code'); + + $this->assertEquals(0, $this->model->getIsLegacy()); + /** * Check that success message is set */ 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/Newsletter/_files/guest_subscriber.php b/dev/tests/integration/testsuite/Magento/Newsletter/_files/guest_subscriber.php new file mode 100644 index 0000000000000..e10dcd5985a2e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Newsletter/_files/guest_subscriber.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Newsletter\Model\Subscriber; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +$storeId = $objectManager->get(StoreManagerInterface::class) + ->getStore() + ->getId(); + +/** @var Subscriber $subscriber */ +$subscriber = $objectManager->create(Subscriber::class); + +$subscriber->setStoreId($storeId) + ->setSubscriberEmail('guest@example.com') + ->setSubscriberStatus(Subscriber::STATUS_SUBSCRIBED) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/_files/guest_subscriber_rollback.php b/dev/tests/integration/testsuite/Magento/Newsletter/_files/guest_subscriber_rollback.php new file mode 100644 index 0000000000000..225f6515b5ce7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Newsletter/_files/guest_subscriber_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\Newsletter\Model\Subscriber; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$storeId = $objectManager->get(StoreManagerInterface::class) + ->getStore() + ->getId(); + +/** @var Subscriber $subscriber */ +$subscriber = $objectManager->get(Subscriber::class); +$subscriber->loadBySubscriberEmail('guest@example.com', (int)$storeId); +if ($subscriber->getId()) { + $subscriber->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Payment/Block/Transparent/IframeTest.php b/dev/tests/integration/testsuite/Magento/Payment/Block/Transparent/IframeTest.php index f84b493c43f29..c08a31c909d8a 100644 --- a/dev/tests/integration/testsuite/Magento/Payment/Block/Transparent/IframeTest.php +++ b/dev/tests/integration/testsuite/Magento/Payment/Block/Transparent/IframeTest.php @@ -37,7 +37,7 @@ public function testToHtml($xssString) $content = $block->toHtml(); $this->assertStringNotContainsString($xssString, $content, 'Params must be escaped'); - $this->assertStringContainsString($block->escapeXssInUrl($xssString), $content, 'Content must be present'); + $this->assertStringContainsString($block->escapeJs($xssString), $content, 'Content must be present'); } /** diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/NvpTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/NvpTest.php index 9821a148589fd..05e572f5b64f0 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/NvpTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/NvpTest.php @@ -95,7 +95,7 @@ public function testRequestTotalsAndLineItemsWithFPT() . '&SHIPPINGAMT=0.00&ITEMAMT=112.70&TAXAMT=0.00' . '&L_NAME0=Simple+Product+FPT&L_QTY0=1&L_AMT0=100.00' . '&L_NAME1=FPT&L_QTY1=1&L_AMT1=12.70' - . '&METHOD=SetExpressCheckout&VERSION=72.0&BUTTONSOURCE=Magento_Cart_'; + . '&METHOD=SetExpressCheckout&VERSION=72.0&BUTTONSOURCE=Magento_2_'; $this->httpClient->method('write') ->with( @@ -146,7 +146,7 @@ public function testCallRefundTransaction() $httpQuery = 'TRANSACTIONID=fooTransactionId&REFUNDTYPE=Partial' .'&CURRENCYCODE=USD&AMT=145.98&METHOD=RefundTransaction' - .'&VERSION=72.0&BUTTONSOURCE=Magento_Cart_'; + .'&VERSION=72.0&BUTTONSOURCE=Magento_2_'; $this->httpClient->expects($this->once())->method('write') ->with( diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/PayflowNvpTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/PayflowNvpTest.php index 274475b35ba6d..c10785624fc59 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/PayflowNvpTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/PayflowNvpTest.php @@ -95,7 +95,7 @@ public function testRequestLineItems() . 'L_NAME1=Simple 2&L_QTY1=2&L_COST1=9.69&' . 'L_NAME2=Simple 3&L_QTY2=3&L_COST2=11.69&' . 'L_NAME3=Discount&L_QTY3=1&L_COST3=-10.00&' - . 'TRXTYPE=A&ACTION=S&BUTTONSOURCE=Magento_Cart_'; + . 'TRXTYPE=A&ACTION=S&BUTTONSOURCE=Magento_2_'; $this->httpClient->method('write') ->with( diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index c1ac7cf1ef723..13fcd53d78186 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -17,11 +17,13 @@ use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\ResourceModel\Quote\Collection; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CheckoutTest extends \PHPUnit\Framework\TestCase +class CheckoutTest extends TestCase { /** * @var ObjectManagerInterface @@ -29,22 +31,22 @@ class CheckoutTest extends \PHPUnit\Framework\TestCase private $objectManager; /** - * @var Info|\PHPUnit\Framework\MockObject\MockObject + * @var Info|MockObject */ private $paypalInfo; /** - * @var Config|\PHPUnit\Framework\MockObject\MockObject + * @var Config|MockObject */ private $paypalConfig; /** - * @var Factory|\PHPUnit\Framework\MockObject\MockObject + * @var Factory|MockObject */ private $apiTypeFactory; /** - * @var Nvp|\PHPUnit\Framework\MockObject\MockObject + * @var Nvp|MockObject */ private $api; @@ -215,6 +217,28 @@ public function testPlaceGuestQuote() $this->assertNotEmpty($order->getShippingAddress()); } + /** + * Place the order as guest when `Automatic Assignment to Customer Group` is enabled. + * + * @magentoDataFixture Magento/Paypal/_files/quote_express.php + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * + * @return void + */ + public function testPlaceGuestQuoteAutomaticAssignmentEnabled(): void + { + $quote = $this->getFixtureQuote(); + $quote->setCheckoutMethod(Onepage::METHOD_GUEST); + $quote->getShippingAddress()->setSameAsBilling(0); + $quote->setReservedOrderId(null); + + $checkout = $this->getCheckout($quote); + $checkout->place('token'); + + $order = $checkout->getOrder(); + $this->assertNotEmpty($order->getRealOrderId()); + } + /** * @param Quote $quote * @return Checkout @@ -721,11 +745,11 @@ private function getFixtureQuote(): Quote /** * Adds countryFactory to a mock. * - * @param \PHPUnit\Framework\MockObject\MockObject $api + * @param MockObject $api * @return void * @throws \ReflectionException */ - private function addCountryFactory(\PHPUnit\Framework\MockObject\MockObject $api): void + private function addCountryFactory(MockObject $api): void { $reflection = new \ReflectionClass($api); $property = $reflection->getProperty('_countryFactory'); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Payflow/TransparentTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Payflow/TransparentTest.php index eb0976a696300..3f24aaaa8686e 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Payflow/TransparentTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Payflow/TransparentTest.php @@ -47,14 +47,14 @@ protected function setUp(): void } /** - * Checks a case when order should be placed in "Suspected Fraud" status based on after account verification. + * Checks a case when order should be placed in "Suspected Fraud" status based on account verification. * * @magentoDataFixture Magento/Checkout/_files/quote_with_shipping_method.php * @magentoConfigFixture current_store payment/payflowpro/active 1 * @magentoConfigFixture current_store payment/payflowpro/payment_action Authorization * @magentoConfigFixture current_store payment/payflowpro/fmf 1 */ - public function testPlaceOrderSuspectedFraud() + public function testPlaceOrderSuspectedFraud(): void { $quote = $this->getQuote('test_order_1'); $this->addFraudPayment($quote); @@ -114,7 +114,7 @@ private function getQuote(string $reservedOrderId): CartInterface * * @return void */ - private function addFraudPayment(CartInterface $quote) + private function addFraudPayment(CartInterface $quote): void { $payment = $quote->getPayment(); $payment->setMethod(Config::METHOD_PAYFLOWPRO); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/PayflowproVoidTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/PayflowproVoidTest.php new file mode 100644 index 0000000000000..dc1c97e593fae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/PayflowproVoidTest.php @@ -0,0 +1,268 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Model; + +use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Api\ExtensionAttributesFactory; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Model\Context; +use Magento\Framework\Module\ModuleListInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Payment\Gateway\Command\CommandException; +use Magento\Payment\Helper\Data; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Model\Method\ConfigInterfaceFactory; +use Magento\Payment\Model\Method\Logger; +use Magento\Paypal\Model\Payflow\Service\Gateway; +use Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface; +use Magento\Quote\Api\Data\PaymentMethodInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Item; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PayflowproVoidTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Tests PayflowPro payment void operation. + * + * @magentoDataFixture Magento/Paypal/_files/order_payflowpro.php + * @magentoConfigFixture current_store payment/payflowpro/active 1 + */ + public function testPaymentVoid(): void + { + $response = new DataObject( + [ + 'result' => '0', + 'pnref' => 'V19A3D27B61E', + 'respmsg' => 'Approved', + 'authcode' => '510PNI', + 'hostcode' => 'A', + 'request_id' => 'f930d3dc6824c1f7230c5529dc37ae5e', + 'result_code' => '0', + ] + ); + + $order = $this->getOrder(); + $payment = $order->getPayment(); + $instance = $this->getPaymentMethodInstance($response); + $payment->setMethodInstance($instance); + + $this->assertTrue($order->canVoidPayment()); + + $payment->void(new DataObject()); + /** @var OrderRepositoryInterface $orderRepository */ + $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + $orderRepository->save($order); + + $order = $this->getOrderByIncrementId('100000001'); + $this->assertFalse($order->canVoidPayment()); + } + + /** + * Tests canceling order with acceptable void transaction results. + * + * @param DataObject $response + * @magentoDataFixture Magento/Paypal/_files/order_payflowpro.php + * @magentoConfigFixture current_store payment/payflowpro/active 1 + * @dataProvider orderCancelSuccessDataProvider + */ + public function testOrderCancelSuccess(DataObject $response): void + { + $order = $this->getOrder(); + $payment = $order->getPayment(); + $instance = $this->getPaymentMethodInstance($response); + $payment->setMethodInstance($instance); + $order->cancel(); + + $this->assertEquals(Order::STATE_CANCELED, $order->getState()); + $this->assertEquals(Order::STATE_CANCELED, $order->getStatus()); + } + + /** + * @return array + */ + public function orderCancelSuccessDataProvider(): array + { + return [ + 'Authorization has expired' => [ + new DataObject( + [ + 'respmsg' => 'Declined: 10601-Authorization has expired.', + 'result_code' => '10601', + ] + ) + ], + 'Authorization voided successfully' => [ + new DataObject( + [ + 'respmsg' => 'Approved', + 'result_code' => '0', + ] + ) + ] + ]; + } + + /** + * Tests canceling the order when got an error during transaction voiding. + * + * @magentoDataFixture Magento/Paypal/_files/order_payflowpro.php + * @magentoConfigFixture current_store payment/payflowpro/active 1 + */ + public function testOrderCancelWithVoidError(): void + { + $response = new DataObject( + [ + 'respmsg' => 'Declined: for some reason other then expired authorization', + 'result_code' => '111', + ] + ); + $order = $this->getOrder(); + $payment = $order->getPayment(); + $instance = $this->getPaymentMethodInstance($response); + $payment->setMethodInstance($instance); + + $this->expectException(CommandException::class); + $order->cancel(); + } + + /** + * Returns prepared order. + * + * @return Order + * @throws \ReflectionException + */ + private function getOrder(): Order + { + /** @var $order Order */ + $order = $this->getOrderByIncrementId('100000001'); + $orderItem = $this->createMock(Item::class); + $orderItem->method('getQtyToInvoice') + ->willReturn(true); + $order->setItems([$orderItem]); + + $payment = $order->getPayment(); + $canVoidLookupProperty = new \ReflectionProperty(get_class($payment), '_canVoidLookup'); + $canVoidLookupProperty->setAccessible(true); + $canVoidLookupProperty->setValue($payment, true); + + return $order; + } + + /** + * Returns payment method instance. + * + * @param DataObject $response + * @return PaymentMethodInterface + * @throws \ReflectionException + */ + private function getPaymentMethodInstance(DataObject $response): PaymentMethodInterface + { + $gatewayMock = $this->createMock(Gateway::class); + $gatewayMock->expects($this->once()) + ->method('postRequest') + ->willReturn($response); + + $configMock = $this->createMock(PayflowConfig::class); + $configFactoryMock = $this->createPartialMock( + ConfigInterfaceFactory::class, + ['create'] + ); + + $configFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($configMock); + + $configMock->expects($this->any()) + ->method('getValue') + ->willReturnMap( + [ + ['use_proxy', false], + ['sandbox_flag', '1'], + ['transaction_url_test_mode', 'https://test_transaction_url'] + ] + ); + + /** @var Payflowpro|MockObject $instance */ + $instance = $this->getMockBuilder(Payflowpro::class) + ->setMethods(['setStore', 'getInfoInstance']) + ->setConstructorArgs( + [ + $this->objectManager->get(Context::class), + $this->objectManager->get(Registry::class), + $this->objectManager->get(ExtensionAttributesFactory::class), + $this->objectManager->get(AttributeValueFactory::class), + $this->objectManager->get(Data::class), + $this->objectManager->get(ScopeConfigInterface::class), + $this->objectManager->get(Logger::class), + $this->objectManager->get(ModuleListInterface::class), + $this->objectManager->get(TimezoneInterface::class), + $this->objectManager->get(StoreManagerInterface::class), + $configFactoryMock, + $gatewayMock, + $this->objectManager->get(HandlerInterface::class), + null, + null, + [] + ] + ) + ->getMock(); + + $instance->expects($this->once()) + ->method('setStore') + ->willReturnSelf(); + $paymentInfoInstance = $this->createMock(InfoInterface::class); + $instance->method('getInfoInstance') + ->willReturn($paymentInfoInstance); + + return $instance; + } + + /** + * Get stored order. + * + * @param string $incrementId + * @return OrderInterface + */ + private function getOrderByIncrementId(string $incrementId): OrderInterface + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, $incrementId) + ->create(); + + $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + $orders = $orderRepository->getList($searchCriteria) + ->getItems(); + + /** @var OrderInterface $order */ + return array_pop($orders); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/VoidTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/VoidTest.php deleted file mode 100644 index 6f295a62f48fb..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/VoidTest.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Paypal\Model; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class VoidTest extends \PHPUnit\Framework\TestCase -{ - /** - * @magentoDataFixture Magento/Paypal/_files/order_payflowpro.php - * @magentoConfigFixture current_store payment/payflowpro/active 1 - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testPayflowProVoid() - { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - /** @var $order \Magento\Sales\Model\Order */ - $order = $objectManager->create(\Magento\Sales\Model\Order::class); - $order->loadByIncrementId('100000001'); - $payment = $order->getPayment(); - - $gatewayMock = $this->createMock(\Magento\Paypal\Model\Payflow\Service\Gateway::class); - - $configMock = $this->createMock(\Magento\Paypal\Model\PayflowConfig::class); - $configFactoryMock = $this->createPartialMock( - \Magento\Payment\Model\Method\ConfigInterfaceFactory::class, - ['create'] - ); - - $configFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($configMock); - - $configMock->expects($this->any()) - ->method('getValue') - ->willReturnMap( - [ - ['use_proxy', false], - ['sandbox_flag', '1'], - ['transaction_url_test_mode', 'https://test_transaction_url'] - ] - ); - - /** @var \Magento\Paypal\Model\Payflowpro|\PHPUnit\Framework\MockObject\MockObject $instance */ - $instance = $this->getMockBuilder(\Magento\Paypal\Model\Payflowpro::class) - ->setMethods(['setStore']) - ->setConstructorArgs( - [ - $objectManager->get(\Magento\Framework\Model\Context::class), - $objectManager->get(\Magento\Framework\Registry::class), - $objectManager->get(\Magento\Framework\Api\ExtensionAttributesFactory::class), - $objectManager->get(\Magento\Framework\Api\AttributeValueFactory::class), - $objectManager->get(\Magento\Payment\Helper\Data::class), - $objectManager->get(\Magento\Framework\App\Config\ScopeConfigInterface::class), - $objectManager->get(\Magento\Payment\Model\Method\Logger::class), - $objectManager->get(\Magento\Framework\Module\ModuleListInterface::class), - $objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class), - $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class), - $configFactoryMock, - $gatewayMock, - $objectManager->get(\Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface::class), - null, - null, - [] - ] - ) - ->getMock(); - - $response = new \Magento\Framework\DataObject( - [ - 'result' => '0', - 'pnref' => 'V19A3D27B61E', - 'respmsg' => 'Approved', - 'authcode' => '510PNI', - 'hostcode' => 'A', - 'request_id' => 'f930d3dc6824c1f7230c5529dc37ae5e', - 'result_code' => '0', - ] - ); - - $gatewayMock->expects($this->once()) - ->method('postRequest') - ->willReturn($response); - $instance->expects($this->once()) - ->method('setStore') - ->willReturnSelf(); - - $payment->setMethodInstance($instance); - $payment->void(new \Magento\Framework\DataObject()); - $order->save(); - - $order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Sales\Model\Order::class); - $order->loadByIncrementId('100000001'); - $this->assertFalse($order->canVoidPayment()); - } -} diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php index a8a12650f9935..aebe8b4e3ef47 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php @@ -119,14 +119,14 @@ public function testResolvePlaceOrderWithPayflowLinkForCustomer(): void cart_id: "$cartId" payment_method: { code: "$paymentMethod" - payflow_link: + payflow_link: { cancel_url:"paypal/payflow/cancelPayment" return_url:"paypal/payflow/returnUrl" error_url:"paypal/payflow/errorUrl" } } - }) { + }) { cart { selected_payment_method { code @@ -142,7 +142,7 @@ public function testResolvePlaceOrderWithPayflowLinkForCustomer(): void QUERY; $productMetadata = ObjectManager::getInstance()->get(ProductMetadataInterface::class); - $button = 'Magento_Cart_' . $productMetadata->getEdition(); + $button = 'Magento_2_' . $productMetadata->getEdition(); $payflowLinkResponse = new DataObject( [ diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProCCVaultTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProCCVaultTest.php new file mode 100644 index 0000000000000..3ebfaf8890edb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProCCVaultTest.php @@ -0,0 +1,363 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Resolver\Customer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Checkout\Api\ShippingInformationManagementInterface; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\DataObject; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Integration\Model\Oauth\Token; +use Magento\PaypalGraphQl\PaypalPayflowProAbstractTest; +use Magento\Quote\Api\BillingAddressManagementInterface; +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteId; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ShippingAddressManagementInterface; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Model\PaymentTokenManagement; +use Magento\Vault\Model\PaymentTokenRepository; + +/** + * End to end place order test using payflowpro_cc_vault via graphql endpoint for customer + * + * @magentoAppArea graphql + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PlaceOrderWithPayflowProCCVaultTest extends PaypalPayflowProAbstractTest +{ + /** + * @var SerializerInterface + */ + private $json; + + /** + * @var QuoteIdToMaskedQuoteId + */ + private $quoteIdToMaskedId; + + protected function setUp(): void + { + parent::setUp(); + + $this->json = $this->objectManager->get(SerializerInterface::class); + $this->quoteIdToMaskedId = $this->objectManager->get(QuoteIdToMaskedQuoteId::class); + } + + /** + * Place order use payflowpro method and save cart data to future + * + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * + * @return void + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testPlaceOrderWithCCVault(): void + { + $this->placeOrderPayflowPro('is_active_payment_token_enabler: true'); + $publicHash = $this->getVaultCartData()->getPublicHash(); + /** @var CartManagementInterface $cartManagement */ + $cartManagement = $this->objectManager->get(CartManagementInterface::class); + /** @var CartRepositoryInterface $cartRepository */ + $cartRepository = $this->objectManager->get(CartRepositoryInterface::class); + /** @var QuoteIdMaskFactory $quoteIdMaskFactory */ + $quoteIdMaskFactory = $this->objectManager->get(QuoteIdMaskFactory::class); + $cartId = $cartManagement->createEmptyCartForCustomer(1); + $cart = $cartRepository->get($cartId); + $cart->setReservedOrderId('test_quote_1'); + $cartRepository->save($cart); + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = $quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($cartId) + ->save(); + + $reservedQuoteId = 'test_quote_1'; + $cart = $this->getQuoteByReservedOrderId($reservedQuoteId); + $cartId = $this->quoteIdToMaskedId->execute((int)$cart->getId()); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var QuoteFactory $quoteFactory */ + $quoteFactory = $this->objectManager->get(QuoteFactory::class); + /** @var QuoteResource $quoteResource */ + $quoteResource = $this->objectManager->get(QuoteResource::class); + $product = $productRepository->get('simple_product'); + $quote = $quoteFactory->create(); + $quoteResource->load($quote, 'test_quote_1', 'reserved_order_id'); + $quote->addProduct($product, 2); + $cartRepository->save($quote); + + /** @var AddressInterfaceFactory $quoteAddressFactory */ + $quoteAddressFactory = $this->objectManager->get(AddressInterfaceFactory::class); + /** @var DataObjectHelper $dataObjectHelper */ + $dataObjectHelper = $this->objectManager->get(DataObjectHelper::class); + /** @var ShippingAddressManagementInterface $shippingAddressManagement */ + $shippingAddressManagement = $this->objectManager->get(ShippingAddressManagementInterface::class); + + $quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => '75477', + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityM', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => 'Green str, 67', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_ID => 1, + ]; + $quoteAddress = $quoteAddressFactory->create(); + $dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); + + $quote = $quoteFactory->create(); + $quoteResource->load($quote, 'test_quote_1', 'reserved_order_id'); + $shippingAddressManagement->assign($quote->getId(), $quoteAddress); + + /** @var BillingAddressManagementInterface $billingAddressManagement */ + $billingAddressManagement = $this->objectManager->get(BillingAddressManagementInterface::class); + $billingAddressManagement->assign($quote->getId(), $quoteAddress); + + /** @var ShippingInformationInterfaceFactory $shippingInformationFactory */ + $shippingInformationFactory = $this->objectManager->get(ShippingInformationInterfaceFactory::class); + /** @var ShippingInformationManagementInterface $shippingInformationManagement */ + $shippingInformationManagement = $this->objectManager->get(ShippingInformationManagementInterface::class); + $quoteAddress = $quote->getShippingAddress(); + + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $shippingInformationFactory->create([ + 'data' => [ + ShippingInformationInterface::SHIPPING_ADDRESS => $quoteAddress, + ShippingInformationInterface::SHIPPING_CARRIER_CODE => 'flatrate', + ShippingInformationInterface::SHIPPING_METHOD_CODE => 'flatrate', + ], + ]); + $shippingInformationManagement->saveAddressInformation($quote->getId(), $shippingInformation); + + $secondQuery = <<<QUERY +mutation { +setPaymentMethodOnCart(input: { +payment_method: { + code: "payflowpro_cc_vault", + payflowpro_cc_vault: { + public_hash:"{$publicHash}" + } +}, +cart_id: "{$cartId}"}) +{ +cart { + selected_payment_method {code} + } +} +placeOrder(input: {cart_id: "{$cartId}"}) { +order {order_number} + } +} +QUERY; + /** @var Token $tokenModel */ + $tokenModel = $this->objectManager->create(Token::class); + $customerToken = $tokenModel->createCustomerToken(1)->getToken(); + + $requestHeaders = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $customerToken + ]; + $vaultResponse = $this->graphQlRequest->send($secondQuery, [], '', $requestHeaders); + + $responseData = $this->json->unserialize($vaultResponse->getContent()); + $this->assertArrayHasKey('data', $responseData); + $this->assertTrue( + isset($responseData['data']['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']) + ); + $this->assertEquals( + 'payflowpro_cc_vault', + $responseData['data']['setPaymentMethodOnCart']['cart']['selected_payment_method']['code'] + ); + $this->assertTrue( + isset($responseData['data']['placeOrder']['order']['order_number']) + ); + $this->assertEquals( + 'test_quote_1', + $responseData['data']['placeOrder']['order']['order_number'] + ); + } + + /** + * @param $isActivePaymentTokenEnabler + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function placeOrderPayflowPro($isActivePaymentTokenEnabler) + { + $paymentMethod = 'payflowpro'; + $this->enablePaymentMethod($paymentMethod); + $this->enablePaymentMethod('payflowpro_cc_vault'); + $reservedQuoteId = 'test_quote'; + + $payload = 'BILLTOCITY=CityM&AMT=0.00&BILLTOSTREET=Green+str,+67&VISACARDLEVEL=12&SHIPTOCITY=CityM' + . '&NAMETOSHIP=John+Smith&ZIP=75477&BILLTOLASTNAME=Smith&BILLTOFIRSTNAME=John' + . '&RESPMSG=Verified&PROCCVV2=M&STATETOSHIP=AL&NAME=John+Smith&BILLTOZIP=75477&CVV2MATCH=Y' + . '&PNREF=B70CCC236815&ZIPTOSHIP=75477&SHIPTOCOUNTRY=US&SHIPTOSTREET=Green+str,+67&CITY=CityM' + . '&HOSTCODE=A&LASTNAME=Smith&STATE=AL&SECURETOKEN=MYSECURETOKEN&CITYTOSHIP=CityM&COUNTRYTOSHIP=US' + . '&AVSDATA=YNY&ACCT=1111&AUTHCODE=111PNI&FIRSTNAME=John&RESULT=0&IAVS=N&POSTFPSMSG=No+Rules+Triggered&' + . 'BILLTOSTATE=AL&BILLTOCOUNTRY=US&EXPDATE=0222&CARDTYPE=0&PREFPSMSG=No+Rules+Triggered&SHIPTOZIP=75477&' + . 'PROCAVS=A&COUNTRY=US&AVSZIP=N&ADDRESS=Green+str,+67&BILLTONAME=John+Smith&' + . 'ADDRESSTOSHIP=Green+str,+67&' + . 'AVSADDR=Y&SECURETOKENID=MYSECURETOKENID&SHIPTOSTATE=AL&TRANSTIME=2019-06-24+07%3A53%3A10'; + + $cart = $this->getQuoteByReservedOrderId($reservedQuoteId); + $cartId = $this->quoteIdToMaskedId->execute((int)$cart->getId()); + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + payment_method: { + code: "{$paymentMethod}", + payflowpro: { + {$isActivePaymentTokenEnabler} + cc_details: { + cc_exp_month: 12, + cc_exp_year: 2030, + cc_last_4: 1111, + cc_type: "IV", + } + } + }, + cart_id: "{$cartId}"}) + { + cart { + selected_payment_method { + code + } + } + } + createPayflowProToken( + input: { + cart_id:"{$cartId}", + urls: { + cancel_url: "paypal/transparent/cancel/" + error_url: "paypal/transparent/error/" + return_url: "paypal/transparent/response/" + } + } + ) { + response_message + result + result_code + secure_token + secure_token_id + } + handlePayflowProResponse(input: { + paypal_payload: "$payload", + cart_id: "{$cartId}" + }) + { + cart { + selected_payment_method { + code + } + } + } + placeOrder(input: {cart_id: "{$cartId}"}) { + order { + order_number + } + } +} +QUERY; + + /** @var Token $tokenModel */ + $tokenModel = $this->objectManager->create(Token::class); + $customerToken = $tokenModel->createCustomerToken(1)->getToken(); + + $requestHeaders = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $customerToken + ]; + $paypalResponse = new DataObject( + [ + 'result' => '0', + 'securetoken' => 'mysecuretoken', + 'securetokenid' => 'mysecuretokenid', + 'respmsg' => 'Approved', + 'result_code' => '0', + ] + ); + + $this->gatewayMock + ->method('postRequest') + ->willReturn($paypalResponse); + + $this->gatewayMock + ->method('postRequest') + ->willReturn( + new DataObject( + [ + 'result' => '0', + 'pnref' => 'A70AAC2378BA', + 'respmsg' => 'Approved', + 'authcode' => '647PNI', + 'avsaddr' => 'Y', + 'avszip' => 'N', + 'hostcode' => 'A', + 'procavs' => 'A', + 'visacardlevel' => '12', + 'transtime' => '2019-06-24 10:12:03', + 'firstname' => 'Cristian', + 'lastname' => 'Partica', + 'amt' => '14.99', + 'acct' => '1111', + 'expdate' => '0221', + 'cardtype' => '0', + 'iavs' => 'N', + 'result_code' => '0', + ] + ) + ); + + $response = $this->graphQlRequest->send($query, [], '', $requestHeaders); + + return $this->json->unserialize($response->getContent()); + } + + /** + * Get saved cart data + * + * @return PaymentTokenInterface + */ + private function getVaultCartData() + { + /** @var PaymentTokenManagement $tokenManagement */ + $tokenManagement = $this->objectManager->get(PaymentTokenManagement::class); + $token = $tokenManagement->getByGatewayToken( + 'B70CCC236815', + 'payflowpro', + 1 + ); + /** @var PaymentTokenRepository $tokenRepository */ + $tokenRepository = $this->objectManager->get(PaymentTokenRepository::class); + return $tokenRepository->getById($token->getEntityId()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/SaveCartDataWithPayflowProTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/SaveCartDataWithPayflowProTest.php new file mode 100644 index 0000000000000..11bd306211b9a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/SaveCartDataWithPayflowProTest.php @@ -0,0 +1,258 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Resolver\Customer; + +use Magento\Integration\Model\Oauth\Token; +use Magento\PaypalGraphQl\PaypalPayflowProAbstractTest; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Quote\Model\QuoteIdToMaskedQuoteId; +use Magento\Framework\DataObject; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Model\PaymentTokenManagement; +use Magento\Vault\Model\PaymentTokenRepository; + +/** + * End to end place order test using payflowpro via graphql endpoint for customer + * + * @magentoAppArea graphql + */ +class SaveCartDataWithPayflowProTest extends PaypalPayflowProAbstractTest +{ + /** + * @var SerializerInterface + */ + private $json; + + /** + * @var QuoteIdToMaskedQuoteId + */ + private $quoteIdToMaskedId; + + protected function setUp(): void + { + parent::setUp(); + + $this->json = $this->objectManager->get(SerializerInterface::class); + $this->quoteIdToMaskedId = $this->objectManager->get(QuoteIdToMaskedQuoteId::class); + } + + /** + * Place order use payflowpro method and save cart data to future + * + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * + * @return void + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testPlaceOrderAndSaveDataForFuturePayflowPro(): void + { + $responseData = $this->placeOrderPayflowPro('is_active_payment_token_enabler: true'); + $this->assertArrayHasKey('data', $responseData); + $this->assertArrayHasKey('createPayflowProToken', $responseData['data']); + $this->assertNotEmpty($this->getVaultCartData()->getPublicHash()); + $this->assertNotEmpty($this->getVaultCartData()->getTokenDetails()); + $this->assertNotEmpty($this->getVaultCartData()->getGatewayToken()); + $this->assertTrue($this->getVaultCartData()->getIsActive()); + $this->assertTrue($this->getVaultCartData()->getIsVisible()); + } + + /** + * Place order use payflowpro method and not save cart data to future + * + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * + * @return void + */ + public function testPlaceOrderAndNotSaveDataForFuturePayflowPro(): void + { + $responseData = $this->placeOrderPayflowPro('is_active_payment_token_enabler: false'); + $this->assertArrayHasKey('data', $responseData); + $this->assertArrayHasKey('createPayflowProToken', $responseData['data']); + $this->assertNotEmpty($this->getVaultCartData()->getPublicHash()); + $this->assertNotEmpty($this->getVaultCartData()->getTokenDetails()); + $this->assertNotEmpty($this->getVaultCartData()->getGatewayToken()); + $this->assertTrue($this->getVaultCartData()->getIsActive()); + $this->assertFalse($this->getVaultCartData()->getIsVisible()); + } + + /** + * @param $isActivePaymentTokenEnabler + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function placeOrderPayflowPro($isActivePaymentTokenEnabler) + { + $paymentMethod = 'payflowpro'; + $this->enablePaymentMethod($paymentMethod); + $this->enablePaymentMethod('payflowpro_cc_vault'); + $reservedQuoteId = 'test_quote'; + + $payload = 'BILLTOCITY=CityM&AMT=0.00&BILLTOSTREET=Green+str,+67&VISACARDLEVEL=12&SHIPTOCITY=CityM' + . '&NAMETOSHIP=John+Smith&ZIP=75477&BILLTOLASTNAME=Smith&BILLTOFIRSTNAME=John' + . '&RESPMSG=Verified&PROCCVV2=M&STATETOSHIP=AL&NAME=John+Smith&BILLTOZIP=75477&CVV2MATCH=Y' + . '&PNREF=B70CCC236815&ZIPTOSHIP=75477&SHIPTOCOUNTRY=US&SHIPTOSTREET=Green+str,+67&CITY=CityM' + . '&HOSTCODE=A&LASTNAME=Smith&STATE=AL&SECURETOKEN=MYSECURETOKEN&CITYTOSHIP=CityM&COUNTRYTOSHIP=US' + . '&AVSDATA=YNY&ACCT=1111&AUTHCODE=111PNI&FIRSTNAME=John&RESULT=0&IAVS=N&POSTFPSMSG=No+Rules+Triggered&' + . 'BILLTOSTATE=AL&BILLTOCOUNTRY=US&EXPDATE=0222&CARDTYPE=0&PREFPSMSG=No+Rules+Triggered&SHIPTOZIP=75477&' + . 'PROCAVS=A&COUNTRY=US&AVSZIP=N&ADDRESS=Green+str,+67&BILLTONAME=John+Smith&' + . 'ADDRESSTOSHIP=Green+str,+67&' + . 'AVSADDR=Y&SECURETOKENID=MYSECURETOKENID&SHIPTOSTATE=AL&TRANSTIME=2019-06-24+07%3A53%3A10'; + + $cart = $this->getQuoteByReservedOrderId($reservedQuoteId); + $cartId = $this->quoteIdToMaskedId->execute((int)$cart->getId()); + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + payment_method: { + code: "{$paymentMethod}", + payflowpro: { + {$isActivePaymentTokenEnabler} + cc_details: { + cc_exp_month: 12, + cc_exp_year: 2030, + cc_last_4: 1111, + cc_type: "IV", + } + } + }, + cart_id: "{$cartId}"}) + { + cart { + selected_payment_method { + code + } + } + } + createPayflowProToken( + input: { + cart_id:"{$cartId}", + urls: { + cancel_url: "paypal/transparent/cancel/" + error_url: "paypal/transparent/error/" + return_url: "paypal/transparent/response/" + } + } + ) { + response_message + result + result_code + secure_token + secure_token_id + } + handlePayflowProResponse(input: { + paypal_payload: "$payload", + cart_id: "{$cartId}" + }) + { + cart { + selected_payment_method { + code + } + } + } + placeOrder(input: {cart_id: "{$cartId}"}) { + order { + order_number + } + } +} +QUERY; + + /** @var Token $tokenModel */ + $tokenModel = $this->objectManager->create(Token::class); + $customerToken = $tokenModel->createCustomerToken(1)->getToken(); + + $requestHeaders = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $customerToken + ]; + $paypalResponse = new DataObject( + [ + 'result' => '0', + 'securetoken' => 'mysecuretoken', + 'securetokenid' => 'mysecuretokenid', + 'respmsg' => 'Approved', + 'result_code' => '0', + ] + ); + + $this->gatewayMock + ->method('postRequest') + ->willReturn($paypalResponse); + + $this->gatewayMock + ->method('postRequest') + ->willReturn( + new DataObject( + [ + 'result' => '0', + 'pnref' => 'A70AAC2378BA', + 'respmsg' => 'Approved', + 'authcode' => '647PNI', + 'avsaddr' => 'Y', + 'avszip' => 'N', + 'hostcode' => 'A', + 'procavs' => 'A', + 'visacardlevel' => '12', + 'transtime' => '2019-06-24 10:12:03', + 'firstname' => 'Cristian', + 'lastname' => 'Partica', + 'amt' => '14.99', + 'acct' => '1111', + 'expdate' => '0221', + 'cardtype' => '0', + 'iavs' => 'N', + 'result_code' => '0', + ] + ) + ); + + $response = $this->graphQlRequest->send($query, [], '', $requestHeaders); + + return $this->json->unserialize($response->getContent()); + } + + /** + * Get saved cart data + * + * @return PaymentTokenInterface + */ + private function getVaultCartData() + { + /** @var PaymentTokenManagement $tokenManagement */ + $tokenManagement = $this->objectManager->get(PaymentTokenManagement::class); + $token = $tokenManagement->getByGatewayToken( + 'B70CCC236815', + 'payflowpro', + 1 + ); + /** @var PaymentTokenRepository $tokenRepository */ + $tokenRepository = $this->objectManager->get(PaymentTokenRepository::class); + return $tokenRepository->getById($token->getEntityId()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php index 797876cc2318f..a0776250cfc56 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php @@ -117,14 +117,14 @@ public function testResolvePlaceOrderWithPayflowLink(): void cart_id: "$cartId" payment_method: { code: "$paymentMethod" - payflow_link: + payflow_link: { cancel_url:"paypal/payflow/cancel" return_url:"paypal/payflow/return" error_url:"paypal/payflow/error" } } - }) { + }) { cart { selected_payment_method { code @@ -140,7 +140,7 @@ public function testResolvePlaceOrderWithPayflowLink(): void QUERY; $productMetadata = ObjectManager::getInstance()->get(ProductMetadataInterface::class); - $button = 'Magento_Cart_' . $productMetadata->getEdition(); + $button = 'Magento_2_' . $productMetadata->getEdition(); $payflowLinkResponse = new DataObject( [ @@ -219,14 +219,14 @@ public function testResolveWithPayflowLinkDeclined(): void cart_id: "$cartId" payment_method: { code: "$paymentMethod" - payflow_link: + payflow_link: { cancel_url:"paypal/payflow/cancelPayment" return_url:"paypal/payflow/returnUrl" error_url:"paypal/payflow/returnUrl" } } - }) { + }) { cart { selected_payment_method { code diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php index 5de1ded43405a..468d9036992b9 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php @@ -108,7 +108,7 @@ public function testResolvePlaceOrderWithPaymentsAdvanced(): void $cartId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $productMetadata = ObjectManager::getInstance()->get(ProductMetadataInterface::class); - $button = 'Magento_Cart_' . $productMetadata->getEdition(); + $button = 'Magento_2_' . $productMetadata->getEdition(); $payflowLinkResponse = new DataObject( [ @@ -256,7 +256,7 @@ private function setPaymentMethodAndPlaceOrder(string $cartId, string $paymentMe error_url:"paypal/payflowadvanced/customerror" } } - }) { + }) { cart { selected_payment_method { code @@ -300,7 +300,7 @@ private function setPaymentMethodWithInValidUrl(string $cartId, string $paymentM error_url:"paypal/payflowadvanced/error" } } - }) { + }) { cart { selected_payment_method { code diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php index 7a622ab15814e..6c4f96121a96d 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php @@ -13,7 +13,7 @@ $notifyUrl = $url->getUrl('paypal/ipn/'); $productMetadata = ObjectManager::getInstance()->get(ProductMetadataInterface::class); -$button = 'Magento_Cart_' . $productMetadata->getEdition(); +$button = 'Magento_2_' . $productMetadata->getEdition(); return [ 'TOKEN' => $token, 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 c2e499c455983..bd4d24211f1e3 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php @@ -3,72 +3,159 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Persistent\Observer; +use DateTime; +use DateTimeZone; +use Magento\Customer\Api\CustomerRepositoryInterface; +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) */ -class SynchronizePersistentOnLoginObserverTest extends \PHPUnit\Framework\TestCase +class SynchronizePersistentOnLoginObserverTest extends TestCase { /** - * @var \Magento\Persistent\Observer\SynchronizePersistentOnLoginObserver + * @var SynchronizePersistentOnLoginObserver + */ + private $model; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var PersistentSessionHelper + */ + private $persistentSessionHelper; + + /** + * @var CustomerRepositoryInterface */ - protected $_model; + private $customerRepository; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var SessionFactory */ - protected $_objectManager; + private $persistentSessionFactory; /** - * @var \Magento\Persistent\Helper\Session + * @var CookieManagerInterface */ - protected $_persistentSession; + private $cookieManager; /** - * @var \Magento\Customer\Model\Session + * @var CustomerSession */ - protected $_customerSession; + private $customerSession; + /** + * @inheritDoc + */ protected function setUp(): void { - $this->_objectManager = \Magento\TestFramework\Helper\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( - \Magento\Persistent\Observer\SynchronizePersistentOnLoginObserver::class, - [ - 'persistentSession' => $this->_persistentSession, - 'customerSession' => $this->_customerSession - ] - ); + 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 + { + $customer = $this->customerRepository->get('customer@example.com'); + $sessionModel = $this->persistentSessionFactory->create(); + $sessionModel->loadByCustomerId($customer->getId()); + $this->assertNull($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'); + $sessionModel = $this->persistentSessionFactory->create(); + $sessionModel->setCustomerId($customer->getId()); + $sessionModel->setUpdatedAt($lastUpdatedAt); + $sessionModel->save(); + $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)); } /** - * @covers \Magento\Persistent\Observer\SynchronizePersistentOnLoginObserver::execute + * @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 testSynchronizePersistentOnLogin() + public function testDisabledPersistentSessionLifetime(): void { - $event = new \Magento\Framework\Event(); - $observer = new \Magento\Framework\Event\Observer(['event' => $event]); - - /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ - $customerRepository = $this->_objectManager->create( - \Magento\Customer\Api\CustomerRepositoryInterface::class - ); - - /** @var $customer \Magento\Customer\Api\Data\CustomerInterface */ - $customer = $customerRepository->getById(1); - $event->setData('customer', $customer); - $this->_persistentSession->setRememberMeChecked(true); - $this->_model->execute($observer); - - // 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 - ); - $sessionModel->loadByCustomerId(1); - $this->assertEquals(1, $sessionModel->getCustomerId()); + $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/Phpserver/PhpserverTest.php b/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php index e7a1de90fc933..53ed800dbdb31 100644 --- a/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Phpserver; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; + /** * @magentoAppIsolation enabled * @@ -19,47 +22,54 @@ class PhpserverTest extends \PHPUnit\Framework\TestCase { const BASE_URL = '127.0.0.1:8082'; - private static $serverPid; + /** + * @var Process + */ + private $serverProcess; /** * @var \Laminas\Http\Client */ private $httpClient; + private function getUrl($url) + { + return sprintf('http://%s/%s', self::BASE_URL, ltrim($url, '/')); + } + /** - * Instantiate phpserver in the pub folder + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - public static function setUpBeforeClass(): void + protected function setUp(): void { - if (!(defined('TRAVIS') && TRAVIS === true)) { - self::markTestSkipped('Travis environment test'); - } - $return = []; + $this->httpClient = new \Laminas\Http\Client(null, ['timeout' => 10]); - $baseDir = __DIR__ . '/../../../../../../'; + /** @var Process $process */ + $phpBinaryFinder = new PhpExecutableFinder(); + $phpBinaryPath = $phpBinaryFinder->find(); $command = sprintf( - 'cd %s && php -S %s -t ./pub/ ./phpserver/router.php >/dev/null 2>&1 & echo $!', - $baseDir, - static::BASE_URL + "%s -S %s -t ./pub ./phpserver/router.php", + $phpBinaryPath, + self::BASE_URL ); - // phpcs:ignore - exec($command, $return); - static::$serverPid = (int) $return[0]; - } - - private function getUrl($url) - { - return sprintf('http://%s/%s', self::BASE_URL, ltrim($url, '/')); + $this->serverProcess = Process::fromShellCommandline( + $command, + realpath(__DIR__ . '/../../../../../../') + ); + $this->serverProcess->start(); + $this->serverProcess->waitUntil(function ($type, $output) { + return strpos($output, "Development Server") !== false; + }); } - protected function setUp(): void + protected function tearDown(): void { - $this->httpClient = new \Laminas\Http\Client(null, ['timeout' => 10]); + $this->serverProcess->stop(); } public function testServerHasPid() { - $this->assertTrue(static::$serverPid > 0); + $this->assertTrue($this->serverProcess->getPid() > 0); } public function testServerResponds() @@ -86,9 +96,4 @@ public function testStaticImageFile() $this->assertFalse($response->isClientError()); $this->assertStringStartsWith('image/gif', $response->getHeaders()->get('Content-Type')->getMediaType()); } - - public static function tearDownAfterClass(): void - { - posix_kill(static::$serverPid, SIGKILL); - } } diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php index 5f2cee2368c98..94fe0e85a8ddf 100644 --- a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php @@ -3,24 +3,32 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\ProductAlert\Model; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Helper\View; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\MailException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\Website; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; /** * Test for Magento\ProductAlert\Model\Email class. * * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class EmailTest extends \PHPUnit\Framework\TestCase +class EmailTest extends TestCase { /** * @var Email @@ -28,7 +36,7 @@ class EmailTest extends \PHPUnit\Framework\TestCase protected $_emailModel; /** - * @var \Magento\TestFramework\ObjectManager + * @var ObjectManager */ protected $_objectManager; @@ -38,7 +46,7 @@ class EmailTest extends \PHPUnit\Framework\TestCase protected $customerAccountManagement; /** - * @var \Magento\Customer\Helper\View + * @var View */ protected $_customerViewHelper; @@ -62,11 +70,11 @@ class EmailTest extends \PHPUnit\Framework\TestCase */ protected function setUp(): void { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->_objectManager = Bootstrap::getObjectManager(); $this->customerAccountManagement = $this->_objectManager->create( AccountManagementInterface::class ); - $this->_customerViewHelper = $this->_objectManager->create(\Magento\Customer\Helper\View::class); + $this->_customerViewHelper = $this->_objectManager->create(View::class); $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); $this->customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class); $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); @@ -100,7 +108,7 @@ public function testSend($isCustomerIdUsed) $this->_emailModel->setCustomerData($customer); } - /** @var \Magento\Catalog\Model\Product $product */ + /** @var Product $product */ $product = $this->productRepository->getById(1); $this->_emailModel->addPriceProduct($product); @@ -165,4 +173,36 @@ public function testEmailForDifferentCustomers(): void ); } } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_store_with_second_identity.php + */ + public function testScopedMessageIdentity() + { + /** @var Website $website */ + $website = $this->_objectManager->create(Website::class); + $website->load(1); + $this->_emailModel->setWebsite($website); + + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->_objectManager->create(StoreManagerInterface::class); + $store = $storeManager->getStore('fixture_second_store'); + $this->_emailModel->setStoreId($store->getId()); + + $customer = $this->customerRepository->getById(1); + $this->_emailModel->setCustomerData($customer); + + /** @var Product $product */ + $product = $this->productRepository->getById(1); + + $this->_emailModel->addPriceProduct($product); + $this->_emailModel->send(); + + $from = $this->transportBuilder->getSentMessage()->getFrom()[0]; + $this->assertEquals('Fixture Store Owner', $from->getName()); + $this->assertEquals('fixture.store.owner@example.com', $from->getEmail()); + } } 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/Model/CustomerManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/CustomerManagementTest.php new file mode 100644 index 0000000000000..a34bfa382d427 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/CustomerManagementTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; +use PHPUnit\Framework\TestCase; + +/** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + */ +class CustomerManagementTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var CustomerManagement + */ + private $customerManagemet; + + /** + * @var CustomerInterface + */ + private $customer; + + protected function setUp(): void + { + $this->objectManager = BootstrapHelper::getObjectManager(); + $this->customerManagemet = $this->objectManager->create(CustomerManagement::class); + $this->customer = $this->objectManager->create(CustomerInterface::class); + } + + protected function tearDown(): void + { + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class); + $customer = $customerRepository->get('john1.doe001@test.com'); + $customerRepository->delete($customer); + } + + /** + * @magentoDataFixture Magento/Sales/_files/quote.php + */ + public function testCustomerAddressIdQuote(): void + { + $reservedOrderId = 'test01'; + + $this->customer->setEmail('john1.doe001@test.com') + ->setFirstname('doe') + ->setLastname('john'); + + $quote = $this->getQuote($reservedOrderId)->setCustomer($this->customer); + $this->customerManagemet->populateCustomerInfo($quote); + self::assertNotNull($quote->getBillingAddress()->getCustomerAddressId()); + self::assertNotNull($quote->getShippingAddress()->getCustomerAddressId()); + } + + /** + * Gets quote by reserved order ID. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuote(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/RemoveQuoteItemsTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/RemoveQuoteItemsTest.php new file mode 100644 index 0000000000000..ccc146c459b07 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/RemoveQuoteItemsTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Product\Plugin; + +use Magento\Catalog\Model\ResourceModel\Product as ProductResourceModel; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Tests for remove quote items plugin. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class RemoveQuoteItemsTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var ProductResourceModel */ + private $productResoure; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->productResoure = $this->objectManager->get(ProductResourceModel::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + } + + /** + * @return void + */ + public function testPluginIsRegistered(): void + { + $pluginInfo = $this->objectManager->get(PluginList::class)->get(ProductResourceModel::class); + $this->assertSame( + RemoveQuoteItems::class, + $pluginInfo['clean_quote_items_after_product_delete']['instance'] + ); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * + * @return void + */ + public function testDeleteProduct(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $this->assertNotNull($quote); + $quoteItems = $quote->getItems(); + $quoteItem = current($quoteItems); + $this->assertNotNull($quoteItem); + $this->productResoure->delete($quoteItem->getProduct()); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $this->assertNotNull($quote); + $this->assertEmpty($quote->getItems()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php new file mode 100644 index 0000000000000..3aadad7e9ebec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Product\Plugin; + +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Tests for update quote items plugin + * + * @magentoAppArea adminhtml + */ +class UpdateQuoteItemsTest extends TestCase +{ + /** + * @var GetQuoteByReservedOrderId + */ + private $getQuoteByReservedOrderId; + + /** + * @var ProductRepository + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $objectManager = Bootstrap::getObjectManager(); + $this->getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); + $this->productRepository = $objectManager->get(ProductRepository::class); + } + + /** + * Test to mark the quote as need to recollect and doesn't update the field "updated_at" after change product price + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @return void + */ + public function testMarkQuoteRecollectAfterChangeProductPrice(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $this->assertNotNull($quote); + $this->assertFalse((bool)$quote->getTriggerRecollect()); + $this->assertNotEmpty($quote->getItems()); + $quoteItem = current($quote->getItems()); + $product = $quoteItem->getProduct(); + + $product->setPrice((float)$product->getPrice() + 10); + $this->productRepository->save($product); + + /** @var AdapterInterface $connection */ + $connection = $quote->getResource()->getConnection(); + $select = $connection->select() + ->from( + $connection->getTableName('quote'), + ['updated_at', 'trigger_recollect'] + )->where( + "reserved_order_id = 'test_order_with_simple_product_without_address'" + ); + + $quoteRow = $connection->fetchRow($select); + $this->assertNotEmpty($quoteRow); + $this->assertTrue((bool)$quoteRow['trigger_recollect']); + $this->assertEquals($quote->getUpdatedAt(), $quoteRow['updated_at']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/CartItemPersisterTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/CartItemPersisterTest.php new file mode 100644 index 0000000000000..647b8a188a55c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/CartItemPersisterTest.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Quote\Item; + +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\Quote\Api\Data\CartItemInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Test for quote item persister model. + * + * @see \Magento\Quote\Model\Quote\Item\CartItemPersister + * @magentoDbIsolation enabled + */ +class CartItemPersisterTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CartItemPersister */ + private $model; + + /** @var CartInterfaceFactory */ + private $quoteFactory; + + /** @var CartItemInterfaceFactory */ + private $itemFactory; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(CartItemPersister::class); + $this->quoteFactory = $this->objectManager->get(CartInterfaceFactory::class); + $this->itemFactory = $this->objectManager->get(CartItemInterfaceFactory::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/simple_product_disabled.php + * + * @return void + */ + public function testSaveDisabledItem(): void + { + $quote = $this->quoteFactory->create(); + $item = $this->itemFactory->create(); + $item->setSku('product_disabled')->setQty(1); + $this->expectExceptionObject( + new LocalizedException(__('Product that you are trying to add is not available.')) + ); + $this->model->save($quote, $item); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * + * @return void + */ + public function testSaveQuoteItemWithoutQty(): void + { + $quote = $this->quoteFactory->create(); + $item = $this->itemFactory->create(); + $item->setSku('simple-1'); + $this->expectExceptionObject(InputException::invalidFieldValue('qty', null)); + $this->model->save($quote, $item); + } + + /** + * @return void + */ + public function testSaveQuoteItemWithNotExistingProduct(): void + { + $quote = $this->quoteFactory->create(); + $item = $this->itemFactory->create(); + $item->setSku('not_existing_product_sku')->setQty(1); + $this->expectExceptionObject( + new NoSuchEntityException( + __('The product that was requested doesn\'t exist. Verify the product and try again.') + ) + ); + $this->model->save($quote, $item); + } + + /** + * @return void + */ + public function testUpdateNotExistingQuoteItem(): void + { + $quote = $this->quoteFactory->create(); + $item = $this->itemFactory->create(); + $item->setItemId(989)->setQty(1); + $this->expectExceptionObject( + new NoSuchEntityException( + __('The %1 Cart doesn\'t contain the %2 item.', null, 989) + ) + ); + $this->model->save($quote, $item); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_taxable_product_and_customer.php + * + * @return void + */ + public function testUpdateQuoteItemMoreQty(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_taxable_product'); + $quoteItem = current($quote->getItems()); + $item = $this->itemFactory->create(); + $item->setQty(9999)->setSku($quoteItem->getSku())->setItemId($quoteItem->getItemId()); + $this->expectExceptionObject(new LocalizedException(__('The requested qty is not available'))); + $this->model->save($quote, $item); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php index dac05f17089a1..facb4879650b1 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php @@ -9,22 +9,30 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Type; -use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\StateException; +use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\CartManagementInterface; -use Magento\Quote\Api\CartRepositoryInterface; use Magento\Sales\Api\OrderManagementInterface; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\TestCase; /** * Class for testing QuoteManagement model + * + * @see \Magento\Quote\Model\QuoteManagement + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class QuoteManagementTest extends \PHPUnit\Framework\TestCase +class QuoteManagementTest extends TestCase { /** - * @var ObjectManager + * @var ObjectManagerInterface */ private $objectManager; @@ -33,14 +41,46 @@ class QuoteManagementTest extends \PHPUnit\Framework\TestCase */ private $cartManagement; + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var GetQuoteByReservedOrderId + */ + private $getQuoteByReservedOrderId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * @inheritdoc */ protected function setUp(): void { - $this->objectManager = Bootstrap::getObjectManager(); + parent::setUp(); - $this->cartManagement = $this->objectManager->create(CartManagementInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->cartManagement = $this->objectManager->get(CartManagementInterface::class); + $this->orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); } /** @@ -48,22 +88,20 @@ protected function setUp(): void * * @magentoAppIsolation enabled * @magentoDataFixture Magento/Sales/_files/quote_with_bundle.php + * + * @return void */ - public function testSubmit() + public function testSubmit(): void { - $quote = $this->getQuote('test01'); + $quote = $this->getQuoteByReservedOrderId->execute('test01'); $orderId = $this->cartManagement->placeOrder($quote->getId()); - - /** @var OrderRepositoryInterface $orderRepository */ - $orderRepository = $this->objectManager->create(OrderRepositoryInterface::class); - $order = $orderRepository->get($orderId); - + $order = $this->orderRepository->get($orderId); $orderItems = $order->getItems(); - self::assertCount(3, $orderItems); + $this->assertCount(3, $orderItems); foreach ($orderItems as $orderItem) { if ($orderItem->getProductType() == Type::TYPE_SIMPLE) { - self::assertNotEmpty($orderItem->getParentItem(), 'Parent is not set for child product'); - self::assertNotEmpty($orderItem->getParentItemId(), 'Parent is not set for child product'); + $this->assertNotEmpty($orderItem->getParentItem(), 'Parent is not set for child product'); + $this->assertNotEmpty($orderItem->getParentItemId(), 'Parent is not set for child product'); } } } @@ -75,17 +113,13 @@ public function testSubmit() * @magentoAppIsolation enabled * @magentoDataFixture Magento/Sales/_files/quote_with_bundle.php */ - public function testSubmitWithDeletedItem() + public function testSubmitWithDeletedItem(): void { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('Some of the products below do not have all the required options.'); - - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple-2'); - $productRepository->delete($product); - $quote = $this->getQuote('test01'); - + $this->productRepository->deleteById('simple-2'); + $quote = $this->getQuoteByReservedOrderId->execute('test01'); + $this->expectExceptionObject( + new LocalizedException(__('Some of the products below do not have all the required options.')) + ); $this->cartManagement->placeOrder($quote->getId()); } @@ -95,13 +129,11 @@ public function testSubmitWithDeletedItem() * @magentoDataFixture Magento/Sales/_files/quote.php * @magentoDbIsolation enabled */ - public function testSubmitWithItemOutOfStock() + public function testSubmitWithItemOutOfStock(): void { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('Some of the products are out of stock.'); - $this->makeProductOutOfStock('simple'); - $quote = $this->getQuote('test01'); + $quote = $this->getQuoteByReservedOrderId->execute('test01'); + $this->expectExceptionObject(new LocalizedException(__('Some of the products are out of stock.'))); $this->cartManagement->placeOrder($quote->getId()); } @@ -111,21 +143,20 @@ public function testSubmitWithItemOutOfStock() * Order should not start placing if order validation is failed. * * @magentoDataFixture Magento/Quote/Fixtures/quote_without_customer_email.php + * + * @return void */ - public function testSubmitWithEmptyCustomerEmail() + public function testSubmitWithEmptyCustomerEmail(): void { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('Email has a wrong format'); - - $quote = $this->getQuote('test01'); - $orderManagement = $this->getMockForAbstractClass(OrderManagementInterface::class); + $quote = $this->getQuoteByReservedOrderId->execute('test01'); + $orderManagement = $this->createMock(OrderManagementInterface::class); $orderManagement->expects($this->never()) ->method('place'); $cartManagement = $this->objectManager->create( CartManagementInterface::class, ['orderManagement' => $orderManagement] ); - + $this->expectExceptionObject(new LocalizedException(__('Email has a wrong format'))); try { $cartManagement->placeOrder($quote->getId()); } catch (ExpectationFailedException $e) { @@ -134,24 +165,56 @@ public function testSubmitWithEmptyCustomerEmail() } /** - * Gets quote by reserved order ID. + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Customer/_files/customer.php * - * @param string $reservedOrderId - * @return Quote + * @return void */ - private function getQuote(string $reservedOrderId): Quote + public function testAssignCustomerToQuote(): void { - /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); - $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) - ->create(); + $customer = $this->customerRepository->get('customer@example.com'); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $result = $this->cartManagement->assignCustomer($quote->getId(), $customer->getId(), $customer->getStoreId()); + $this->assertTrue($result); + $customerQuote = $this->cartManagement->getCartForCustomer($customer->getId()); + $this->assertEquals($quote->getId(), $customerQuote->getId()); + $this->assertEquals($customer->getId(), $customerQuote->getCustomerId()); + $this->assertEquals($customer->getEmail(), $customerQuote->getCustomerEmail()); + } - /** @var CartRepositoryInterface $quoteRepository */ - $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); - $items = $quoteRepository->getList($searchCriteria) - ->getItems(); + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Customer/_files/customer_for_second_website.php + * + * @return void + */ + public function testAssignCustomerFromAnotherWebsiteToQuote(): void + { + $websiteId = $this->storeManager->getWebsite('test')->getId(); + $customer = $this->customerRepository->get('customer@example.com', $websiteId); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $this->expectExceptionObject( + new StateException( + __('The customer can\'t be assigned to the cart. The cart belongs to a different store.') + ) + ); + $this->cartManagement->assignCustomer($quote->getId(), $customer->getId(), $quote->getStoreId()); + } - return array_pop($items); + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * @magentoDataFixture Magento/Customer/_files/customer_with_uk_address.php + * + * @return void + */ + public function testAssignCustomerToQuoteAlreadyHaveCustomer(): void + { + $customer = $this->customerRepository->get('customer_uk_address@test.com'); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->expectExceptionObject( + new StateException(__('The customer can\'t be assigned to the cart because the cart isn\'t anonymous.')) + ); + $this->cartManagement->assignCustomer($quote->getId(), $customer->getId(), $quote->getStoreId()); } /** @@ -160,14 +223,12 @@ private function getQuote(string $reservedOrderId): Quote * @param string $sku * @return void */ - private function makeProductOutOfStock(string $sku) + private function makeProductOutOfStock(string $sku): void { - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($sku); + $product = $this->productRepository->get($sku); $extensionAttributes = $product->getExtensionAttributes(); $stockItem = $extensionAttributes->getStockItem(); $stockItem->setIsInStock(false); - $productRepository->save($product); + $this->productRepository->save($product); } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php index 3b18ee0ceaa5e..f3684e5167b58 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php @@ -3,26 +3,37 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Quote\Model; -use Magento\Store\Model\StoreRepository; -use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; -use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Api\SearchCriteria; -use Magento\Framework\Api\SearchResults; use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartExtension; use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\Quote\Api\Data\CartItemInterfaceFactory; use Magento\Quote\Api\Data\CartSearchResultsInterface; -use Magento\Quote\Api\Data\CartExtension; -use Magento\User\Api\Data\UserInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Api\Data\ShippingInterface; use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; /** + * Test for quote repository + * + * @see \Magento\Quote\Model\QuoteRepository + * @magentoDbIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @magentoDbIsolation disabled */ -class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase +class QuoteRepositoryTest extends TestCase { /** * @var ObjectManagerInterface @@ -30,7 +41,7 @@ class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase private $objectManager; /** - * @var QuoteRepository + * @var CartRepositoryInterface */ private $quoteRepository; @@ -45,14 +56,63 @@ class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase private $filterBuilder; /** - * Set up + * @var GetQuoteByReservedOrderId + */ + private $getQuoteByReservedOrderId; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var AddressInterfaceFactory + */ + private $addressFactory; + + /** + * @var CartInterfaceFactory + */ + private $quoteFactory; + + /** + * @var CartItemInterfaceFactory + */ + private $itemFactory; + + /** + * @var CartInterface|null + */ + private $quote; + + /** + * @inheritdoc */ protected function setUp(): void { + parent::setUp(); + $this->objectManager = BootstrapHelper::getObjectManager(); - $this->quoteRepository = $this->objectManager->create(QuoteRepository::class); - $this->searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); - $this->filterBuilder = $this->objectManager->create(FilterBuilder::class); + $this->quoteRepository = $this->objectManager->create(CartRepositoryInterface::class); + $this->searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $this->filterBuilder = $this->objectManager->get(FilterBuilder::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); + $this->addressFactory = $this->objectManager->get(AddressInterfaceFactory::class); + $this->quoteFactory = $this->objectManager->get(CartInterfaceFactory::class); + $this->itemFactory = $this->objectManager->get(CartItemInterfaceFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + + parent::tearDown(); } /** @@ -60,22 +120,18 @@ protected function setUp(): void * * @magentoDataFixture Magento/Sales/_files/quote.php * @magentoDataFixture Magento/Store/_files/second_store.php + * + * @return void */ - public function testGetQuoteWithCustomStoreId() + public function testGetQuoteWithCustomStoreId(): void { $secondStoreCode = 'fixture_second_store'; $reservedOrderId = 'test01'; - - $storeRepository = $this->objectManager->create(StoreRepository::class); - $secondStore = $storeRepository->get($secondStoreCode); - - // Set store_id in quote to second store_id - $quote = $this->getQuote($reservedOrderId); + $secondStore = $this->storeRepository->get($secondStoreCode); + $quote = $this->getQuoteByReservedOrderId->execute($reservedOrderId); $quote->setStoreId($secondStore->getId()); $this->quoteRepository->save($quote); - $savedQuote = $this->quoteRepository->get($quote->getId()); - $this->assertEquals( $secondStore->getId(), $savedQuote->getStoreId(), @@ -85,8 +141,10 @@ public function testGetQuoteWithCustomStoreId() /** * @magentoDataFixture Magento/Sales/_files/quote.php + * + * @return void */ - public function testGetList() + public function testGetList(): void { $searchCriteria = $this->getSearchCriteria('test01'); $searchResult = $this->quoteRepository->getList($searchCriteria); @@ -95,62 +153,50 @@ public function testGetList() /** * @magentoDataFixture Magento/Sales/_files/quote.php + * + * @return void */ - public function testGetListDoubleCall() + public function testGetListDoubleCall(): void { $searchCriteria1 = $this->getSearchCriteria('test01'); $searchCriteria2 = $this->getSearchCriteria('test02'); $searchResult = $this->quoteRepository->getList($searchCriteria1); $this->performAssertions($searchResult); $searchResult = $this->quoteRepository->getList($searchCriteria2); - $this->assertEmpty($searchResult->getItems()); } /** * @magentoAppIsolation enabled + * + * @return void */ - public function testSaveWithNotExistingCustomerAddress() + public function testSaveWithNotExistingCustomerAddress(): void { $addressData = include __DIR__ . '/../../Sales/_files/address_data.php'; - - /** @var QuoteAddress $billingAddress */ - $billingAddress = $this->objectManager->create(QuoteAddress::class, ['data' => $addressData]); - $billingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_BILLING) - ->setCustomerAddressId('not_existing'); - - /** @var QuoteAddress $shippingAddress */ - $shippingAddress = $this->objectManager->create(QuoteAddress::class, ['data' => $addressData]); - $shippingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_SHIPPING) - ->setCustomerAddressId('not_existing'); - - /** @var Shipping $shipping */ - $shipping = $this->objectManager->create(Shipping::class); + $billingAddress = $this->addressFactory->create(['data' => $addressData]); + $billingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_BILLING)->setCustomerAddressId('not_existing'); + $shippingAddress = $this->addressFactory->create(['data' => $addressData]); + $shippingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_SHIPPING)->setCustomerAddressId('not_existing'); + $shipping = $this->objectManager->get(ShippingInterface::class); $shipping->setAddress($shippingAddress); - - /** @var ShippingAssignment $shippingAssignment */ - $shippingAssignment = $this->objectManager->create(ShippingAssignment::class); + $shippingAssignment = $this->objectManager->get(ShippingAssignmentInterface::class); $shippingAssignment->setItems([]); $shippingAssignment->setShipping($shipping); - - /** @var CartExtension $extensionAttributes */ - $extensionAttributes = $this->objectManager->create(CartExtension::class); + $extensionAttributes = $this->objectManager->get(CartExtension::class); $extensionAttributes->setShippingAssignments([$shippingAssignment]); - - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $quote->setStoreId(1) + $this->quote = $this->quoteFactory->create(); + $this->quote->setStoreId(1) ->setIsActive(true) - ->setIsMultiShipping(false) + ->setIsMultiShipping(0) ->setBillingAddress($billingAddress) ->setShippingAddress($shippingAddress) ->setExtensionAttributes($extensionAttributes) ->save(); - $this->quoteRepository->save($quote); - - $this->assertNull($quote->getBillingAddress()->getCustomerAddressId()); + $this->quoteRepository->save($this->quote); + $this->assertNull($this->quote->getBillingAddress()->getCustomerAddressId()); $this->assertNull( - $quote->getExtensionAttributes() + $this->quote->getExtensionAttributes() ->getShippingAssignments()[0] ->getShipping() ->getAddress() @@ -159,21 +205,37 @@ public function testSaveWithNotExistingCustomerAddress() } /** - * Returns quote by reserved order id. + * @magentoDataFixture Magento/Catalog/_files/multiple_products.php * - * @param string $reservedOrderId - * @return CartInterface + * @return void */ - private function getQuote(string $reservedOrderId) + public function testSaveQuoteWithItems(): void { - $searchCriteria = $this->getSearchCriteria($reservedOrderId); - $searchResult = $this->quoteRepository->getList($searchCriteria); - $items = $searchResult->getItems(); - - /** @var CartInterface $quote */ - $quote = array_pop($items); + $items = $this->prepareQuoteItems(['simple1', 'simple2']); + $this->quote = $this->quoteFactory->create(); + $this->quote->setItems($items); + $this->quoteRepository->save($this->quote); + $this->assertCount(2, $this->quote->getItemsCollection()); + $this->assertEquals(2, $this->quote->getItemsCount()); + $this->assertEquals(2, $this->quote->getItemsQty()); + } - return $quote; + /** + * Prepare quote items by products sku. + * + * @param array $productsSku + * @return array + */ + private function prepareQuoteItems(array $productsSku): array + { + $items = []; + foreach ($productsSku as $sku) { + $item = $this->itemFactory->create(); + $item->setSku($sku)->setQty(1); + $items[] = $item; + } + + return $items; } /** @@ -182,7 +244,7 @@ private function getQuote(string $reservedOrderId) * @param string $filterValue * @return SearchCriteria */ - private function getSearchCriteria($filterValue) + private function getSearchCriteria(string $filterValue): SearchCriteria { $filters = []; $filters[] = $this->filterBuilder->setField('reserved_order_id') @@ -197,24 +259,19 @@ private function getSearchCriteria($filterValue) /** * Perform assertions * - * @param SearchResults|CartSearchResultsInterface $searchResult + * @param CartSearchResultsInterface $searchResult + * @return void */ - private function performAssertions($searchResult) + private function performAssertions(CartSearchResultsInterface $searchResult): void { $expectedExtensionAttributes = [ 'firstname' => 'firstname', 'lastname' => 'lastname', - 'email' => 'admin@example.com' + 'email' => 'admin@example.com', ]; - $items = $searchResult->getItems(); - - /** @var CartInterface $actualQuote */ $actualQuote = array_pop($items); - - /** @var UserInterface $testAttribute */ $testAttribute = $actualQuote->getExtensionAttributes()->getQuoteTestAttribute(); - $this->assertInstanceOf(CartInterface::class, $actualQuote); $this->assertEquals('test01', $actualQuote->getReservedOrderId()); $this->assertEquals($expectedExtensionAttributes['firstname'], $testAttribute->getFirstName()); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php index a9fdbf59a371b..081cae5f98ee5 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php @@ -8,46 +8,111 @@ namespace Magento\Quote\Model; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\ProductRepository; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerInterfaceFactory; -use Magento\Customer\Model\Data\Customer; +use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\GroupFactory; +use Magento\Customer\Model\GroupManagement; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Quote\Api\Data\CartItemInterfaceFactory; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\ObjectManager; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; /** + * Tests for quote model. + * + * @see \Magento\Quote\Model\Quote + * + * @magentoDbIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class QuoteTest extends \PHPUnit\Framework\TestCase +class QuoteTest extends TestCase { - /** - * @var ObjectManager - */ + /** @var ObjectManagerInterface */ private $objectManager; + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** @var QuoteFactory */ + private $quoteFactory; + + /** @var DataObjectHelper */ + private $dataObjectHelper; + + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var CustomerInterfaceFactory */ + private $customerDataFactory; + + /** @var CustomerFactory */ + private $customerFactory; + + /** @var AddressInterfaceFactory */ + private $addressFactory; + + /** @var CartItemInterfaceFactory */ + private $itemFactory; + + /** @var CustomerResourceModel */ + private $customerResourceModel; + + /** @var int */ + private $customerIdToDelete; + + /** @var GroupFactory */ + private $groupFactory; + + /** @var ExtensibleDataObjectConverter */ + private $extensibleDataObjectConverter; + /** * @inheritdoc */ protected function setUp(): void { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->quoteFactory = $this->objectManager->get(QuoteFactory::class); + $this->dataObjectHelper = $this->objectManager->get(DataObjectHelper::class); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->customerDataFactory = $this->objectManager->get(CustomerInterfaceFactory::class); + $this->customerFactory = $this->objectManager->get(CustomerFactory::class); + $this->addressFactory = $this->objectManager->get(AddressInterfaceFactory::class); + $this->itemFactory = $this->objectManager->get(CartItemInterfaceFactory::class); + $this->customerResourceModel = $this->objectManager->get(CustomerResourceModel::class); + $this->groupFactory = $this->objectManager->get(GroupFactory::class); + $this->extensibleDataObjectConverter = $this->objectManager->get(ExtensibleDataObjectConverter::class); } /** - * @param ExtensibleDataInterface $entity - * @return array + * @inheritdoc */ - private function convertToArray(ExtensibleDataInterface $entity): array + protected function tearDown(): void { - return $this->objectManager - ->create(\Magento\Framework\Api\ExtensibleDataObjectConverter::class) - ->toFlatArray($entity); + if ($this->customerIdToDelete) { + $this->customerRepository->deleteById($this->customerIdToDelete); + } + + parent::tearDown(); } /** @@ -57,16 +122,10 @@ private function convertToArray(ExtensibleDataInterface $entity): array */ public function testCollectTotalsWithVirtual(): void { - $quote = $this->objectManager->create(Quote::class); - $quote->load('test01', 'reserved_order_id'); - - $productRepository = $this->objectManager->create( - ProductRepositoryInterface::class - ); - $product = $productRepository->get('virtual-product', false, null, true); + $quote = $this->getQuoteByReservedOrderId->execute('test01'); + $product = $this->productRepository->get('virtual-product', false, null, true); $quote->addProduct($product); $quote->collectTotals(); - $this->assertEquals(2, $quote->getItemsQty()); $this->assertEquals(1, $quote->getVirtualItemsQty()); $this->assertEquals(20, $quote->getGrandTotal()); @@ -75,15 +134,13 @@ public function testCollectTotalsWithVirtual(): void /** * @magentoDataFixture Magento/Catalog/_files/product_virtual.php - * @magentoDataFixture Magento/Quote/_files/empty_quote.php + * * @return void */ - public function testGetAddressWithVirtualProduct() + public function testGetAddressWithVirtualProduct(): void { - /** @var Quote $quote */ $quote = $this->objectManager->create(Quote::class); - $quote->load('reserved_order_id_1', 'reserved_order_id'); - $billingAddress = $this->objectManager->create(AddressInterface::class); + $billingAddress = $this->addressFactory->create(); $billingAddress->setFirstname('Joe') ->setLastname('Doe') ->setCountryId('US') @@ -93,7 +150,7 @@ public function testGetAddressWithVirtualProduct() ->setPostcode('11501') ->setTelephone('123456789'); $quote->setBillingAddress($billingAddress); - $shippingAddress = $this->objectManager->create(AddressInterface::class); + $shippingAddress = $this->addressFactory->create(); $shippingAddress->setFirstname('Joe') ->setLastname('Doe') ->setCountryId('US') @@ -103,10 +160,7 @@ public function testGetAddressWithVirtualProduct() ->setPostcode('07102') ->setTelephone('9734685221'); $quote->setShippingAddress($shippingAddress); - $productRepository = $this->objectManager->create( - ProductRepositoryInterface::class - ); - $product = $productRepository->get('virtual-product', false, null, true); + $product = $this->productRepository->get('virtual-product', false, null, true); $quote->addProduct($product); $quote->save(); $expectedAddress = $quote->getBillingAddress(); @@ -118,73 +172,43 @@ public function testGetAddressWithVirtualProduct() */ public function testSetCustomerData(): void { - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - /** @var CustomerInterfaceFactory $customerFactory */ - $customerFactory = $this->objectManager->create( - CustomerInterfaceFactory::class - ); - /** @var \Magento\Framework\Api\DataObjectHelper $dataObjectHelper */ - $dataObjectHelper = $this->objectManager->create(\Magento\Framework\Api\DataObjectHelper::class); - $expected = $this->_getCustomerDataArray(); - $customer = $customerFactory->create(); - $dataObjectHelper->populateWithArray( - $customer, - $expected, - \Magento\Customer\Api\Data\CustomerInterface::class - ); - - $this->assertEquals($expected, $this->convertToArray($customer)); + $quote = $this->quoteFactory->create(); + $expected = $this->getCustomerDataArray(); + $customer = $this->customerDataFactory->create(); + $this->dataObjectHelper->populateWithArray($customer, $expected, CustomerInterfaceFactory::class); + $this->assertEquals($expected, $this->extensibleDataObjectConverter->toFlatArray($customer)); $quote->setCustomer($customer); $customer = $quote->getCustomer(); - $this->assertEquals($expected, $this->convertToArray($customer)); - $this->assertEquals('qa@example.com', $quote->getCustomerEmail()); - $this->assertEquals('Joe', $quote->getCustomerFirstname()); - $this->assertEquals('Dou', $quote->getCustomerLastname()); - $this->assertEquals('Ivan', $quote->getCustomerMiddlename()); + $this->assertEquals($expected, $this->extensibleDataObjectConverter->toFlatArray($customer)); + $this->assertEquals($expected[CustomerInterface::EMAIL], $quote->getCustomerEmail()); + $this->assertEquals($expected[CustomerInterface::FIRSTNAME], $quote->getCustomerFirstname()); + $this->assertEquals($expected[CustomerInterface::LASTNAME], $quote->getCustomerLastname()); + $this->assertEquals($expected[CustomerInterface::MIDDLENAME], $quote->getCustomerMiddlename()); } /** + * @magentoAppArea adminhtml + * * @return void */ public function testUpdateCustomerData(): void { - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $customerFactory = $this->objectManager->create( - CustomerInterfaceFactory::class - ); - /** @var \Magento\Framework\Api\DataObjectHelper $dataObjectHelper */ - $dataObjectHelper = $this->objectManager->create(\Magento\Framework\Api\DataObjectHelper::class); - $expected = $this->_getCustomerDataArray(); - //For save in repository - $expected = $this->removeIdFromCustomerData($expected); - $customerDataSet = $customerFactory->create(); - $dataObjectHelper->populateWithArray( - $customerDataSet, - $expected, - \Magento\Customer\Api\Data\CustomerInterface::class - ); - $this->assertEquals($expected, $this->convertToArray($customerDataSet)); - /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository - */ - $customerRepository = $this->objectManager - ->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $customerRepository->save($customerDataSet); + $quote = $this->quoteFactory->create(); + $expected = $this->getCustomerDataArray(); + unset($expected[CustomerInterface::ID]); + $customerDataSet = $this->customerDataFactory->create(); + $this->dataObjectHelper->populateWithArray($customerDataSet, $expected, CustomerInterface::class); + $this->assertEquals($expected, $this->extensibleDataObjectConverter->toFlatArray($customerDataSet)); + $customer = $this->customerRepository->save($customerDataSet); + $this->customerIdToDelete = $customer->getId(); $quote->setCustomer($customerDataSet); - $expected = $this->_getCustomerDataArray(); - $expected = $this->changeEmailInCustomerData('test@example.com', $expected); - $customerDataUpdated = $customerFactory->create(); - $dataObjectHelper->populateWithArray( - $customerDataUpdated, - $expected, - \Magento\Customer\Api\Data\CustomerInterface::class - ); + $expected = $this->getCustomerDataArray(); + $expected[CustomerInterface::EMAIL] = 'test@example.com'; + $customerDataUpdated = $this->customerDataFactory->create(); + $this->dataObjectHelper->populateWithArray($customerDataUpdated, $expected, CustomerInterface::class); $quote->updateCustomerData($customerDataUpdated); $customer = $quote->getCustomer(); - $expected = $this->changeEmailInCustomerData('test@example.com', $expected); - $actual = $this->convertToArray($customer); + $actual = $this->extensibleDataObjectConverter->toFlatArray($customer); foreach ($expected as $item) { $this->assertContains($item, $actual); } @@ -198,19 +222,11 @@ public function testUpdateCustomerData(): void */ public function testGetCustomerGroupFromCustomer(): void { - /** Preconditions */ - /** @var CustomerInterfaceFactory $customerFactory */ - $customerFactory = $this->objectManager->create( - CustomerInterfaceFactory::class - ); $customerGroupId = 3; - $customerData = $customerFactory->create()->setId(1)->setGroupId($customerGroupId); - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); + $customerData = $this->customerDataFactory->create()->setId(1)->setGroupId($customerGroupId); + $quote = $this->quoteFactory->create(); $quote->setCustomer($customerData); $quote->unsetData('customer_group_id'); - - /** Execute SUT */ $this->assertEquals($customerGroupId, $quote->getCustomerGroupId(), "Customer group ID is invalid"); } @@ -220,19 +236,11 @@ public function testGetCustomerGroupFromCustomer(): void */ public function testGetCustomerTaxClassId(): void { - /** - * Preconditions: create quote and assign ID of customer group created in fixture to it. - */ $fixtureGroupCode = 'custom_group'; $fixtureTaxClassId = 3; - /** @var \Magento\Customer\Model\Group $group */ - $group = $this->objectManager->create(\Magento\Customer\Model\Group::class); - $fixtureGroupId = $group->load($fixtureGroupCode, 'customer_group_code')->getId(); - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); + $fixtureGroupId = $this->groupFactory->create()->load($fixtureGroupCode, 'customer_group_code')->getId(); + $quote = $this->quoteFactory->create(); $quote->setCustomerGroupId($fixtureGroupId); - - /** Execute SUT */ $this->assertEquals($fixtureTaxClassId, $quote->getCustomerTaxClassId(), 'Customer tax class ID is invalid.'); } @@ -246,60 +254,37 @@ public function testGetCustomerTaxClassId(): void */ public function testAssignCustomerWithAddressChangeAddressesNotSpecified(): void { - /** Preconditions: - * Customer with two addresses created - * First address is default billing, second is default shipping. - */ - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $customerData = $this->_prepareQuoteForTestAssignCustomerWithAddressChange($quote); - - /** Execute SUT */ + $quote = $this->quoteFactory->create(); + $customerData = $this->prepareQuoteForTestAssignCustomerWithAddressChange($quote); $quote->assignCustomerWithAddressChange($customerData); - - /** Check if SUT caused expected effects */ $fixtureCustomerId = 1; $this->assertEquals($fixtureCustomerId, $quote->getCustomerId(), 'Customer ID in quote is invalid.'); $expectedBillingAddressData = [ - 'street' => 'Green str, 67', - 'telephone' => 3468676, - 'postcode' => 75477, - 'country_id' => 'US', - 'city' => 'CityM', - 'lastname' => 'Smith', - 'firstname' => 'John', - 'customer_id' => 1, - 'customer_address_id' => 1, - 'region_id' => 1 + AddressInterface::KEY_STREET => 'Green str, 67', + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => 75477, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityM', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_CUSTOMER_ID => 1, + AddressInterface::CUSTOMER_ADDRESS_ID => 1, + AddressInterface::KEY_REGION_ID => 1, ]; - $billingAddress = $quote->getBillingAddress(); - foreach ($expectedBillingAddressData as $field => $value) { - $this->assertEquals( - $value, - $billingAddress->getData($field), - "'{$field}' value in quote billing address is invalid." - ); - } + $this->assertQuoteAddress($expectedBillingAddressData, $quote->getBillingAddress()); $expectedShippingAddressData = [ - 'customer_address_id' => 2, - 'telephone' => 3234676, - 'postcode' => 47676, - 'country_id' => 'US', - 'city' => 'CityX', - 'street' => 'Black str, 48', - 'lastname' => 'Smith', - 'firstname' => 'John', - 'customer_id' => 1, - 'region_id' => 1 + AddressInterface::CUSTOMER_ADDRESS_ID => 2, + AddressInterface::KEY_TELEPHONE => 3234676, + AddressInterface::KEY_POSTCODE => 47676, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityX', + AddressInterface::KEY_STREET => 'Black str, 48', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_CUSTOMER_ID => 1, + AddressInterface::KEY_REGION_ID => 1, ]; - $shippingAddress = $quote->getShippingAddress(); - foreach ($expectedShippingAddressData as $field => $value) { - $this->assertEquals( - $value, - $shippingAddress->getData($field), - "'{$field}' value in quote shipping address is invalid." - ); - } + $this->assertQuoteAddress($expectedShippingAddressData, $quote->getShippingAddress()); } /** @@ -312,63 +297,37 @@ public function testAssignCustomerWithAddressChangeAddressesNotSpecified(): void */ public function testAssignCustomerWithAddressChange(): void { - /** Preconditions: - * Customer with two addresses created - * First address is default billing, second is default shipping. - */ - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $customerData = $this->_prepareQuoteForTestAssignCustomerWithAddressChange($quote); - /** @var \Magento\Quote\Model\Quote\Address $quoteBillingAddress */ + $quote = $this->quoteFactory->create(); + $customerData = $this->prepareQuoteForTestAssignCustomerWithAddressChange($quote); $expectedBillingAddressData = [ - 'street' => 'Billing str, 67', - 'telephone' => 16546757, - 'postcode' => 2425457, - 'country_id' => 'US', - 'city' => 'CityBilling', - 'lastname' => 'LastBilling', - 'firstname' => 'FirstBilling', - 'region_id' => 1 + AddressInterface::KEY_STREET => 'Billing str, 67', + AddressInterface::KEY_TELEPHONE => 16546757, + AddressInterface::KEY_POSTCODE => 2425457, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityBilling', + AddressInterface::KEY_LASTNAME => 'LastBilling', + AddressInterface::KEY_FIRSTNAME => 'FirstBilling', + AddressInterface::KEY_REGION_ID => 1, ]; - $quoteBillingAddress = $this->objectManager->create(\Magento\Quote\Model\Quote\Address::class); + $quoteBillingAddress = $this->addressFactory->create(); $quoteBillingAddress->setData($expectedBillingAddressData); - $expectedShippingAddressData = [ - 'telephone' => 787878787, - 'postcode' => 117785, - 'country_id' => 'US', - 'city' => 'CityShipping', - 'street' => 'Shipping str, 48', - 'lastname' => 'LastShipping', - 'firstname' => 'FirstShipping', - 'region_id' => 1 + AddressInterface::KEY_TELEPHONE => 787878787, + AddressInterface::KEY_POSTCODE => 117785, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityShipping', + AddressInterface::KEY_STREET => 'Shipping str, 48', + AddressInterface::KEY_LASTNAME => 'LastShipping', + AddressInterface::KEY_FIRSTNAME => 'FirstShipping', + AddressInterface::KEY_REGION_ID => 1, ]; - $quoteShippingAddress = $this->objectManager->create(\Magento\Quote\Model\Quote\Address::class); + $quoteShippingAddress = $this->addressFactory->create(); $quoteShippingAddress->setData($expectedShippingAddressData); - - /** Execute SUT */ $quote->assignCustomerWithAddressChange($customerData, $quoteBillingAddress, $quoteShippingAddress); - - /** Check if SUT caused expected effects */ $fixtureCustomerId = 1; $this->assertEquals($fixtureCustomerId, $quote->getCustomerId(), 'Customer ID in quote is invalid.'); - - $billingAddress = $quote->getBillingAddress(); - foreach ($expectedBillingAddressData as $field => $value) { - $this->assertEquals( - $value, - $billingAddress->getData($field), - "'{$field}' value in quote billing address is invalid." - ); - } - $shippingAddress = $quote->getShippingAddress(); - foreach ($expectedShippingAddressData as $field => $value) { - $this->assertEquals( - $value, - $shippingAddress->getData($field), - "'{$field}' value in quote shipping address is invalid." - ); - } + $this->assertQuoteAddress($expectedBillingAddressData, $quote->getBillingAddress()); + $this->assertQuoteAddress($expectedShippingAddressData, $quote->getShippingAddress()); } /** @@ -379,14 +338,11 @@ public function testAssignCustomerWithAddressChange(): void * @magentoDataFixture Magento/Backend/_files/allowed_countries_fr.php * @return void */ - public function testAssignCustomerWithAddressChangeWithNotAllowedCountry() + public function testAssignCustomerWithAddressChangeWithNotAllowedCountry(): void { - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $customerData = $this->_prepareQuoteForTestAssignCustomerWithAddressChange($quote); + $quote = $this->quoteFactory->create(); + $customerData = $this->prepareQuoteForTestAssignCustomerWithAddressChange($quote); $quote->assignCustomerWithAddressChange($customerData); - - /** Check that addresses are empty */ $this->assertNull($quote->getBillingAddress()->getCountryId()); $this->assertNull($quote->getShippingAddress()->getCountryId()); } @@ -397,17 +353,9 @@ public function testAssignCustomerWithAddressChangeWithNotAllowedCountry() */ public function testAddProductUpdateItem(): void { - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $quote->load('test01', 'reserved_order_id'); - + $quote = $this->quoteFactory->create(); $productStockQty = 100; - - $productRepository = $this->objectManager->create( - ProductRepositoryInterface::class - ); - $product = $productRepository->get('simple-1', false, null, true); - + $product = $this->productRepository->get('simple-1', false, null, true); $quote->addProduct($product, 50); $quote->setTotalsCollectedFlag(false)->collectTotals(); $this->assertEquals(50, $quote->getItemsQty()); @@ -418,98 +366,19 @@ public function testAddProductUpdateItem(): void 'related_product' => '', 'product' => $product->getId(), 'qty' => 1, - 'id' => 0 + 'id' => 0, ]; $updateParams = new \Magento\Framework\DataObject($params); $quote->updateItem($updateParams['id'], $updateParams); $quote->setTotalsCollectedFlag(false)->collectTotals(); $this->assertEquals(1, $quote->getItemsQty()); - - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); // TODO: fix test or implementation as described in https://github.com/magento-engcom/msi/issues/1037 // $this->expectExceptionMessage('The requested qty is not available'); $updateParams['qty'] = $productStockQty + 1; $quote->updateItem($updateParams['id'], $updateParams); } - /** - * Prepare quote for testing assignCustomerWithAddressChange method. - * - * Customer with two addresses created. First address is default billing, second is default shipping. - * - * @param Quote $quote - * @return CustomerInterface - */ - protected function _prepareQuoteForTestAssignCustomerWithAddressChange(Quote $quote): CustomerInterface - { - $customerRepository = $this->objectManager->create( - CustomerRepositoryInterface::class - ); - $fixtureCustomerId = 1; - /** @var \Magento\Customer\Model\Customer $customer */ - $customer = $this->objectManager->create(\Magento\Customer\Model\Customer::class); - $fixtureSecondAddressId = 2; - $customer->load($fixtureCustomerId)->setDefaultShipping($fixtureSecondAddressId)->save(); - $customerData = $customerRepository->getById($fixtureCustomerId); - $this->assertEmpty( - $quote->getBillingAddress()->getId(), - "Precondition failed: billing address should be empty." - ); - $this->assertEmpty( - $quote->getShippingAddress()->getId(), - "Precondition failed: shipping address should be empty." - ); - return $customerData; - } - - /** - * @param string $email - * @param array $customerData - * @return array - */ - protected function changeEmailInCustomerData(string $email, array $customerData): array - { - $customerData[\Magento\Customer\Model\Data\Customer::EMAIL] = $email; - return $customerData; - } - - /** - * @param array $customerData - * @return array - */ - protected function removeIdFromCustomerData(array $customerData): array - { - unset($customerData[\Magento\Customer\Model\Data\Customer::ID]); - return $customerData; - } - - /** - * @return array - */ - protected function _getCustomerDataArray(): array - { - return [ - Customer::CONFIRMATION => 'test', - Customer::CREATED_AT => '2/3/2014', - Customer::CREATED_IN => 'Default', - Customer::DEFAULT_BILLING => 'test', - Customer::DEFAULT_SHIPPING => 'test', - Customer::DOB => '2014-02-03 00:00:00', - Customer::EMAIL => 'qa@example.com', - Customer::FIRSTNAME => 'Joe', - Customer::GENDER => 0, - Customer::GROUP_ID => \Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID, - Customer::ID => 1, - Customer::LASTNAME => 'Dou', - Customer::MIDDLENAME => 'Ivan', - Customer::PREFIX => 'Dr.', - Customer::STORE_ID => 1, - Customer::SUFFIX => 'Jr.', - Customer::TAXVAT => 1, - Customer::WEBSITE_ID => 1 - ]; - } - /** * Test to verify that reserved_order_id will be changed if it already in used * @@ -519,9 +388,7 @@ protected function _getCustomerDataArray(): array */ public function testReserveOrderId(): void { - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $quote->load('reserved_order_id', 'reserved_order_id'); + $quote = $this->getQuoteByReservedOrderId->execute('reserved_order_id'); $quote->reserveOrderId(); $this->assertEquals('reserved_order_id', $quote->getReservedOrderId()); $quote->setReservedOrderId('100000001'); @@ -536,19 +403,10 @@ public function testReserveOrderId(): void */ public function testAddedProductToQuoteIsSalable(): void { - $productId = 99; - - /** @var ProductRepository $productRepository */ - $productRepository = $this->objectManager->get(ProductRepository::class); - - /** @var Quote $quote */ - $product = $productRepository->getById($productId, false, null, true); - + $product = $this->productRepository->getById(99, false, null, true); $this->expectException(LocalizedException::class); - $this->expectExceptionMessage('Product that you are trying to add is not available.'); - - $quote = $this->objectManager->create(Quote::class); - $quote->addProduct($product); + $this->expectExceptionMessage((string)__('Product that you are trying to add is not available.')); + $this->quoteFactory->create()->addProduct($product); } /** @@ -558,20 +416,15 @@ public function testAddedProductToQuoteIsSalable(): void */ public function testGetItemById(): void { - $quote = $this->objectManager->create(Quote::class); - $quote->load('test01', 'reserved_order_id'); - - $quoteItem = $this->objectManager->create(\Magento\Quote\Model\Quote\Item::class); - - $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); - $product = $productRepository->get('simple'); - + $quote = $this->getQuoteByReservedOrderId->execute('test01'); + $quoteItem = $this->itemFactory->create(); + $product = $this->productRepository->get('simple'); $quoteItem->setProduct($product); $quote->addItem($quoteItem); $quote->save(); - - $this->assertInstanceOf(\Magento\Quote\Model\Quote\Item::class, $quote->getItemById($quoteItem->getId())); - $this->assertEquals($quoteItem->getId(), $quote->getItemById($quoteItem->getId())->getId()); + $item = $quote->getItemById($quoteItem->getId()); + $this->assertInstanceOf(CartItemInterface::class, $item); + $this->assertEquals($quoteItem->getId(), $item->getId()); } /** @@ -586,42 +439,32 @@ public function testGetItemById(): void * * @magentoDataFixture Magento/Sales/_files/quote.php * @dataProvider giftMessageDataProvider - * @throws LocalizedException * @return void */ public function testMerge( - $guestItemGiftMessageId, - $customerItemGiftMessageId, - $guestOrderGiftMessageId, - $customerOrderGiftMessageId, - $expectedItemGiftMessageId, - $expectedOrderGiftMessageId + ?int $guestItemGiftMessageId, + ?int $customerItemGiftMessageId, + ?int $guestOrderGiftMessageId, + ?int $customerOrderGiftMessageId, + ?int $expectedItemGiftMessageId, + ?int $expectedOrderGiftMessageId ): void { - $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); - $product = $productRepository->get('simple', false, null, true); - - /** @var Quote $quote */ - $guestQuote = $this->getQuote('test01'); + $product = $this->productRepository->get('simple', false, null, true); + $guestQuote = $this->getQuoteByReservedOrderId->execute('test01'); $guestQuote->setGiftMessageId($guestOrderGiftMessageId); - - /** @var Quote $customerQuote */ - $customerQuote = $this->objectManager->create(Quote::class); + $customerQuote = $this->quoteFactory->create(); $customerQuote->setReservedOrderId('test02') ->setStoreId($guestQuote->getStoreId()) ->addProduct($product); $customerQuote->setGiftMessageId($customerOrderGiftMessageId); - $guestItem = $guestQuote->getItemByProduct($product); $guestItem->setGiftMessageId($guestItemGiftMessageId); - $customerItem = $customerQuote->getItemByProduct($product); $customerItem->setGiftMessageId($customerItemGiftMessageId); - $customerQuote->merge($guestQuote); $mergedItemItem = $customerQuote->getItemByProduct($product); - - self::assertEquals($expectedOrderGiftMessageId, $customerQuote->getGiftMessageId()); - self::assertEquals($expectedItemGiftMessageId, $mergedItemItem->getGiftMessageId()); + $this->assertEquals($expectedOrderGiftMessageId, $customerQuote->getGiftMessageId()); + $this->assertEquals($expectedItemGiftMessageId, $mergedItemItem->getGiftMessageId()); } /** @@ -638,7 +481,7 @@ public function giftMessageDataProvider(): array 'guestOrderId' => null, 'customerOrderId' => 11, 'expectedItemId' => 1, - 'expectedOrderId' => 11 + 'expectedOrderId' => 11, ], [ 'guestItemId' => 1, @@ -646,28 +489,252 @@ public function giftMessageDataProvider(): array 'guestOrderId' => 11, 'customerOrderId' => 22, 'expectedItemId' => 1, - 'expectedOrderId' => 11 - ] + 'expectedOrderId' => 11, + ], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_custom_file_option.php + * + * @return void + */ + public function testAddProductWithoutChosenOptions(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple_with_custom_file_option'); + $result = $quote->addProduct($product); + $this->assertEquals( + (string)__( + 'The product\'s required option(s) weren\'t entered. Make sure the options are entered and try again.' + ), + $result + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * + * @return void + */ + public function testAddProductWithInvalidRequestParams(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-1'); + $this->expectExceptionObject( + new LocalizedException(__('We found an invalid request for adding product to quote.')) + ); + $quote->addProduct($product, ''); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + * + * @return void + */ + public function testAddProductOutOfStock(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-out-of-stock'); + $this->expectExceptionObject( + new LocalizedException(__('Product that you are trying to add is not available.')) + ); + $quote->addProduct($product, 1); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * + * @return void + */ + public function testAddProductWithMoreQty(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-1'); + $this->expectExceptionObject(new LocalizedException(__('The requested qty is not available'))); + $quote->addProduct($product, 1500); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/simple_product_with_qty_increments.php + * + * @return void + */ + public function testAddProductWithQtyIncrements(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple_product_with_qty_increments'); + $this->expectExceptionObject( + new LocalizedException(__('You can buy this product only in quantities of %1 at a time.', 3)) + ); + $quote->addProduct($product, 1); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/simple_product_min_max_sale_qty.php + * @magentoAppIsolation enabled + * + * @return void + */ + public function testAddProductWithMinSaleQty(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple_product_min_max_sale_qty'); + $messages = [ + (string)__('The fewest you may purchase is %1.', 5), + (string)__('The fewest you may purchase is %1', 5), + ]; + $this->expectException(LocalizedException::class); + $this->expectExceptionMessageMatches('/' . implode('|', $messages) . '/'); + $quote->addProduct($product, 1); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/simple_product_min_max_sale_qty.php + * + * @return void + */ + public function testAddProductWithMaxSaleQty(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple_product_min_max_sale_qty'); + $messages = [ + (string)__('The most you may purchase is %1.', 20), + (string)__('The requested qty exceeds the maximum qty allowed in shopping cart'), + ]; + $this->expectException(LocalizedException::class); + $this->expectExceptionMessageMatches('/' . implode('|', $messages) . '/'); + $quote->addProduct($product, 25); + } + + /** + * @magentoConfigFixture current_store cataloginventory/item_options/enable_qty_increments 1 + * @magentoConfigFixture current_store cataloginventory/item_options/qty_increments 3 + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoAppIsolation enabled + * + * @return void + */ + public function testAddProductWithConfigQtyIncrements(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-1'); + $this->expectExceptionObject( + new LocalizedException(__('You can buy this product only in quantities of %1 at a time.', 3)) + ); + $quote->addProduct($product, 1); + } + + /** + * @magentoConfigFixture current_store cataloginventory/item_options/min_sale_qty 5 + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoAppIsolation enabled + * + * @return void + */ + public function testAddProductWithConfigMinSaleQty(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-1'); + $messages = [ + (string)__('The fewest you may purchase is %1.', 5), + (string)__('The fewest you may purchase is %1', 5), + ]; + $this->expectException(LocalizedException::class); + $this->expectExceptionMessageMatches('/' . implode('|', $messages) . '/'); + $quote->addProduct($product, 1); + } + + /** + * @magentoConfigFixture current_store cataloginventory/item_options/max_sale_qty 20 + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoAppIsolation enabled + * + * @return void + */ + public function testAddProductWithConfigMaxSaleQty(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-1'); + $messages = [ + (string)__('The most you may purchase is %1.', 20), + (string)__('The requested qty exceeds the maximum qty allowed in shopping cart'), ]; + $this->expectException(LocalizedException::class); + $this->expectExceptionMessageMatches('/' . implode('|', $messages) . '/'); + $quote->addProduct($product, 25); } /** - * Gets quote by reserved order id. + * Assert address in quote. * - * @param string $reservedOrderId - * @return Quote + * @param array $expectedAddress + * @param AddressInterface $quoteAddress + * @return void + */ + private function assertQuoteAddress(array $expectedAddress, AddressInterface $quoteAddress): void + { + foreach ($expectedAddress as $field => $value) { + $this->assertEquals( + $value, + $quoteAddress->getData($field), + sprintf('"%s" value in quote %s address is invalid.', $field, $quoteAddress->getAddressType()) + ); + } + } + + /** + * Prepare quote for testing assignCustomerWithAddressChange method. + * Customer with two addresses created. First address is default billing, second is default shipping. + * + * @param CartInterface $quote + * @return CustomerInterface */ - private function getQuote(string $reservedOrderId): Quote + private function prepareQuoteForTestAssignCustomerWithAddressChange(CartInterface $quote): CustomerInterface { - /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); - $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) - ->create(); + $fixtureCustomerId = 1; + $fixtureSecondAddressId = 2; + $customer = $this->customerFactory->create(); + $this->customerResourceModel->load($customer, $fixtureCustomerId); + $customer->setDefaultShipping($fixtureSecondAddressId); + $this->customerResourceModel->save($customer); + $customerData = $customer->getDataModel(); + $this->assertEmpty( + $quote->getBillingAddress()->getId(), + "Precondition failed: billing address should be empty." + ); + $this->assertEmpty( + $quote->getShippingAddress()->getId(), + "Precondition failed: shipping address should be empty." + ); - /** @var CartRepositoryInterface $quoteRepository */ - $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); - $items = $quoteRepository->getList($searchCriteria)->getItems(); + return $customerData; + } - return array_pop($items); + /** + * @return array + */ + private function getCustomerDataArray(): array + { + return [ + CustomerInterface::CONFIRMATION => 'test', + CustomerInterface::CREATED_AT => '2/3/2014', + CustomerInterface::CREATED_IN => 'Default', + CustomerInterface::DEFAULT_BILLING => 'test', + CustomerInterface::DEFAULT_SHIPPING => 'test', + CustomerInterface::DOB => '2014-02-03 00:00:00', + CustomerInterface::EMAIL => 'qa@example.com', + CustomerInterface::FIRSTNAME => 'Joe', + CustomerInterface::GENDER => 0, + CustomerInterface::GROUP_ID => GroupManagement::NOT_LOGGED_IN_ID, + CustomerInterface::ID => 1, + CustomerInterface::LASTNAME => 'Dou', + CustomerInterface::MIDDLENAME => 'Ivan', + CustomerInterface::PREFIX => 'Dr.', + CustomerInterface::STORE_ID => 1, + CustomerInterface::SUFFIX => 'Jr.', + CustomerInterface::TAXVAT => 1, + CustomerInterface::WEBSITE_ID => 1, + ]; } } 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..f2ae33ee85093 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 @@ -5,28 +5,43 @@ */ namespace Magento\Quote\Observer\Frontend\Quote\Address; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Group; +use Magento\Framework\Event\Observer; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address\Total; +use Magento\Quote\Model\Shipping; +use Magento\Quote\Model\ShippingAssignment; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; -class CollectTotalsObserverTest extends \PHPUnit\Framework\TestCase +/** + * Test for \Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver. + */ +class CollectTotalsObserverTest extends TestCase { + private const STUB_CUSTOMER_EMAIL = 'customer@example.com'; + /** - * @var \Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver + * @var CollectTotalsObserver */ - protected $model; + private $model; /** - * Object Manager - * - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; + /** + * @inheridoc + */ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); - $this->model = $this->objectManager->create( - \Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver::class - ); + $this->model = $this->objectManager->create(CollectTotalsObserver::class); } /** @@ -37,37 +52,37 @@ protected function setUp(): void * * @covers \Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver::execute */ - public function testChangeQuoteCustomerGroupIdForCustomerWithDisabledAutomaticGroupChange() + public function testChangeQuoteCustomerGroupIdForCustomerWithDisabledAutomaticGroupChange(): void { - /** @var \Magento\Framework\ObjectManagerInterface $objectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var ObjectManagerInterface $objectManager */ + $objectManager = Bootstrap::getObjectManager(); - /** @var $customer \Magento\Customer\Model\Customer */ - $customer = $objectManager->create(\Magento\Customer\Model\Customer::class); + /** @var $customer Customer */ + $customer = $objectManager->create(Customer::class); $customer->load(1); $customer->setDisableAutoGroupChange(1); $customer->setGroupId(2); $customer->save(); - /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ - $customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = $objectManager->create(CustomerRepositoryInterface::class); $customerData = $customerRepository->getById($customer->getId()); - /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var $quote Quote */ + $quote = $objectManager->create(Quote::class); $quote->load('test01', 'reserved_order_id'); $quote->setCustomer($customerData); $quoteAddress = $quote->getBillingAddress(); - $shippingAssignment = $this->objectManager->create(\Magento\Quote\Model\ShippingAssignment::class); - $shipping = $this->objectManager->create(\Magento\Quote\Model\Shipping::class); + $shippingAssignment = $this->objectManager->create(ShippingAssignment::class); + $shipping = $this->objectManager->create(Shipping::class); $shipping->setAddress($quoteAddress); $shippingAssignment->setShipping($shipping); - /** @var \Magento\Quote\Model\Quote\Address\Total $total */ - $total = $this->objectManager->create(\Magento\Quote\Model\Quote\Address\Total::class); + /** @var Total $total */ + $total = $this->objectManager->create(Total::class); $eventObserver = $objectManager->create( - \Magento\Framework\Event\Observer::class, + Observer::class, ['data' => [ 'quote' => $quote, 'shipping_assignment' => $shippingAssignment, @@ -88,51 +103,80 @@ public function testChangeQuoteCustomerGroupIdForCustomerWithDisabledAutomaticGr * * @covers \Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver::execute */ - public function testChangeQuoteCustomerGroupIdForCustomerWithEnabledAutomaticGroupChange() + public function testChangeQuoteCustomerGroupIdForCustomerWithEnabledAutomaticGroupChange(): void { - /** @var \Magento\Framework\ObjectManagerInterface $objectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var ObjectManagerInterface $objectManager */ + $objectManager = Bootstrap::getObjectManager(); - /** @var $customer \Magento\Customer\Model\Customer */ - $customer = $objectManager->create(\Magento\Customer\Model\Customer::class); + /** @var $customer Customer */ + $customer = $objectManager->create(Customer::class); $customer->load(1); $customer->setDisableAutoGroupChange(0); $customer->setGroupId(2); $customer->save(); - /** @var \Magento\Customer\Model\CustomerRegistry $customerRegistry */ - $customerRegistry = $objectManager->get(\Magento\Customer\Model\CustomerRegistry::class); + /** @var CustomerRegistry $customerRegistry */ + $customerRegistry = $objectManager->get(CustomerRegistry::class); $customerRegistry->remove($customer->getId()); - /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ - $customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = $objectManager->create(CustomerRepositoryInterface::class); $customerData = $customerRepository->getById($customer->getId()); - /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var $quote Quote */ + $quote = $objectManager->create(Quote::class); $quote->load('test01', 'reserved_order_id'); $quote->setCustomer($customerData); $quoteAddress = $quote->getBillingAddress(); - $shippingAssignment = $this->objectManager->create(\Magento\Quote\Model\ShippingAssignment::class); - $shipping = $this->objectManager->create(\Magento\Quote\Model\Shipping::class); + $shippingAssignment = $this->objectManager->create(ShippingAssignment::class); + $shipping = $this->objectManager->create(Shipping::class); $shipping->setAddress($quoteAddress); $shippingAssignment->setShipping($shipping); - /** @var \Magento\Quote\Model\Quote\Address\Total $total */ - $total = $this->objectManager->create(\Magento\Quote\Model\Quote\Address\Total::class); + /** @var Total $total */ + $total = $this->objectManager->create(Total::class); $eventObserver = $objectManager->create( - \Magento\Framework\Event\Observer::class, - ['data' => [ - 'quote' => $quote, - 'shipping_assignment' => $shippingAssignment, - 'total' => $total - ] - ] + Observer::class, + ['data' => ['quote' => $quote, 'shipping_assignment' => $shippingAssignment, 'total' => $total]] + ); + $this->model->execute($eventObserver); + + $this->assertEquals(2, $quote->getCustomer()->getGroupId()); + } + + /** + * Dispatch event with guest quote and check that email will not be override to null when auto group assign enabled + * + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * + * @return void + */ + public function testQuoteCustomerEmailNotChanged(): void + { + // prepare quote for guest + $quote = $this->objectManager->create(Quote::class); + $quote->setCustomerId(null) + ->setCustomerEmail(self::STUB_CUSTOMER_EMAIL) + ->setCustomerIsGuest(true) + ->setCustomerGroupId(Group::NOT_LOGGED_IN_ID); + + $quoteAddress = $quote->getBillingAddress(); + + $shippingAssignment = $this->objectManager->create(ShippingAssignment::class); + $shipping = $this->objectManager->create(Shipping::class); + $shipping->setAddress($quoteAddress); + $shippingAssignment->setShipping($shipping); + /** @var Total $total */ + $total = $this->objectManager->create(Total::class); + + $eventObserver = $this->objectManager->create( + Observer::class, + ['data' => ['quote' => $quote, 'shipping_assignment' => $shippingAssignment, 'total' => $total]] ); $this->model->execute($eventObserver); - $this->assertEquals(1, $quote->getCustomer()->getGroupId()); + $this->assertEquals(self::STUB_CUSTOMER_EMAIL, $quote->getCustomerEmail()); } } diff --git a/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStartTest.php b/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStartTest.php index c7a09163c5fa1..2ff5b7c6ee3d2 100644 --- a/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStartTest.php +++ b/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStartTest.php @@ -33,26 +33,25 @@ public function testGetElementHtml(): void $this->dispatch('backend/admin/system_config/edit/section/reports/'); $body = $this->getResponse()->getBody(); - $this->assertStringContainsString($this->getOptionsHtml('01'), $body); + $this->assertOptionSelected('01', $body); } /** - * Options html + * Assert that given option is selected. * - * @param string $selected - * @return string + * @param string $option Option value. + * @param string $content HTML content + * @return void */ - private function getOptionsHtml(string $selected): string + private function assertOptionSelected(string $option, string $content): void { - $html = ''; - foreach ($this->monthNumbers as $number) { - $html .= $number === $selected - ? '<option value="' . $selected . '" selected="selected">' . $selected . '</option>' - : '<option value="' . $number . '">' . $number . '</option>'; - - $html .= PHP_EOL; + foreach ($this->monthNumbers as $monthNumber) { + $regEx = "\<option[^\>]+value\=\\\"$monthNumber\\\"[^\>]*?"; + if ($monthNumber === $option) { + $regEx .= 'selected\=\"selected\"[^\>]*?'; + } + $regEx .= "\>$monthNumber\<\/option\>"; + $this->assertRegExp("#$regEx#", $content); } - - return $html; } } diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product.php new file mode 100644 index 0000000000000..42e0a956f6c8d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product.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\Customer\Model\Session; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Reports\Observer\CatalogProductCompareAddProductObserver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/out_of_stock_product_with_category.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +$product = $productRepository->get('out-of-stock-product'); +$session = $objectManager->get(Session::class); +/** @var CatalogProductCompareAddProductObserver $reportObserver */ +$reportObserver = $objectManager->get(CatalogProductCompareAddProductObserver::class); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originValues = [ + 'reports/options/enabled' => $config->getValue('reports/options/enabled'), + 'reports/options/product_compare_enabled' => $config->getValue('reports/options/product_compare_enabled'), +]; + +try { + $config->setValue('reports/options/enabled', 1); + $config->setValue('reports/options/product_compare_enabled', 1); + $session->loginById(1); + $reportObserver->execute( + new Observer( + [ + 'event' => new DataObject( + [ + 'product' => new DataObject(['id' => $product->getId()]), + ] + ), + ] + ) + ); +} finally { + $session->logout(); + foreach ($originValues as $key => $value) { + $config->setValue($key, $value); + } +} 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 new file mode 100644 index 0000000000000..f3d31bee387f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product_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/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_compared_product.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product.php new file mode 100644 index 0000000000000..1695247e8ba13 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Reports\Observer\CatalogProductCompareAddProductObserver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +$session = $objectManager->get(Session::class); +/** @var CatalogProductCompareAddProductObserver $reportObserver */ +$reportObserver = $objectManager->get(CatalogProductCompareAddProductObserver::class); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originValues = [ + 'reports/options/enabled' => $config->getValue('reports/options/enabled'), + 'reports/options/product_compare_enabled' => $config->getValue('reports/options/product_compare_enabled'), +]; + +try { + $config->setValue('reports/options/enabled', 1); + $config->setValue('reports/options/product_compare_enabled', 1); + $session->loginById(1); + $reportObserver->execute( + new Observer( + [ + 'event' => new DataObject( + [ + 'product' => new DataObject(['id' => 6]), + ] + ), + ] + ) + ); +} finally { + $session->logout(); + foreach ($originValues as $key => $value) { + $config->setValue($key, $value); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_rollback.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_rollback.php new file mode 100644 index 0000000000000..1181618afecdb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_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/Catalog/_files/second_product_simple_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.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer.php new file mode 100644 index 0000000000000..33e09155e7c03 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer.php @@ -0,0 +1,50 @@ +<?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\Session; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Reports\Observer\CatalogProductViewObserver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/simple_product_disabled.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var Session $session */ +$session = $objectManager->get(Session::class); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originalValue = $config->getValue('reports/options/enabled'); +/** @var CatalogProductViewObserver $reportObserver */ +$reportObserver = $objectManager->get(CatalogProductViewObserver::class); +$product = $productRepository->get('product_disabled'); + +try { + $config->setValue('reports/options/enabled', 1); + $session->loginById(1); + $reportObserver->execute( + new Observer( + [ + 'event' => new DataObject( + [ + 'product' => new DataObject(['id' => $product->getId()]), + ] + ), + ] + ) + ); +} finally { + $session->logout(); + $config->setValue('reports/options/enabled', $originalValue); +} 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 new file mode 100644 index 0000000000000..5c5ad143ac77f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer_rollback.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Reports\Observer\CatalogProductViewObserver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Session $session */ +$session = $objectManager->get(Session::class); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originalValue = $config->getValue('reports/options/enabled'); +/** @var CatalogProductViewObserver $reportObserver */ +$reportObserver = $objectManager->get(CatalogProductViewObserver::class); + +try { + $config->setValue('reports/options/enabled', 1); + $session->loginById(1); + $reportObserver->execute( + new Observer( + [ + 'event' => new DataObject( + [ + 'product' => new DataObject(['id' => 6]), + ] + ), + ] + ) + ); +} finally { + $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/Reports/_files/recently_viewed_product_by_customer.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer.php new file mode 100644 index 0000000000000..f3dedf0a35d96 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Reports\Observer\CatalogProductViewObserver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Session $session */ +$session = $objectManager->get(Session::class); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originalValue = $config->getValue('reports/options/enabled'); +/** @var CatalogProductViewObserver $reportObserver */ +$reportObserver = $objectManager->get(CatalogProductViewObserver::class); + +try { + $config->setValue('reports/options/enabled', 1); + $session->loginById(1); + $reportObserver->execute( + new Observer( + [ + 'event' => new DataObject( + [ + 'product' => new DataObject(['id' => 6]), + ] + ), + ] + ) + ); +} finally { + $session->logout(); + $config->setValue('reports/options/enabled', $originalValue); +} diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_rollback.php new file mode 100644 index 0000000000000..1181618afecdb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_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/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/Review/_files/customer_review_with_rating_rollback.php b/dev/tests/integration/testsuite/Magento/Review/_files/customer_review_with_rating_rollback.php new file mode 100644 index 0000000000000..0931d881a6fdc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/_files/customer_review_with_rating_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_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/different_reviews_rollback.php b/dev/tests/integration/testsuite/Magento/Review/_files/different_reviews_rollback.php new file mode 100644 index 0000000000000..328c1e229da5c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/_files/different_reviews_rollback.php @@ -0,0 +1,10 @@ +<?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/Catalog/_files/product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings.php b/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings.php new file mode 100644 index 0000000000000..0c097f62101f8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Review\Model\ResourceModel\Rating\Collection as RatingCollection; +use Magento\Review\Model\ResourceModel\Rating as RatingResourceModel; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +Bootstrap::getInstance()->loadArea(FrontNameResolver::AREA_CODE); + +$objectManager = Bootstrap::getObjectManager(); + +$storeId = $objectManager->get(StoreManagerInterface::class)->getStore()->getId(); + +/** @var RatingResourceModel $ratingResourceModel */ +$ratingResourceModel = $objectManager->create(RatingResourceModel::class); + +/** @var RatingCollection $ratingCollection */ +$ratingCollection = $objectManager->create(RatingCollection::class)->setOrder('rating_code', 'ASC'); +$position = 0; + +foreach ($ratingCollection as $rating) { + $rating->setStores([$storeId])->setPosition($position++); + $ratingResourceModel->save($rating); +} diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings_rollback.php b/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings_rollback.php new file mode 100644 index 0000000000000..3a96a1be17a8b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Review\Model\ResourceModel\Rating\Collection as RatingCollection; +use Magento\Review\Model\ResourceModel\Rating as RatingResourceModel; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +Bootstrap::getInstance()->loadArea(FrontNameResolver::AREA_CODE); +$objectManager = Bootstrap::getObjectManager(); + +$storeId = Bootstrap::getObjectManager()->get(StoreManagerInterface::class)->getStore()->getId(); + +/** @var RatingResourceModel $ratingResourceModel */ +$ratingResourceModel = $objectManager->create(RatingResourceModel::class); + +/** @var RatingCollection $ratingCollection */ +$ratingCollection = Bootstrap::getObjectManager()->create(RatingCollection::class); + +foreach ($ratingCollection as $rating) { + $rating->setStores([])->setPosition(0); + $ratingResourceModel->save($rating); +} diff --git a/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php b/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php index 991914460c61b..f0d61bc618054 100644 --- a/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php @@ -7,37 +7,42 @@ namespace Magento\Rss\Controller\Feed; -class IndexTest extends \Magento\TestFramework\TestCase\AbstractBackendController +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Session; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Wishlist\Model\Wishlist; + +/** + * Test for \Magento\Rss\Controller\Feed\Index. + */ +class IndexTest extends AbstractBackendController { - /** - * @var \Magento\Rss\Model\UrlBuilder - */ - private $urlBuilder; + private const RSS_NEW_PRODUCTS_PATH = 'rss/feed/index/type/new_products/'; /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface + * @var CustomerRepositoryInterface */ private $customerRepository; /** - * @var \Magento\Wishlist\Model\Wishlist + * @var Wishlist */ private $wishlist; /** - * @var + * @var Session */ private $customerSession; + /** + * @inheritDoc + */ protected function setUp(): void { parent::setUp(); - $this->urlBuilder = $this->_objectManager->get(\Magento\Rss\Model\UrlBuilder::class); - $this->customerRepository = $this->_objectManager->get( - \Magento\Customer\Api\CustomerRepositoryInterface::class - ); - $this->wishlist = $this->_objectManager->get(\Magento\Wishlist\Model\Wishlist::class); - $this->customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + $this->wishlist = $this->_objectManager->get(Wishlist::class); + $this->customerSession = $this->_objectManager->get(Session::class); } /** @@ -60,6 +65,21 @@ public function testRssResponse() $this->assertStringContainsString('<title>John Smith\'s Wishlist', $body); } + /** + * Check Rss response from `New Products`. + * + * @magentoConfigFixture current_store rss/catalog/new 1 + * @magentoConfigFixture current_store rss/config/active 1 + * + * @return void + */ + public function testRssResponseNewProducts(): void + { + $this->dispatch(self::RSS_NEW_PRODUCTS_PATH); + $body = $this->getResponse()->getBody(); + $this->assertStringContainsString('New Products from Main Website Store', $body); + } + /** * Check Rss with incorrect wishlist id. * @@ -83,7 +103,6 @@ public function testRssResponseWithIncorrectWishlistId() private function getLink($customerId, $customerEmail, $wishlistId) { - return 'rss/feed/index/type/wishlist/data/' . base64_encode($customerId . ',' . $customerEmail) . '/wishlist_id/' . $wishlistId; diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php new file mode 100644 index 0000000000000..0a8db20d86966 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php @@ -0,0 +1,75 @@ +objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Form::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + } + + /** + * @return void + */ + protected function tearDown(): void + { + $this->registry->unregister('order_address'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testGetFormValues(): void + { + $this->registry->unregister('order_address'); + $order = $this->orderFactory->create()->loadByIncrementId(100000001); + $address = $order->getShippingAddress(); + $this->registry->register('order_address', $address); + $formValues = $this->block->getFormValues(); + $this->assertEquals($address->getData(), $formValues); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/AddressTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/AddressTest.php new file mode 100644 index 0000000000000..8542d9bc48dcd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/AddressTest.php @@ -0,0 +1,111 @@ +objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Address::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->orderFactory = $this->objectManager->get(OrderFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('order_address'); + + parent::tearDown(); + } + + /** + * @dataProvider addressTypeProvider + * + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @param string $type + * @return void + */ + public function testGetHeaderText(string $type): void + { + $order = $this->orderFactory->create()->loadByIncrementId(100000001); + $address = $this->getAddressByType($order, $type); + $this->registry->unregister('order_address'); + $this->registry->register('order_address', $address); + $text = $this->block->getHeaderText(); + $this->assertEquals( + (string)__('Edit Order %1 %2 Address', $order->getIncrementId(), ucfirst($type)), + (string)$text + ); + } + + /** + * @return array + */ + public function addressTypeProvider(): array + { + return [ + 'billing_address' => [ + AddressType::TYPE_BILLING, + ], + 'shipping_address' => [ + AddressType::TYPE_SHIPPING, + ] + ]; + } + + /** + * Get address by address type + * + * @param OrderInterface $order + * @param string $type + * @return OrderAddressInterface|null + */ + private function getAddressByType(OrderInterface $order, string $type): ?OrderAddressInterface + { + return $type === AddressType::TYPE_BILLING ? $order->getBillingAddress() : $order->getShippingAddress(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/AbstractAddressFormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/AbstractAddressFormTest.php new file mode 100644 index 0000000000000..5219fd72ec94e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/AbstractAddressFormTest.php @@ -0,0 +1,125 @@ +layout = $objectManager->get(LayoutInterface::class); + $this->form = $this->getFormBlock(); + $this->customerRegistry = $objectManager->get(CustomerRegistry::class); + $this->quoteRepository = $objectManager->get(CartRepositoryInterface::class); + $this->config = $objectManager->get(ScopeConfigInterface::class); + $this->formAttributes = array_keys($objectManager->get(FormFactory::class) + ->create('customer_address', 'adminhtml_customer_address')->getAttributes()); + } + + /** + * Check that all form values are filled according to address attributes values + * + * @param int $customerId + * @return void + */ + protected function checkFormValuesExist(int $customerId): void + { + $address = $this->getAddress($customerId); + $form = $this->prepareForm($customerId); + foreach ($this->formAttributes as $attribute) { + $this->assertEquals($address->getData($attribute), $form->getElement($attribute)->getValue()); + } + } + + /** + * Check that form values is empty + * + * @param int $customerId + * @return void + */ + protected function checkFormValuesAreEmpty(int $customerId): void + { + $defaultCountryCode = $this->config->getValue(Custom::XML_PATH_GENERAL_COUNTRY_DEFAULT); + $form = $this->prepareForm($customerId); + foreach ($this->formAttributes as $attribute) { + if ($attribute === AddressInterface::COUNTRY_ID) { + $this->assertEquals($defaultCountryCode, $form->getElement($attribute)->getValue()); + continue; + } + $this->assertNull($form->getElement($attribute)->getValue()); + } + } + + /** + * Prepare form + * + * @param int $customerId + * @return Form + */ + private function prepareForm(int $customerId): Form + { + $quote = $this->quoteRepository->getForCustomer($customerId); + $this->form->getCreateOrderModel()->setQuote($quote); + + return $this->form->getForm(); + } + + /** + * Get form block + * + * @return BlockInterface + */ + abstract protected function getFormBlock(): BlockInterface; + + /** + * Get appropriate customer address + * + * @param int $customerId + * @return AddressModelInterface + */ + abstract protected function getAddress(int $customerId): AddressModelInterface; +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Billing/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Billing/FormTest.php new file mode 100644 index 0000000000000..58a4616e3a219 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Billing/FormTest.php @@ -0,0 +1,59 @@ +checkFormValuesExist(1); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testFormValuesAreEmpty(): void + { + $this->checkFormValuesAreEmpty(1); + } + + /** + * @inheritdoc + */ + protected function getFormBlock(): BlockInterface + { + return $this->layout->createBlock(Address::class); + } + + /** + * @inheritdoc + */ + protected function getAddress(int $customerId): AddressModelInterface + { + return $this->customerRegistry->retrieve($customerId)->getDefaultBillingAddress(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php index 580f5a3a1dbc9..861559acd8c20 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php @@ -94,21 +94,21 @@ public function testGetFormWithCustomer() $expectedFields = ['group_id', 'email']; $form = $this->accountBlock->getForm(); - self::assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); + $this->assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); $fieldset = $form->getElements()[0]; $content = $form->toHtml(); - self::assertEquals(count($expectedFields), $fieldset->getElements()->count()); + $this->assertEquals(count($expectedFields), $fieldset->getElements()->count()); foreach ($fieldset->getElements() as $element) { - self::assertTrue( + $this->assertTrue( in_array($element->getId(), $expectedFields), sprintf('Unexpected field "%s" in form.', $element->getId()) ); } - self::assertStringContainsString( - '', + self::assertRegExp( + '/', + self::assertRegExp( + '/\', + self::assertRegExp( + '/